3x-ui/frontend/src/pages/inbounds/InboundFormModal.tsx

2249 lines
106 KiB
TypeScript
Raw Normal View History

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
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import dayjs, { type Dayjs } from 'dayjs';
import {
Button,
Card,
Checkbox,
Col,
Divider,
Empty,
Form,
Input,
InputNumber,
Modal,
Radio,
Row,
Select,
Space,
Switch,
Tabs,
Tooltip,
Typography,
message,
} from 'antd';
import {
SyncOutlined,
PlusOutlined,
MinusOutlined,
DeleteOutlined,
CaretUpOutlined,
CaretDownOutlined,
SettingOutlined,
} from '@ant-design/icons';
import {
HttpUtil,
RandomUtil,
NumberFormatter,
SizeFormatter,
Wireguard,
} from '@/utils';
import InputAddon from '@/components/InputAddon';
import { getRandomRealityTarget } from '@/models/reality-targets';
import {
Inbound,
Protocols,
SSMethods,
SNIFFING_OPTION,
TLS_VERSION_OPTION,
TLS_CIPHER_OPTION,
UTLS_FINGERPRINT,
ALPN_OPTION,
USAGE_OPTION,
DOMAIN_STRATEGY_OPTION,
TCP_CONGESTION_OPTION,
MODE_OPTION,
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
} from '@/models/inbound';
import { DBInbound } from '@/models/dbinbound';
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
import FinalMaskForm from '@/components/FinalMaskForm';
import DateTimePicker from '@/components/DateTimePicker';
import JsonEditor from '@/components/JsonEditor';
feat(frontend): TanStack Query + React Router migration & in-panel API docs (#4541) * feat(frontend): introduce TanStack Query with status polling Wires @tanstack/react-query into every entry and migrates useStatus to useStatusQuery as the foundation for the multi-page MPA → SPA migration. - QueryProvider wraps each entry inside ThemeProvider, with devtools gated on import.meta.env.DEV - Shared queryClient: 30s staleTime, refetchOnWindowFocus, 1 retry - useStatusQuery preserves the { status, fetched, refresh } shape so IndexPage swaps in without further changes - refetchIntervalInBackground:false stops the 2s status poll when the panel tab is hidden, cutting idle traffic against the server * feat(frontend): collapse panel pages into a single React Router SPA Replaces the 7-entry MPA shell (index/clients/inbounds/nodes/settings/ xray/api-docs HTML files) with one main.tsx + createBrowserRouter. The Go backend now serves the same index.html for every authenticated panel route; React Router reads the URL and mounts the page from cache on subsequent navigation — no more full reloads between tabs. Frontend - main.tsx: single bootstrap (setupAxios, i18n, ThemeProvider, QueryProvider, RouterProvider) replacing 7 near-duplicate entries - routes.tsx: declarative router with lazy()-loaded pages, basename derived from window.X_UI_BASE_PATH so panels at /secret/panel work - layouts/PanelLayout.tsx: shell mount-point for the WS → queryClient bridge so connection survives navigation - api/websocketBridge.ts: subscribes the singleton WebSocketClient to queryClient and dispatches invalidate/outbounds events to cached queries (page-level useWebSocket handlers stay until Phase 3 hooks migrate) - AppSidebar: navigates via useNavigate + useLocation instead of window.location.href; drops basePath/requestUri props - Pages: drop the unused basePath/requestUri locals exposed only for the old sidebar Build - vite.config: 9 rollup inputs → 3 (index, login, subpage). Dev proxy bypass collapses /panel/* to index.html and skips API prefixes - vendor-tanstack + vendor-router chunks added to manualChunks Backend - xui.go: 7 per-page HTML handlers → one panelSPA handler serving index.html for /, /inbounds, /clients, /nodes, /settings, /xray, /api-docs. The /panel/api, /panel/setting, /panel/xray sub-routers are untouched * feat(frontend): migrate useNodes to TanStack Query Splits the hand-rolled useNodes hook into useNodesQuery (server data + NodeRecord type + derived totals) and useNodeMutations (add/update/del/ setEnable/probe/test). Mutations invalidate ['nodes'] on success, so the list refreshes without each call awaiting a manual refresh(). NodesPage drops useWebSocket({ nodes: applyNodesEvent }) — the WebSocket → query bridge now forwards the 'nodes' push to setQueryData(['nodes', 'list']) once at the SPA root. InboundsPage and the inbound form/list components import NodeRecord from its new home next to the query hook. * feat(frontend): migrate useAllSetting to TanStack Query Replaces the hand-rolled fetch + dirty-tracking hook with useAllSettings backed by useQuery + useMutation. The draft (current edits) is kept in local state and reset whenever query.data lands. saveAll posts the draft via a mutation; on success, invalidating ['settings'] refetches and the useEffect resets the draft so saveDisabled flips back to true. staleTime: Infinity prevents refetchOnWindowFocus from clobbering in-flight edits — settings only change in response to this user's own save. setSpinning stays as a pass-through to a local flag so the existing restartPanel flow in SettingsPage keeps showing its spinner. * feat(frontend): route useInbounds fetches through TanStack Query Rewrites useInbounds so its four server fetches (slim list, default settings, online clients, last-online map) live in useQuery with staleTime: Infinity. The in-place WS merge logic for traffic and client_stats is preserved — applyTrafficEvent / applyClientStatsEvent still mutate the locally-mirrored dbInbounds so the panel doesn't refetch every 1-2 seconds when stats stream in. refresh() becomes a thin invalidateQueries on the three list keys, which mutations in the page already call after add/edit/del. The bridge now forwards the WebSocket 'inbounds' push to setQueryData(['inbounds', 'slim']), and InboundsPage drops its useEffect(fetchDefaultSettings → refresh) plus the invalidate / inbounds wiring on useWebSocket — both are owned by the bridge now. * feat(frontend): migrate useClients to TanStack Query Replaces 12 hand-rolled mutation callbacks and a tangle of useState + useRef + useEffect with one useQuery (paged list) + nine useMutation wrappers. The list query uses keepPreviousData so paging/filter changes don't blank the table mid-fetch. The setQuery shallow-compare logic is preserved for backward compatibility with ClientsPage's effect that rebuilds the params on every render. Internally setQuery only updates state when the params actually differ — Query's queryKey equality handles the rest. WS-driven applyTrafficEvent / applyClientStatsEvent now mutate the query cache via setQueryData(['clients', 'list', currentParams]) so per-second stats updates skip a full refetch. applyInvalidate is gone from the hook — the bridge owns coarse 'clients' invalidation. ClientsPage drops the invalidate handler from its useWebSocket subscription; auxiliary queries (inboundOptions, defaults, onlines) load via TanStack Query and are shared with useInbounds via the same query keys. * feat(frontend): route useXraySetting fetches through TanStack Query Keeps the bidirectional xraySetting ↔ templateSettings editor sync and the 1s dirty-tracking interval intact (those are local editor state, not server data). All seven server calls move: - config + traffic → useQuery on ['xray', 'config'] and ['xray', 'outboundsTraffic'] - saveAll → useMutation that invalidates the config query - resetOutboundsTraffic → useMutation that invalidates the traffic query - restartXray → useMutation (fires the restart, then reads the result string) - resetToDefault → useMutation (fetch default config, push it into the editor via setTemplateSettings) The WebSocket 'outbounds' event already lands in keys.xray.outboundsTraffic() via the bridge, so XrayPage drops its useWebSocket({ outbounds: applyOutboundsEvent }) wiring entirely and the hook no longer exposes applyOutboundsEvent. A useEffect seeds xraySetting / templateSettings / tags / test URL from query data on first fetch and on every refetch, mirroring what the original fetchAll() did. * fix(frontend): restore per-route document titles in the SPA When the multi-entry MPA collapsed into a single index.html, every route inherited the static <title>3X-UI</title> from the shared shell, so every panel page showed "hostname - 3X-UI" instead of the original "hostname - Overview / Clients / Inbounds / ...". usePageTitle reads the current pathname and rewrites document.title on every navigation, matching the titles the deleted *.html files used to carry. Mounted in PanelLayout so it covers all panel routes without each page having to opt in. The startup applyDocumentTitle() call in main.tsx is gone — the hook sets the full "hostname - PageTitle" string itself. * feat(api-docs): expose OpenAPI spec + render Swagger UI in panel Replaces the hand-rolled API docs UI with industry-standard tooling so external integrations (Postman, Insomnia, openapi-generator) can consume the panel API without parsing endpoints.js by hand. Generator - frontend/scripts/build-openapi.mjs: walks the existing endpoints.js (still the single source of truth) and emits an OpenAPI 3.0.3 spec at frontend/public/openapi.json. Handles Gin :param → {param} path translation, body / query / path parameter splits, 200 + error response examples, and Bearer + cookie security schemes - npm run build now runs gen:api before vite build, so the spec is always in sync with what's documented Backend - web/controller/dist.go exposes ServeOpenAPISpec which streams the embedded dist/openapi.json with a short Cache-Control. Public endpoint (no auth) so Postman can fetch it without first logging in - web/web.go wires GET /panel/api/openapi.json before the auth-gated /panel/api router Panel - ApiDocsPage now renders swagger-ui-react fed by the basePath-aware openapi.json URL. Dark mode is overridden via CSS targeting the Swagger UI internals - CodeBlock / EndpointRow / EndpointSection are gone; the swagger-ui vendor chunk (134 KB gzipped) only loads on this lazy route, not on every panel page - vite.config: vendor-swagger manualChunk keeps the new dep out of the main vendor bundle For Postman: import http://<panel>/panel/api/openapi.json. Everything from /login + /panel/api/* shows up with auth, params, and examples. * style(api-docs): dark/ultra theme for Swagger UI Override every visual surface Swagger does not theme on its own: opblocks, tables, model boxes, form inputs, code blocks, modals, Servers dropdown, per-endpoint padlocks and expand chevrons. Replaces Swagger's default light-arrow chevron on selects with a light-fill SVG positioned at the corner so the dark background-color is visible. Also disables deepLinking to silence the noisy v4 underscore warning; not used in our panel.
2026-05-24 19:34:52 +00:00
import type { NodeRecord } from '@/api/queries/useNodesQuery';
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
import './InboundFormModal.css';
const { TextArea } = Input;
const { Text, Paragraph } = Typography;
interface InboundFormModalProps {
open: boolean;
onClose: () => void;
onSaved: () => void;
mode: 'add' | 'edit';
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
dbInbound: DBInbound | null;
dbInbounds: DBInbound[];
Reduce list-page payloads with slim/paged endpoints (#4500) * perf(inbounds): slim list payload + lazy hydrate for row actions Adds GET /panel/api/inbounds/list/slim that returns the same list shape but strips every per-client field besides email/enable/comment from settings.clients[] and skips UUID/SubId enrichment on ClientStats. The inbounds page only reads those three to compute its client counters and badges, so the slim variant trims tens of bytes per client (uuid, password, flow, security, totalGB, expiryTime, limitIp, tgId, ...). On a panel with thousands of clients this is the dominant load-time cost. Detail flows (edit / info / qr / export / clone) call /get/:id through a new hydrateInbound helper before opening — the slim list view never needs the secrets it doesn't render. * perf(clients): server-side pagination + slim row payload Adds GET /panel/api/clients/list/paged that filters, sorts, and paginates on the server, returns a slim row shape (drops uuid/password/auth/flow/ security/reverse/tgId per client), and includes a stable summary (total, active, online[], depleted[], expiring[], deactive[]) computed across the full DB row set so the dashboard cards don't change as the user paginates or filters. Page size capped at 200. useClients now exposes { clients (current page), total, filtered, query, setQuery, summary, hydrate }. ClientsPage feeds its filter/sort/page state into setQuery via a single effect, debounces search by 300ms, and hydrates the full client record via /get/:email before opening edit/info/ qr modals. Local filter/sort logic and the all-clients summary memo are gone. On a 2000-client panel this turns the initial response from ~MB to ~25 row slice (~10s of KB) and removes the all-client parse cost from every refresh. * perf(settings): use /inbounds/options for LDAP tag picker The General settings tab only needs each inbound's tag/protocol/port to fill a dropdown but was calling /panel/api/inbounds/list which ships the full settings JSON with every embedded client. Switched it to /options and added Tag to the projection. On a panel with thousands of clients this drops the General-tab load payload from megabytes to a tiny per-inbound row each. * perf(clients): de-duplicate options + paged list fetches Two issues caused each clients-page load to fire its requests twice: 1. setQuery in the hook took whatever object the consumer passed and stored it as-is. The consumer (ClientsPage) constructs a new object literal in an effect, so even when nothing actually changed the ref was new — the hook's useEffect saw a new query and re-fetched. Wrapped setQuery with a shallow value compare so identical params are a no-op. 2. The picker /inbounds/options fetch was bundled into refresh() with a length==0 guard, but the two back-to-back refreshes both saw an empty inbounds array (the first hadn't resolved yet) so both fired the request. Moved the options fetch into its own one-shot effect. * perf(inbounds): share nodes list with form modal instead of refetching InboundsPage and InboundFormModal both called useNodes() — each instance maintains its own state and fires its own /panel/api/nodes/list fetch on mount. Since the modal is always rendered (open or not), every page load hit the endpoint twice. Threaded nodes from the page through an availableNodes prop on the form modal so they share one fetch. * docs(api): register /clients/list/paged endpoint TestAPIRoutesDocumented was failing because the new paginated clients endpoint added in this branch wasn't listed in endpoints.js.
2026-05-23 15:43:43 +00:00
availableNodes?: NodeRecord[];
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
}
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
interface StreamLike {
network?: string;
tcp?: { type?: string; request?: { path?: string[] }; acceptProxyProtocol?: boolean };
ws?: { path?: string; acceptProxyProtocol?: boolean };
grpc?: { serviceName?: string; multiMode?: boolean };
httpupgrade?: { path?: string; acceptProxyProtocol?: boolean };
xhttp?: { path?: string };
security?: string;
tls?: { certs?: TlsCert[] };
reality?: unknown;
externalProxy?: unknown;
}
interface TlsCert {
useFile?: boolean;
certFile?: string;
keyFile?: string;
cert?: string;
key?: string;
ocspStapling?: number;
oneTimeLoading?: boolean;
usage?: string;
buildChain?: boolean;
}
interface VlessClient {
id?: string;
email?: string;
flow?: string;
enable?: boolean;
subId?: string;
totalGB?: number;
expiryTime?: number;
limitIp?: number;
comment?: string;
tgId?: string;
}
interface ShadowsocksClient {
email?: string;
password?: string;
method?: string;
enable?: boolean;
subId?: string;
totalGB?: number;
expiryTime?: number;
limitIp?: number;
comment?: string;
tgId?: string;
}
interface HttpAccount {
user?: string;
pass?: string;
}
interface WireguardPeer {
privateKey?: string;
publicKey?: string;
psk?: string;
allowedIPs: string[];
keepAlive?: number;
}
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
const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'];
const PROTOCOLS = Object.values(Protocols) as string[];
const TLS_VERSIONS = Object.values(TLS_VERSION_OPTION) as string[];
const CIPHER_SUITES = Object.entries(TLS_CIPHER_OPTION) as [string, string][];
const FINGERPRINTS = Object.values(UTLS_FINGERPRINT) as string[];
const ALPNS = Object.values(ALPN_OPTION) as string[];
const USAGES = Object.values(USAGE_OPTION) as string[];
const DOMAIN_STRATEGIES = Object.values(DOMAIN_STRATEGY_OPTION) as string[];
const TCP_CONGESTIONS = Object.values(TCP_CONGESTION_OPTION) as string[];
const MODE_OPTIONS = Object.values(MODE_OPTION) as string[];
const NODE_ELIGIBLE_PROTOCOLS = new Set([
Protocols.VLESS,
Protocols.VMESS,
Protocols.TROJAN,
Protocols.SHADOWSOCKS,
Protocols.HYSTERIA,
Protocols.WIREGUARD,
]);
const FALLBACK_ELIGIBLE_TRANSPORTS = new Set(['tcp', 'ws', 'grpc', 'httpupgrade', 'xhttp']);
interface FallbackRow {
rowKey: string;
childId: number | null;
name: string;
alpn: string;
path: string;
xver: number;
}
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
function deriveFallbackDefaults(childDb: DBInbound | null | undefined): Omit<FallbackRow, 'rowKey' | 'childId'> {
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
const out = { name: '', alpn: '', path: '', xver: 0 };
if (!childDb) return out;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
let stream: StreamLike | undefined;
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
try {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
stream = childDb.toInbound()?.stream as StreamLike | undefined;
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
} catch {
return out;
}
if (!stream) return out;
switch (stream.network) {
case 'tcp': {
const tcp = stream.tcp;
if (tcp?.type === 'http') {
const p = tcp?.request?.path;
if (Array.isArray(p) && p.length) out.path = p[0];
}
if (tcp?.acceptProxyProtocol) out.xver = 2;
break;
}
case 'ws': {
out.path = stream.ws?.path || '';
if (stream.ws?.acceptProxyProtocol) out.xver = 2;
break;
}
case 'grpc': {
out.path = stream.grpc?.serviceName || '';
out.alpn = 'h2';
break;
}
case 'httpupgrade': {
out.path = stream.httpupgrade?.path || '';
if (stream.httpupgrade?.acceptProxyProtocol) out.xver = 2;
break;
}
case 'xhttp': {
out.path = stream.xhttp?.path || '';
break;
}
}
return out;
}
export default function InboundFormModal({
open,
onClose,
onSaved,
mode,
dbInbound,
dbInbounds,
Reduce list-page payloads with slim/paged endpoints (#4500) * perf(inbounds): slim list payload + lazy hydrate for row actions Adds GET /panel/api/inbounds/list/slim that returns the same list shape but strips every per-client field besides email/enable/comment from settings.clients[] and skips UUID/SubId enrichment on ClientStats. The inbounds page only reads those three to compute its client counters and badges, so the slim variant trims tens of bytes per client (uuid, password, flow, security, totalGB, expiryTime, limitIp, tgId, ...). On a panel with thousands of clients this is the dominant load-time cost. Detail flows (edit / info / qr / export / clone) call /get/:id through a new hydrateInbound helper before opening — the slim list view never needs the secrets it doesn't render. * perf(clients): server-side pagination + slim row payload Adds GET /panel/api/clients/list/paged that filters, sorts, and paginates on the server, returns a slim row shape (drops uuid/password/auth/flow/ security/reverse/tgId per client), and includes a stable summary (total, active, online[], depleted[], expiring[], deactive[]) computed across the full DB row set so the dashboard cards don't change as the user paginates or filters. Page size capped at 200. useClients now exposes { clients (current page), total, filtered, query, setQuery, summary, hydrate }. ClientsPage feeds its filter/sort/page state into setQuery via a single effect, debounces search by 300ms, and hydrates the full client record via /get/:email before opening edit/info/ qr modals. Local filter/sort logic and the all-clients summary memo are gone. On a 2000-client panel this turns the initial response from ~MB to ~25 row slice (~10s of KB) and removes the all-client parse cost from every refresh. * perf(settings): use /inbounds/options for LDAP tag picker The General settings tab only needs each inbound's tag/protocol/port to fill a dropdown but was calling /panel/api/inbounds/list which ships the full settings JSON with every embedded client. Switched it to /options and added Tag to the projection. On a panel with thousands of clients this drops the General-tab load payload from megabytes to a tiny per-inbound row each. * perf(clients): de-duplicate options + paged list fetches Two issues caused each clients-page load to fire its requests twice: 1. setQuery in the hook took whatever object the consumer passed and stored it as-is. The consumer (ClientsPage) constructs a new object literal in an effect, so even when nothing actually changed the ref was new — the hook's useEffect saw a new query and re-fetched. Wrapped setQuery with a shallow value compare so identical params are a no-op. 2. The picker /inbounds/options fetch was bundled into refresh() with a length==0 guard, but the two back-to-back refreshes both saw an empty inbounds array (the first hadn't resolved yet) so both fired the request. Moved the options fetch into its own one-shot effect. * perf(inbounds): share nodes list with form modal instead of refetching InboundsPage and InboundFormModal both called useNodes() — each instance maintains its own state and fires its own /panel/api/nodes/list fetch on mount. Since the modal is always rendered (open or not), every page load hit the endpoint twice. Threaded nodes from the page through an availableNodes prop on the form modal so they share one fetch. * docs(api): register /clients/list/paged endpoint TestAPIRoutesDocumented was failing because the new paginated clients endpoint added in this branch wasn't listed in endpoints.js.
2026-05-23 15:43:43 +00:00
availableNodes,
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
}: InboundFormModalProps) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
const selectableNodes = useMemo(
() => (availableNodes || []).filter((n: NodeRecord) => n.enable),
[availableNodes],
);
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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
const inboundRef = useRef<any>(null);
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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
const dbFormRef = useRef<any>(null);
const fallbackKeyRef = useRef(0);
const advancedTextRef = useRef({ stream: '', sniffing: '', settings: '' });
const [, setTick] = useState(0);
const refresh = useCallback(() => setTick((n) => n + 1), []);
const [saving, setSaving] = useState(false);
const [activeTabKey, setActiveTabKey] = useState('basic');
const [advancedSectionKey, setAdvancedSectionKey] = useState('all');
const [defaultCert, setDefaultCert] = useState('');
const [defaultKey, setDefaultKey] = useState('');
const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
const [fallbackEditing, setFallbackEditing] = useState<Set<string>>(new Set());
const isVlessLike = inboundRef.current?.protocol === Protocols.VLESS;
const isFallbackHost = useMemo(() => {
const ib = inboundRef.current;
if (!ib) return false;
if (ib.protocol !== Protocols.VLESS && ib.protocol !== Protocols.TROJAN) return false;
if (ib.stream?.network !== 'tcp') return false;
const sec = ib.stream?.security;
return sec === 'tls' || sec === 'reality';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inboundRef.current?.protocol, inboundRef.current?.stream?.network, inboundRef.current?.stream?.security]);
const canEnableStream = inboundRef.current?.canEnableStream?.() === true;
const canEnableTls = inboundRef.current?.canEnableTls?.() === true;
const canEnableReality = inboundRef.current?.canEnableReality?.() === true;
const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(inboundRef.current?.protocol);
const hasProtocolTabContent = useMemo(() => {
const ib = inboundRef.current;
if (!ib) return false;
if (ib.protocol === Protocols.VLESS) return true;
if (isFallbackHost) return true;
switch (ib.protocol) {
case Protocols.SHADOWSOCKS:
case Protocols.HTTP:
case Protocols.MIXED:
case Protocols.TUNNEL:
case Protocols.TUN:
case Protocols.WIREGUARD:
return true;
default:
return false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inboundRef.current?.protocol, isFallbackHost]);
const externalProxyOn = Array.isArray(inboundRef.current?.stream?.externalProxy)
&& inboundRef.current.stream.externalProxy.length > 0;
const stampAdvancedTextFor = useCallback((slice: 'stream' | 'sniffing' | 'settings') => {
const ib = inboundRef.current;
if (!ib) return;
if (slice === 'stream' && !ib.canEnableStream?.()) {
advancedTextRef.current.stream = '{}';
return;
}
const obj = ib[slice];
if (!obj) return;
try {
advancedTextRef.current[slice] = JSON.stringify(JSON.parse(obj.toString()), null, 2);
} catch {
/* keep prior */
}
}, []);
const primeAdvancedJson = useCallback(() => {
(['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
}, [stampAdvancedTextFor]);
const loadFallbacks = useCallback(async (masterId: number | null) => {
if (!masterId) {
setFallbacks([]);
return;
}
const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
if (!msg?.success || !Array.isArray(msg.obj)) {
setFallbacks([]);
return;
}
setFallbacks(
(msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[]).map((r) => ({
rowKey: `fb-${++fallbackKeyRef.current}`,
childId: r.childId,
name: r.name || '',
alpn: r.alpn || '',
path: r.path || '',
xver: r.xver || 0,
})),
);
}, []);
const fetchDefaultCertSettings = useCallback(async () => {
try {
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg?.success && msg.obj) {
const obj = msg.obj as { defaultCert?: string; defaultKey?: string };
setDefaultCert(obj.defaultCert || '');
setDefaultKey(obj.defaultKey || '');
}
} catch {
/* non-fatal */
}
}, []);
useEffect(() => {
if (!open) return;
setFallbackEditing(new Set());
if (mode === 'edit' && dbInbound) {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const parsed = Inbound.fromJson(dbInbound.toInbound().toJson());
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
inboundRef.current = parsed;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
dbFormRef.current = new DBInbound(dbInbound);
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
primeAdvancedJson();
if (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN) {
loadFallbacks(dbInbound.id);
} else {
setFallbacks([]);
}
} else {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const ib = new Inbound();
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
ib.protocol = Protocols.VLESS;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
ib.settings = Inbound.Settings.getSettings(Protocols.VLESS);
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
ib.port = RandomUtil.randomInteger(10000, 60000);
inboundRef.current = ib;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const form = new DBInbound();
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
form.enable = true;
form.remark = '';
form.total = 0;
form.expiryTime = 0;
form.trafficReset = 'never';
dbFormRef.current = form;
primeAdvancedJson();
setFallbacks([]);
}
setActiveTabKey('basic');
setAdvancedSectionKey('all');
fetchDefaultCertSettings();
refresh();
}, [open, mode, dbInbound, primeAdvancedJson, loadFallbacks, fetchDefaultCertSettings, refresh]);
const setExternalProxy = useCallback((on: boolean) => {
const ib = inboundRef.current;
if (!ib?.stream) return;
if (on) {
ib.stream.externalProxy = [{
forceTls: 'same',
dest: window.location.hostname,
port: ib.port,
remark: '',
sni: '',
fingerprint: '',
alpn: [],
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
}];
} else {
ib.stream.externalProxy = [];
}
refresh();
}, [refresh]);
const onProtocolChange = useCallback((next: string) => {
const ib = inboundRef.current;
if (mode === 'edit' || !ib) return;
ib.protocol = next;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
ib.settings = Inbound.Settings.getSettings(next);
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
if (!NODE_ELIGIBLE_PROTOCOLS.has(next) && dbFormRef.current) {
dbFormRef.current.nodeId = null;
}
primeAdvancedJson();
refresh();
}, [mode, primeAdvancedJson, refresh]);
const onNetworkChange = useCallback((next: string) => {
const ib = inboundRef.current;
if (!ib?.stream) return;
ib.stream.network = next;
if (!ib.canEnableTls()) ib.stream.security = 'none';
if (!ib.canEnableReality()) ib.reality = false;
if (
ib.protocol === Protocols.VLESS
&& !ib.canEnableTlsFlow()
&& Array.isArray(ib.settings.vlesses)
) {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
ib.settings.vlesses.forEach((c: VlessClient) => { c.flow = ''; });
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
}
if (next !== 'kcp' && ib.stream.finalmask) {
ib.stream.finalmask.udp = [];
}
stampAdvancedTextFor('stream');
refresh();
}, [stampAdvancedTextFor, refresh]);
const setSecurity = useCallback((v: string) => {
const ib = inboundRef.current;
if (ib?.stream) {
ib.stream.security = v;
refresh();
}
}, [refresh]);
const addFallback = useCallback((childId: number | null = null) => {
const row: FallbackRow = {
rowKey: `fb-${++fallbackKeyRef.current}`,
childId: childId || null,
name: '',
alpn: '',
path: '',
xver: 0,
};
if (childId) {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const child = (dbInbounds || []).find((ib) => ib.id === childId);
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
Object.assign(row, deriveFallbackDefaults(child));
}
setFallbacks((prev) => [...prev, row]);
}, [dbInbounds]);
const removeFallback = useCallback((idx: number) => {
setFallbacks((prev) => prev.filter((_, i) => i !== idx));
}, []);
const moveFallback = useCallback((idx: number, dir: number) => {
setFallbacks((prev) => {
const arr = [...prev];
const j = idx + dir;
if (j < 0 || j >= arr.length) return prev;
[arr[idx], arr[j]] = [arr[j], arr[idx]];
return arr;
});
}, []);
const onFallbackChildPicked = useCallback((rowKey: string, childId: number) => {
setFallbacks((prev) => prev.map((row) => {
if (row.rowKey !== rowKey) return row;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const child = (dbInbounds || []).find((ib) => ib.id === childId);
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
const defaults = deriveFallbackDefaults(child);
return { ...row, childId, ...defaults };
}));
}, [dbInbounds]);
const updateFallback = useCallback((rowKey: string, patch: Partial<FallbackRow>) => {
setFallbacks((prev) => prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch } : row)));
}, []);
const rederiveFallback = useCallback((rowKey: string) => {
setFallbacks((prev) => prev.map((row) => {
if (row.rowKey !== rowKey || !row.childId) return row;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const child = (dbInbounds || []).find((ib) => ib.id === row.childId);
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
const defaults = deriveFallbackDefaults(child);
return { ...row, ...defaults };
}));
messageApi.success(t('pages.inbounds.fallbacks.rederived') || 'Re-filled from child');
}, [dbInbounds, t, messageApi]);
const quickAddAllFallbacks = useCallback(() => {
const masterId = dbInbound?.id;
const list = dbInbounds || [];
setFallbacks((prev) => {
const existing = new Set(prev.map((r) => r.childId).filter(Boolean));
const next = [...prev];
let added = 0;
for (const ib of list) {
if (ib.id === masterId) continue;
if (existing.has(ib.id)) continue;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
let stream: StreamLike | undefined;
try { stream = ib.toInbound()?.stream as StreamLike | undefined; } catch { continue; }
if (!stream || !FALLBACK_ELIGIBLE_TRANSPORTS.has(stream.network ?? '')) continue;
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
const row: FallbackRow = {
rowKey: `fb-${++fallbackKeyRef.current}`,
childId: ib.id,
...deriveFallbackDefaults(ib),
};
next.push(row);
added += 1;
}
if (added > 0) {
messageApi.success(t('pages.inbounds.fallbacks.quickAdded', { n: added }) || `Added ${added} fallback(s)`);
} else {
messageApi.info(t('pages.inbounds.fallbacks.quickAddedNone') || 'No new eligible inbounds to add');
}
return next;
});
}, [dbInbound, dbInbounds, t, messageApi]);
const fallbackChildOptions = useMemo(() => {
const list = dbInbounds || [];
const masterId = dbInbound?.id;
return list
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
.filter((ib) => ib.id !== masterId)
.map((ib) => ({
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
label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
value: ib.id,
}));
}, [dbInbounds, dbInbound]);
const toggleFallbackEdit = useCallback((rowKey: string) => {
setFallbackEditing((prev) => {
const next = new Set(prev);
if (next.has(rowKey)) next.delete(rowKey); else next.add(rowKey);
return next;
});
}, []);
const describeFallback = useCallback((record: FallbackRow) => {
const parts: string[] = [];
if (record.name) parts.push(`SNI=${record.name}`);
if (record.alpn) parts.push(`ALPN=${record.alpn}`);
if (record.path) parts.push(`path=${record.path}`);
const condition = parts.length
? `${t('pages.inbounds.fallbacks.routesWhen') || 'Routes when'} ${parts.join(' · ')}`
: (t('pages.inbounds.fallbacks.defaultCatchAll') || 'Default — catches anything else');
const proxyTag = record.xver === 2 ? ' · PROXY v2' : record.xver === 1 ? ' · PROXY v1' : '';
return { condition, proxyTag };
}, [t]);
const withSaving = useCallback(async <T,>(fn: () => Promise<T>): Promise<T> => {
setSaving(true);
try { return await fn(); } finally { setSaving(false); }
}, []);
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const randomSSPassword = useCallback((target: ShadowsocksClient) => {
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
if (target) {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
target.password = RandomUtil.randomShadowsocksPassword(inboundRef.current.settings.method);
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
refresh();
}
}, [refresh]);
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const regenWgKeypair = useCallback((target: WireguardPeer) => {
const kp = Wireguard.generateKeypair();
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
target.publicKey = kp.publicKey;
target.privateKey = kp.privateKey;
refresh();
}, [refresh]);
const regenInboundWg = useCallback(() => {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const kp = Wireguard.generateKeypair();
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
inboundRef.current.settings.pubKey = kp.publicKey;
inboundRef.current.settings.secretKey = kp.privateKey;
refresh();
}, [refresh]);
const genRealityKeypair = useCallback(async () => {
await withSaving(async () => {
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
if (msg?.success) {
const obj = msg.obj as { privateKey: string; publicKey: string };
inboundRef.current.stream.reality.privateKey = obj.privateKey;
inboundRef.current.stream.reality.settings.publicKey = obj.publicKey;
refresh();
}
});
}, [withSaving, refresh]);
const clearRealityKeypair = useCallback(() => {
if (!inboundRef.current?.stream?.reality) return;
inboundRef.current.stream.reality.privateKey = '';
inboundRef.current.stream.reality.settings.publicKey = '';
refresh();
}, [refresh]);
const genMldsa65 = useCallback(async () => {
await withSaving(async () => {
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
if (msg?.success) {
const obj = msg.obj as { seed: string; verify: string };
inboundRef.current.stream.reality.mldsa65Seed = obj.seed;
inboundRef.current.stream.reality.settings.mldsa65Verify = obj.verify;
refresh();
}
});
}, [withSaving, refresh]);
const clearMldsa65 = useCallback(() => {
if (!inboundRef.current?.stream?.reality) return;
inboundRef.current.stream.reality.mldsa65Seed = '';
inboundRef.current.stream.reality.settings.mldsa65Verify = '';
refresh();
}, [refresh]);
const randomizeRealityTarget = useCallback(() => {
if (!inboundRef.current?.stream?.reality) return;
const target = getRandomRealityTarget() as { target: string; sni: string };
inboundRef.current.stream.reality.target = target.target;
inboundRef.current.stream.reality.serverNames = target.sni;
refresh();
}, [refresh]);
const randomizeShortIds = useCallback(() => {
if (!inboundRef.current?.stream?.reality) return;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
inboundRef.current.stream.reality.shortIds = RandomUtil.randomShortIds();
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
refresh();
}, [refresh]);
const getNewEchCert = useCallback(async () => {
if (!inboundRef.current?.stream?.tls) return;
await withSaving(async () => {
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', {
sni: inboundRef.current.stream.tls.sni,
});
if (msg?.success) {
const obj = msg.obj as { echServerKeys: string; echConfigList: string };
inboundRef.current.stream.tls.echServerKeys = obj.echServerKeys;
inboundRef.current.stream.tls.settings.echConfigList = obj.echConfigList;
refresh();
}
});
}, [withSaving, refresh]);
const clearEchCert = useCallback(() => {
if (!inboundRef.current?.stream?.tls) return;
inboundRef.current.stream.tls.echServerKeys = '';
inboundRef.current.stream.tls.settings.echConfigList = '';
refresh();
}, [refresh]);
const setDefaultCertData = useCallback((idx: number) => {
if (!inboundRef.current?.stream?.tls?.certs?.[idx]) return;
inboundRef.current.stream.tls.certs[idx].certFile = defaultCert;
inboundRef.current.stream.tls.certs[idx].keyFile = defaultKey;
refresh();
}, [defaultCert, defaultKey, refresh]);
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const matchesVlessAuth = useCallback((block: { id?: string; label?: string } | undefined | null, authId: string) => {
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
if (block?.id === authId) return true;
const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
if (authId === 'mlkem768') return label.includes('mlkem768');
if (authId === 'x25519') return label.includes('x25519');
return false;
}, []);
const getNewVlessEnc = useCallback(async (authId: string) => {
if (!authId || !inboundRef.current?.settings) return;
await withSaving(async () => {
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
if (!msg?.success) return;
const obj = msg.obj as { auths?: { decryption: string; encryption: string; label?: string; id?: string }[] };
const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
if (!block) return;
inboundRef.current.settings.decryption = block.decryption;
inboundRef.current.settings.encryption = block.encryption;
refresh();
});
}, [withSaving, refresh, matchesVlessAuth]);
const clearVlessEnc = useCallback(() => {
if (!inboundRef.current?.settings) return;
inboundRef.current.settings.decryption = 'none';
inboundRef.current.settings.encryption = 'none';
refresh();
}, [refresh]);
const selectedVlessAuth = useMemo(() => {
const encryption = inboundRef.current?.settings?.encryption;
if (!encryption || encryption === 'none') return 'None';
const parts = encryption.split('.').filter(Boolean);
const authKey = parts[parts.length - 1] || '';
if (!authKey) return t('pages.inbounds.vlessAuthCustom');
return authKey.length > 300
? t('pages.inbounds.vlessAuthMlkem768')
: t('pages.inbounds.vlessAuthX25519');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inboundRef.current?.settings?.encryption, t]);
const onSSMethodChange = useCallback(() => {
const ib = inboundRef.current;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
ib.settings.password = RandomUtil.randomShadowsocksPassword(ib.settings.method);
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
if (ib.isSSMultiUser) {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
ib.settings.shadowsockses.forEach((c: ShadowsocksClient) => {
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
c.method = ib.isSS2022 ? '' : ib.settings.method;
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
c.password = RandomUtil.randomShadowsocksPassword(ib.settings.method);
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
});
} else {
ib.settings.shadowsockses = [];
}
refresh();
}, [refresh]);
const parseAdvancedSliceOrFallback = (rawText: string, fallback: unknown) => {
if (!rawText?.trim()) return fallback;
return JSON.parse(rawText);
};
const settingsFallback = () => inboundRef.current?.settings?.toJson?.() || {};
const sniffingFallback = () => inboundRef.current?.sniffing?.toJson?.() || {};
const streamFallback = () => inboundRef.current?.stream?.toJson?.() || {};
const parseAdvancedSliceWithLabel = useCallback((rawText: string, fallback: unknown, label: string) => {
try {
return parseAdvancedSliceOrFallback(rawText, fallback);
} catch (e) {
messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
throw e;
}
}, [messageApi]);
const compactAdvancedJson = useCallback((raw: string, fallback: string, label: string) => {
try {
return JSON.stringify(JSON.parse(raw || fallback));
} catch (e) {
messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
throw e;
}
}, [messageApi]);
const applyAdvancedJsonToBasic = useCallback((): boolean => {
const ib = inboundRef.current;
if (!ib) return true;
let settings: unknown;
let streamSettings: unknown;
let sniffing: unknown;
try {
settings = parseAdvancedSliceWithLabel(advancedTextRef.current.settings, settingsFallback(), t('pages.inbounds.advanced.settings'));
streamSettings = parseAdvancedSliceWithLabel(advancedTextRef.current.stream, streamFallback(), t('pages.inbounds.advanced.stream'));
sniffing = parseAdvancedSliceWithLabel(advancedTextRef.current.sniffing, sniffingFallback(), t('pages.inbounds.advanced.sniffing'));
} catch {
return false;
}
try {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
inboundRef.current = Inbound.fromJson({
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
port: ib.port,
listen: ib.listen,
protocol: ib.protocol,
settings,
streamSettings,
tag: ib.tag,
sniffing,
clientStats: ib.clientStats,
});
refresh();
} catch (e) {
messageApi.error(`${t('pages.inbounds.advanced.jsonErrorPrefix')}: ${(e as Error).message}`);
return false;
}
return true;
}, [t, refresh, parseAdvancedSliceWithLabel, messageApi]);
const handleTabChange = (next: string) => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
if (activeTabKey === 'advanced' && next !== 'advanced') {
if (!applyAdvancedJsonToBasic()) return;
}
setActiveTabKey(next);
};
const unwrapWrappedObject = (parsed: unknown, key: string): unknown => {
if (
parsed
&& typeof parsed === 'object'
&& !Array.isArray(parsed)
&& (parsed as Record<string, unknown>)[key] !== undefined
) {
return (parsed as Record<string, unknown>)[key];
}
return parsed;
};
const wrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings'): string => {
const ib = inboundRef.current;
if (!ib) return '';
try {
const fb = slice === 'settings' ? settingsFallback() : slice === 'sniffing' ? sniffingFallback() : streamFallback();
const value = parseAdvancedSliceOrFallback(advancedTextRef.current[slice], fb);
return JSON.stringify({ [key]: value }, null, 2);
} catch {
return '';
}
};
const setWrappedConfigValue = (key: string, slice: 'stream' | 'sniffing' | 'settings', label: string, next: string) => {
let parsed: unknown;
try {
parsed = JSON.parse(next);
} catch (e) {
messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
return;
}
const unwrapped = unwrapWrappedObject(parsed, key);
if (!unwrapped || typeof unwrapped !== 'object' || Array.isArray(unwrapped)) {
messageApi.error(`${label} JSON must be an object or { ${key}: { ... } }.`);
return;
}
try {
advancedTextRef.current[slice] = JSON.stringify(unwrapped, null, 2);
refresh();
} catch (e) {
messageApi.error(`${label} JSON invalid: ${(e as Error).message}`);
}
};
const advancedAllValue = (() => {
const ib = inboundRef.current;
if (!ib) return '';
try {
const result: Record<string, unknown> = {
listen: ib.listen,
port: ib.port,
protocol: ib.protocol,
settings: parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback()),
sniffing: parseAdvancedSliceOrFallback(advancedTextRef.current.sniffing, sniffingFallback()),
tag: ib.tag,
};
if (canEnableStream) {
result.streamSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.stream, streamFallback());
}
return JSON.stringify(result, null, 2);
} catch {
return '';
}
})();
const setAdvancedAllValue = (next: string) => {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
let parsedRaw: unknown;
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
try {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
parsedRaw = JSON.parse(next);
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
} catch (e) {
messageApi.error(`All JSON invalid: ${(e as Error).message}`);
return;
}
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
if (!parsedRaw || typeof parsedRaw !== 'object' || Array.isArray(parsedRaw)) {
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
messageApi.error('All JSON must be an inbound object.');
return;
}
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const parsed = parsedRaw as {
listen?: string;
port?: number | string;
protocol?: string;
tag?: string;
settings?: unknown;
sniffing?: unknown;
streamSettings?: unknown;
};
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
const ib = inboundRef.current;
try {
if (typeof parsed.listen === 'string') ib.listen = parsed.listen;
if (parsed.port !== undefined) {
const port = Number(parsed.port);
if (Number.isFinite(port)) ib.port = port;
}
if (typeof parsed.protocol === 'string' && PROTOCOLS.includes(parsed.protocol)) {
ib.protocol = parsed.protocol;
}
if (typeof parsed.tag === 'string') ib.tag = parsed.tag;
const existingSettings = parseAdvancedSliceOrFallback(advancedTextRef.current.settings, settingsFallback());
advancedTextRef.current.settings = JSON.stringify(parsed.settings ?? existingSettings, null, 2);
advancedTextRef.current.sniffing = JSON.stringify(parsed.sniffing ?? sniffingFallback(), null, 2);
advancedTextRef.current.stream = canEnableStream
? JSON.stringify(parsed.streamSettings ?? streamFallback(), null, 2)
: '{}';
refresh();
} catch (e) {
messageApi.error(`All JSON invalid: ${(e as Error).message}`);
}
};
const saveFallbacks = useCallback(async (masterId: number) => {
if (!masterId) return true;
const payload = {
fallbacks: fallbacks
.filter((c) => c.childId)
.map((c, i) => ({
childId: c.childId,
name: c.name,
alpn: c.alpn,
path: c.path,
xver: Number(c.xver) || 0,
sortOrder: i,
})),
};
const msg = await HttpUtil.post(
`/panel/api/inbounds/${masterId}/fallbacks`,
payload,
{ headers: { 'Content-Type': 'application/json' } },
);
return !!msg?.success;
}, [fallbacks]);
const submit = useCallback(async () => {
const ib = inboundRef.current;
const form = dbFormRef.current;
if (!ib || !form) return;
setSaving(true);
try {
let streamSettings: string;
let sniffing: string;
let settings: string;
try {
streamSettings = canEnableStream
? compactAdvancedJson(advancedTextRef.current.stream, '', t('pages.inbounds.advanced.stream'))
: (ib.stream?.sockopt
? JSON.stringify({ sockopt: ib.stream.sockopt.toJson() })
: '');
sniffing = compactAdvancedJson(advancedTextRef.current.sniffing, ib.sniffing.toString(), t('pages.inbounds.advanced.sniffing'));
settings = compactAdvancedJson(advancedTextRef.current.settings, ib.settings.toString(), t('pages.inbounds.advanced.settings'));
} catch { return; }
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const payload: Record<string, unknown> = {
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
up: form.up || 0,
down: form.down || 0,
total: form.total,
remark: form.remark,
enable: form.enable,
expiryTime: form.expiryTime,
trafficReset: form.trafficReset,
lastTrafficResetTime: form.lastTrafficResetTime || 0,
listen: ib.listen,
port: ib.port,
protocol: ib.protocol,
settings,
streamSettings,
sniffing,
};
if (form.nodeId != null) payload.nodeId = form.nodeId;
const url = mode === 'edit'
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
? `/panel/api/inbounds/update/${dbInbound!.id}`
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
: '/panel/api/inbounds/add';
const msg = await HttpUtil.post(url, payload);
if (msg?.success) {
if (isFallbackHost) {
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
const obj = msg.obj as { id?: number; Id?: number } | null;
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
const masterId = mode === 'edit'
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
? dbInbound!.id
: (obj?.id || obj?.Id);
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
if (masterId) await saveFallbacks(masterId);
}
onSaved();
onClose();
}
} finally {
setSaving(false);
}
}, [canEnableStream, compactAdvancedJson, t, mode, dbInbound, isFallbackHost, saveFallbacks, onSaved, onClose]);
const protocolSnapshot = inboundRef.current?.protocol;
const streamSnapshot = JSON.stringify(inboundRef.current?.stream?.toJson?.() || {});
const sniffingSnapshot = JSON.stringify(inboundRef.current?.sniffing?.toJson?.() || {});
const settingsSnapshot = JSON.stringify(inboundRef.current?.settings?.toJson?.() || {});
useEffect(() => {
if (!inboundRef.current) return;
(['stream', 'sniffing', 'settings'] as const).forEach(stampAdvancedTextFor);
}, [protocolSnapshot, streamSnapshot, sniffingSnapshot, settingsSnapshot, stampAdvancedTextFor]);
const title = mode === 'edit' ? t('pages.inbounds.modifyInbound') : t('pages.inbounds.addInbound');
const okText = mode === 'edit' ? t('pages.clients.submitEdit') : t('create');
const ib = inboundRef.current;
const form = dbFormRef.current;
if (!ib || !form) {
return <Modal open={open} onCancel={onClose} title={title} footer={null} width={780} />;
}
const totalGB = form.total ? Math.round((form.total / SizeFormatter.ONE_GB) * 100) / 100 : 0;
const expiryDate: Dayjs | null = form.expiryTime > 0 ? dayjs(form.expiryTime) : null;
const renderBasicsTab = () => (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
<Form.Item label={t('enable')}>
<Switch checked={!!form.enable} onChange={(v) => { form.enable = v; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.remark')}>
<Input value={form.remark} onChange={(e) => { form.remark = e.target.value; refresh(); }} />
</Form.Item>
{selectableNodes.length > 0 && isNodeEligible && (
<Form.Item label={t('pages.inbounds.deployTo')}>
<Select
value={form.nodeId ?? ''}
disabled={mode === 'edit'}
placeholder={t('pages.inbounds.localPanel')}
allowClear
onChange={(v) => { form.nodeId = v === '' || v == null ? null : v; refresh(); }}
>
<Select.Option value="">{t('pages.inbounds.localPanel')}</Select.Option>
{selectableNodes.map((n: NodeRecord) => (
<Select.Option key={n.id} value={n.id} disabled={n.status === 'offline'}>
{n.name}{n.status === 'offline' ? ' (offline)' : ''}
</Select.Option>
))}
</Select>
</Form.Item>
)}
<Form.Item label={t('pages.inbounds.protocol')}>
<Select
value={ib.protocol}
disabled={mode === 'edit'}
onChange={onProtocolChange}
>
{PROTOCOLS.map((p) => <Select.Option key={p} value={p}>{p}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label={t('pages.inbounds.address')}>
<Input
value={ib.listen}
placeholder={t('pages.inbounds.monitorDesc')}
onChange={(e) => { ib.listen = e.target.value; refresh(); }}
/>
</Form.Item>
<Form.Item label={t('pages.inbounds.port')}>
<InputNumber
value={ib.port}
min={1}
max={65535}
onChange={(v) => { ib.port = Number(v) || 0; refresh(); }}
/>
</Form.Item>
<Form.Item label={<Tooltip title={t('pages.inbounds.meansNoLimit')}>{t('pages.inbounds.totalFlow')}</Tooltip>}>
<InputNumber
value={totalGB}
min={0}
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
step={1}
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
onChange={(v) => {
form.total = NumberFormatter.toFixed((Number(v) || 0) * SizeFormatter.ONE_GB, 0);
refresh();
}}
/>
</Form.Item>
<Form.Item label={t('pages.inbounds.periodicTrafficResetTitle')}>
<Select value={form.trafficReset} onChange={(v) => { form.trafficReset = v; refresh(); }}>
{TRAFFIC_RESETS.map((r) => (
<Select.Option key={r} value={r}>{t(`pages.inbounds.periodicTrafficReset.${r}`)}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={<Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>{t('pages.inbounds.expireDate')}</Tooltip>}>
<DateTimePicker
value={expiryDate}
onChange={(d) => { form.expiryTime = d ? d.valueOf() : 0; refresh(); }}
/>
</Form.Item>
</Form>
);
const renderFallbacksCard = () => (
<Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
<Paragraph type="secondary" style={{ marginBottom: 12 }}>
{t('pages.inbounds.fallbacks.help') || 'When a connection on this inbound does not match any client, route it to another inbound. Pick a child below and the routing fields (SNI / ALPN / path / xver) auto-fill from its transport — most setups need no further tweaking. Each child should listen on 127.0.0.1 with security=none.'}
</Paragraph>
{fallbacks.length === 0 && (
<Empty description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'} styles={{ image: { height: 40 } }} style={{ margin: '8px 0 12px' }} />
)}
{fallbacks.map((record, index) => (
<div key={record.rowKey} style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}>
<Row gutter={8} align="middle" wrap={false}>
<Col flex="none">
<Space orientation="vertical" size={2}>
<Button size="small" type="text" disabled={index === 0} onClick={() => moveFallback(index, -1)}>
<CaretUpOutlined />
</Button>
<Button size="small" type="text" disabled={index === fallbacks.length - 1} onClick={() => moveFallback(index, 1)}>
<CaretDownOutlined />
</Button>
</Space>
</Col>
<Col flex="auto">
<Select
value={record.childId}
options={fallbackChildOptions}
showSearch
placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
filterOption={(input, option) => ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase())}
style={{ width: '100%' }}
onChange={(v) => onFallbackChildPicked(record.rowKey, v)}
/>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 4 }}>
{describeFallback(record).condition}{describeFallback(record).proxyTag}
</Text>
</Col>
<Col flex="none">
<Space size={4}>
<Tooltip title={t('pages.inbounds.fallbacks.rederive') || 'Re-fill from child'}>
<Button size="small" type="text" disabled={!record.childId} onClick={() => rederiveFallback(record.rowKey)}>
<SyncOutlined />
</Button>
</Tooltip>
<Tooltip title={fallbackEditing.has(record.rowKey)
? (t('pages.inbounds.fallbacks.hideAdvanced') || 'Hide advanced')
: (t('pages.inbounds.fallbacks.editAdvanced') || 'Edit routing fields')}>
<Button size="small" type="text" onClick={() => toggleFallbackEdit(record.rowKey)}>
<SettingOutlined />
</Button>
</Tooltip>
<Button size="small" type="text" danger onClick={() => removeFallback(index)}>
<DeleteOutlined />
</Button>
</Space>
</Col>
</Row>
{fallbackEditing.has(record.rowKey) && (
<Row gutter={8} style={{ marginTop: 8 }}>
<Col xs={24} md={8}>
<Space.Compact block>
<InputAddon>SNI</InputAddon>
<Input placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
value={record.name} onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })} />
</Space.Compact>
</Col>
<Col xs={24} md={5}>
<Space.Compact block>
<InputAddon>ALPN</InputAddon>
<Input placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
value={record.alpn} onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })} />
</Space.Compact>
</Col>
<Col xs={24} md={7}>
<Space.Compact block>
<InputAddon>Path</InputAddon>
<Input placeholder="/" value={record.path}
onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })} />
</Space.Compact>
</Col>
<Col xs={24} md={4}>
<Space.Compact block>
<InputAddon>xver</InputAddon>
<InputNumber min={0} max={2} style={{ width: '100%' }}
value={record.xver}
onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })} />
</Space.Compact>
</Col>
</Row>
)}
</div>
))}
<Space size={8} style={{ marginTop: 4 }} wrap>
<Button size="small" onClick={() => addFallback()}>
<PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
</Button>
<Button size="small" type="primary" ghost onClick={quickAddAllFallbacks}>
{t('pages.inbounds.fallbacks.quickAddAll') || 'Quick add all eligible'}
</Button>
</Space>
</Card>
);
const renderProtocolTab = () => (
<>
{isVlessLike && (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
<Form.Item label={t('pages.inbounds.decryption')}>
<Input value={ib.settings.decryption} onChange={(e) => { ib.settings.decryption = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.encryption')}>
<Input value={ib.settings.encryption} onChange={(e) => { ib.settings.encryption = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label=" ">
<Space size={8} wrap>
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
{t('pages.inbounds.vlessAuthX25519')}
</Button>
<Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
{t('pages.inbounds.vlessAuthMlkem768')}
</Button>
<Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
</Space>
<Text type="secondary" className="vless-auth-state">
{t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
</Text>
</Form.Item>
</Form>
)}
{isFallbackHost && renderFallbacksCard()}
{ib.protocol === Protocols.SHADOWSOCKS && (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
<Form.Item label="Encryption method">
<Select value={ib.settings.method} onChange={(v) => { ib.settings.method = v; onSSMethodChange(); }}>
{Object.entries(SSMethods).map(([k, m]) => (
<Select.Option key={k} value={m as string}>{k}</Select.Option>
))}
</Select>
</Form.Item>
{ib.isSS2022 && (
<Form.Item label={<>Password <SyncOutlined className="random-icon" onClick={() => randomSSPassword(ib.settings)} /></>}>
<Input value={ib.settings.password} onChange={(e) => { ib.settings.password = e.target.value; refresh(); }} />
</Form.Item>
)}
<Form.Item label="Network">
<Select value={ib.settings.network} style={{ width: 120 }} onChange={(v) => { ib.settings.network = v; refresh(); }}>
<Select.Option value="tcp,udp">TCP, UDP</Select.Option>
<Select.Option value="tcp">TCP</Select.Option>
<Select.Option value="udp">UDP</Select.Option>
</Select>
</Form.Item>
<Form.Item label="ivCheck">
<Switch checked={!!ib.settings.ivCheck} onChange={(v) => { ib.settings.ivCheck = v; refresh(); }} />
</Form.Item>
</Form>
)}
{(ib.protocol === Protocols.HTTP || ib.protocol === Protocols.MIXED) && (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
<Form.Item label="Accounts">
<Button size="small" onClick={() => {
const Account = ib.protocol === Protocols.HTTP
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
? Inbound.HttpSettings.HttpAccount
: Inbound.MixedSettings.SocksAccount;
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
ib.settings.addAccount(new Account());
refresh();
}}>
<PlusOutlined /> Add
</Button>
</Form.Item>
<Form.Item wrapperCol={{ span: 24 }}>
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
{(ib.settings.accounts || []).map((account: HttpAccount, idx: number) => (
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
<Space.Compact key={idx} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={account.user} placeholder="Username"
onChange={(e) => { account.user = e.target.value; refresh(); }} />
<Input value={account.pass} placeholder="Password"
onChange={(e) => { account.pass = e.target.value; refresh(); }} />
<Button onClick={() => { ib.settings.delAccount(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
{ib.protocol === Protocols.HTTP && (
<Form.Item label="Allow transparent">
<Switch checked={!!ib.settings.allowTransparent} onChange={(v) => { ib.settings.allowTransparent = v; refresh(); }} />
</Form.Item>
)}
{ib.protocol === Protocols.MIXED && (
<>
<Form.Item label="Auth">
<Select value={ib.settings.auth} onChange={(v) => { ib.settings.auth = v; refresh(); }}>
<Select.Option value="noauth">noauth</Select.Option>
<Select.Option value="password">password</Select.Option>
</Select>
</Form.Item>
<Form.Item label="UDP">
<Switch checked={!!ib.settings.udp} onChange={(v) => { ib.settings.udp = v; refresh(); }} />
</Form.Item>
{ib.settings.udp && (
<Form.Item label="UDP IP">
<Input value={ib.settings.ip} onChange={(e) => { ib.settings.ip = e.target.value; refresh(); }} />
</Form.Item>
)}
</>
)}
</Form>
)}
{ib.protocol === Protocols.TUNNEL && (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
<Form.Item label="Rewrite address">
<Input value={ib.settings.rewriteAddress} onChange={(e) => { ib.settings.rewriteAddress = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="Rewrite port">
<InputNumber value={ib.settings.rewritePort} min={0} max={65535}
onChange={(v) => { ib.settings.rewritePort = Number(v) || 0; refresh(); }} />
</Form.Item>
<Form.Item label="Allowed network">
<Select value={ib.settings.allowedNetwork} onChange={(v) => { ib.settings.allowedNetwork = v; refresh(); }}>
<Select.Option value="tcp,udp">TCP, UDP</Select.Option>
<Select.Option value="tcp">TCP</Select.Option>
<Select.Option value="udp">UDP</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Port map">
<Button size="small" onClick={() => { ib.settings.addPortMap('', ''); refresh(); }}>
<PlusOutlined />
</Button>
</Form.Item>
{(ib.settings.portMap || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.settings.portMap as { name: string; value: string }[]).map((pm, idx) => (
<Space.Compact key={`pm-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={pm.name} placeholder="5555"
onChange={(e) => { pm.name = e.target.value; refresh(); }} />
<Input value={pm.value} placeholder="1.1.1.1:7777"
onChange={(e) => { pm.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.settings.removePortMap(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
<Form.Item label="Follow redirect">
<Switch checked={!!ib.settings.followRedirect} onChange={(v) => { ib.settings.followRedirect = v; refresh(); }} />
</Form.Item>
</Form>
)}
{ib.protocol === Protocols.TUN && (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
<Form.Item label="Interface name">
<Input value={ib.settings.name} placeholder="xray0"
onChange={(e) => { ib.settings.name = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="MTU">
<InputNumber value={ib.settings.mtu} min={0}
onChange={(v) => { ib.settings.mtu = Number(v) || 0; refresh(); }} />
</Form.Item>
<Form.Item label="Gateway">
<Button size="small" onClick={() => { ib.settings.gateway.push(''); refresh(); }}>
<PlusOutlined />
</Button>
{(ib.settings.gateway || []).map((_ip: string, j: number) => (
<Space.Compact key={`tun-gw-${j}`} block className="mt-4">
<Input
placeholder={j === 0 ? '10.0.0.1/16' : 'fc00::1/64'}
value={ib.settings.gateway[j]}
onChange={(e) => { ib.settings.gateway[j] = e.target.value; refresh(); }} />
<Button size="small" onClick={() => { ib.settings.gateway.splice(j, 1); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
<Form.Item label="DNS">
<Button size="small" onClick={() => { ib.settings.dns.push(''); refresh(); }}>
<PlusOutlined />
</Button>
{(ib.settings.dns || []).map((_ip: string, j: number) => (
<Space.Compact key={`tun-dns-${j}`} block className="mt-4">
<Input
placeholder={j === 0 ? '1.1.1.1' : '8.8.8.8'}
value={ib.settings.dns[j]}
onChange={(e) => { ib.settings.dns[j] = e.target.value; refresh(); }} />
<Button size="small" onClick={() => { ib.settings.dns.splice(j, 1); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
<Form.Item label="User level">
<InputNumber value={ib.settings.userLevel} min={0}
onChange={(v) => { ib.settings.userLevel = Number(v) || 0; refresh(); }} />
</Form.Item>
<Form.Item label={<Tooltip title="Windows-only. CIDRs added to the system routing table automatically so matching traffic goes through TUN.">Auto system routes</Tooltip>}>
<Button size="small" onClick={() => { ib.settings.autoSystemRoutingTable.push(''); refresh(); }}>
<PlusOutlined />
</Button>
{(ib.settings.autoSystemRoutingTable || []).map((_ip: string, j: number) => (
<Space.Compact key={`tun-rt-${j}`} block className="mt-4">
<Input
placeholder={j === 0 ? '0.0.0.0/0' : '::/0'}
value={ib.settings.autoSystemRoutingTable[j]}
onChange={(e) => { ib.settings.autoSystemRoutingTable[j] = e.target.value; refresh(); }} />
<Button size="small" onClick={() => { ib.settings.autoSystemRoutingTable.splice(j, 1); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
<Form.Item label={<Tooltip title="Physical interface for outbound traffic. Use 'auto' to detect; auto-enabled when Auto system routes is set.">Auto outbounds interface</Tooltip>}>
<Input value={ib.settings.autoOutboundsInterface} placeholder="auto"
onChange={(e) => { ib.settings.autoOutboundsInterface = e.target.value; refresh(); }} />
</Form.Item>
</Form>
)}
{ib.protocol === Protocols.WIREGUARD && (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }} className="mt-12">
<Form.Item label={<>Secret key <SyncOutlined className="random-icon" onClick={regenInboundWg} /></>}>
<Input value={ib.settings.secretKey}
onChange={(e) => { ib.settings.secretKey = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="Public key">
<Input value={ib.settings.pubKey} disabled />
</Form.Item>
<Form.Item label="MTU">
<InputNumber value={ib.settings.mtu}
onChange={(v) => { ib.settings.mtu = Number(v) || 0; refresh(); }} />
</Form.Item>
<Form.Item label="No-kernel TUN">
<Switch checked={!!ib.settings.noKernelTun}
onChange={(v) => { ib.settings.noKernelTun = v; refresh(); }} />
</Form.Item>
<Form.Item label="Peers">
<Button size="small" onClick={() => { ib.settings.addPeer(); refresh(); }}>
<PlusOutlined /> Add peer
</Button>
</Form.Item>
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
{(ib.settings.peers || []).map((peer: WireguardPeer, idx: number) => (
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
<div key={idx} className="wg-peer">
<Divider style={{ margin: '8px 0' }}>
Peer {idx + 1}
{ib.settings.peers.length > 1 && (
<DeleteOutlined className="danger-icon" onClick={() => { ib.settings.delPeer(idx); refresh(); }} />
)}
</Divider>
<Form.Item label={<>Secret key <SyncOutlined className="random-icon" onClick={() => regenWgKeypair(peer)} /></>}>
<Input value={peer.privateKey} onChange={(e) => { peer.privateKey = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="Public key">
<Input value={peer.publicKey} onChange={(e) => { peer.publicKey = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="PSK">
<Input value={peer.psk} onChange={(e) => { peer.psk = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="Allowed IPs">
<Button size="small" onClick={() => { peer.allowedIPs.push(''); refresh(); }}>
<PlusOutlined />
</Button>
{(peer.allowedIPs || []).map((_ip: string, j: number) => (
<Space.Compact key={j} block className="mt-4">
<Input
value={peer.allowedIPs[j]}
onChange={(e) => { peer.allowedIPs[j] = e.target.value; refresh(); }} />
{peer.allowedIPs.length > 1 && (
<Button size="small" onClick={() => { peer.allowedIPs.splice(j, 1); refresh(); }}>
<MinusOutlined />
</Button>
)}
</Space.Compact>
))}
</Form.Item>
<Form.Item label="Keep-alive">
<InputNumber value={peer.keepAlive} min={0}
onChange={(v) => { peer.keepAlive = Number(v) || 0; refresh(); }} />
</Form.Item>
</div>
))}
</Form>
)}
</>
);
const renderStreamTab = () => {
const network = ib.stream?.network;
return (
<>
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
{ib.protocol !== Protocols.HYSTERIA && (
<Form.Item label="Transmission">
<Select value={network} style={{ width: '75%' }} onChange={onNetworkChange}>
<Select.Option value="tcp">TCP (RAW)</Select.Option>
<Select.Option value="kcp">mKCP</Select.Option>
<Select.Option value="ws">WebSocket</Select.Option>
<Select.Option value="grpc">gRPC</Select.Option>
<Select.Option value="httpupgrade">HTTPUpgrade</Select.Option>
<Select.Option value="xhttp">XHTTP</Select.Option>
</Select>
</Form.Item>
)}
{network === 'tcp' && (
<>
{canEnableTls && (
<Form.Item label="Proxy Protocol">
<Switch checked={!!ib.stream.tcp.acceptProxyProtocol}
onChange={(v) => { ib.stream.tcp.acceptProxyProtocol = v; refresh(); }} />
</Form.Item>
)}
<Form.Item label={`HTTP ${t('camouflage')}`}>
<Switch checked={ib.stream.tcp.type === 'http'}
onChange={(v) => { ib.stream.tcp.type = v ? 'http' : 'none'; refresh(); }} />
</Form.Item>
{ib.stream.tcp.type === 'http' && (
<>
<Divider style={{ margin: 0 }}>{t('pages.inbounds.stream.general.request')}</Divider>
<Form.Item label={t('pages.inbounds.stream.tcp.version')}>
<Input value={ib.stream.tcp.request.version}
onChange={(e) => { ib.stream.tcp.request.version = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.method')}>
<Input value={ib.stream.tcp.request.method}
onChange={(e) => { ib.stream.tcp.request.method = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={<>{t('pages.inbounds.stream.tcp.path')} <Button size="small" style={{ marginLeft: 6 }} onClick={() => { ib.stream.tcp.request.addPath('/'); refresh(); }}><PlusOutlined /></Button></>}>
{(ib.stream.tcp.request.path || []).map((_p: string, idx: number) => (
<Space.Compact key={`tcp-path-${idx}`} block className="mb-4">
<Input
value={ib.stream.tcp.request.path[idx]}
onChange={(e) => { ib.stream.tcp.request.path[idx] = e.target.value; refresh(); }} />
{ib.stream.tcp.request.path.length > 1 && (
<Button size="small" onClick={() => { ib.stream.tcp.request.removePath(idx); refresh(); }}>
<MinusOutlined />
</Button>
)}
</Space.Compact>
))}
</Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
<Button size="small" onClick={() => { ib.stream.tcp.request.addHeader('Host', ''); refresh(); }}>
<PlusOutlined />
</Button>
</Form.Item>
{(ib.stream.tcp.request.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.tcp.request.headers as { name: string; value: string }[]).map((h, idx) => (
<Space.Compact key={`tcp-rh-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={h.name}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input value={h.value}
placeholder={t('pages.inbounds.stream.general.value')}
onChange={(e) => { h.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.stream.tcp.request.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
<Divider style={{ margin: 0 }}>{t('pages.inbounds.stream.general.response')}</Divider>
<Form.Item label={t('pages.inbounds.stream.tcp.version')}>
<Input value={ib.stream.tcp.response.version}
onChange={(e) => { ib.stream.tcp.response.version = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.status')}>
<Input value={ib.stream.tcp.response.status}
onChange={(e) => { ib.stream.tcp.response.status = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.statusDescription')}>
<Input value={ib.stream.tcp.response.reason}
onChange={(e) => { ib.stream.tcp.response.reason = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.responseHeader')}>
<Button size="small" onClick={() => { ib.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream'); refresh(); }}>
<PlusOutlined />
</Button>
</Form.Item>
{(ib.stream.tcp.response.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.tcp.response.headers as { name: string; value: string }[]).map((h, idx) => (
<Space.Compact key={`tcp-rsh-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={h.name}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input value={h.value}
placeholder={t('pages.inbounds.stream.general.value')}
onChange={(e) => { h.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.stream.tcp.response.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
</>
)}
</>
)}
{network === 'kcp' && (
<>
<Form.Item label="MTU"><InputNumber value={ib.stream.kcp.mtu} min={576} max={1460} onChange={(v) => { ib.stream.kcp.mtu = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="TTI (ms)"><InputNumber value={ib.stream.kcp.tti} min={10} max={100} onChange={(v) => { ib.stream.kcp.tti = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Uplink (MB/s)"><InputNumber value={ib.stream.kcp.upCap} min={0} onChange={(v) => { ib.stream.kcp.upCap = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Downlink (MB/s)"><InputNumber value={ib.stream.kcp.downCap} min={0} onChange={(v) => { ib.stream.kcp.downCap = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="CWND Multiplier"><InputNumber value={ib.stream.kcp.cwndMultiplier} min={1} onChange={(v) => { ib.stream.kcp.cwndMultiplier = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Max Sending Window"><InputNumber value={ib.stream.kcp.maxSendingWindow} min={0} onChange={(v) => { ib.stream.kcp.maxSendingWindow = Number(v) || 0; refresh(); }} /></Form.Item>
</>
)}
{network === 'ws' && (
<>
<Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.ws.acceptProxyProtocol} onChange={(v) => { ib.stream.ws.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
<Form.Item label={t('host')}><Input value={ib.stream.ws.host} onChange={(e) => { ib.stream.ws.host = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={t('path')}><Input value={ib.stream.ws.path} onChange={(e) => { ib.stream.ws.path = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Heartbeat Period"><InputNumber value={ib.stream.ws.heartbeatPeriod} min={0} onChange={(v) => { ib.stream.ws.heartbeatPeriod = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
<Button size="small" onClick={() => { ib.stream.ws.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
</Form.Item>
{(ib.stream.ws.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.ws.headers as { name: string; value: string }[]).map((h, idx) => (
<Space.Compact key={`ws-h-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={h.name}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input value={h.value}
placeholder={t('pages.inbounds.stream.general.value')}
onChange={(e) => { h.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.stream.ws.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
</>
)}
{network === 'grpc' && (
<>
<Form.Item label="Service Name"><Input value={ib.stream.grpc.serviceName} onChange={(e) => { ib.stream.grpc.serviceName = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Authority"><Input value={ib.stream.grpc.authority} onChange={(e) => { ib.stream.grpc.authority = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Multi Mode"><Switch checked={!!ib.stream.grpc.multiMode} onChange={(v) => { ib.stream.grpc.multiMode = v; refresh(); }} /></Form.Item>
</>
)}
{network === 'httpupgrade' && (
<>
<Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.httpupgrade.acceptProxyProtocol} onChange={(v) => { ib.stream.httpupgrade.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
<Form.Item label={t('host')}><Input value={ib.stream.httpupgrade.host} onChange={(e) => { ib.stream.httpupgrade.host = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={t('path')}><Input value={ib.stream.httpupgrade.path} onChange={(e) => { ib.stream.httpupgrade.path = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
<Button size="small" onClick={() => { ib.stream.httpupgrade.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
</Form.Item>
{(ib.stream.httpupgrade.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.httpupgrade.headers as { name: string; value: string }[]).map((h, idx) => (
<Space.Compact key={`hu-h-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={h.name}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input value={h.value}
placeholder={t('pages.inbounds.stream.general.value')}
onChange={(e) => { h.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.stream.httpupgrade.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
</>
)}
{network === 'xhttp' && (
<>
<Form.Item label={t('host')}><Input value={ib.stream.xhttp.host} onChange={(e) => { ib.stream.xhttp.host = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={t('path')}><Input value={ib.stream.xhttp.path} onChange={(e) => { ib.stream.xhttp.path = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={t('pages.inbounds.stream.tcp.requestHeader')}>
<Button size="small" onClick={() => { ib.stream.xhttp.addHeader('', ''); refresh(); }}><PlusOutlined /></Button>
</Form.Item>
{(ib.stream.xhttp.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.xhttp.headers as { name: string; value: string }[]).map((h, idx) => (
<Space.Compact key={`xh-h-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={h.name}
placeholder={t('pages.inbounds.stream.general.name')}
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input value={h.value}
placeholder={t('pages.inbounds.stream.general.value')}
onChange={(e) => { h.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.stream.xhttp.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
<Form.Item label="Mode">
<Select value={ib.stream.xhttp.mode} style={{ width: '50%' }} onChange={(v) => { ib.stream.xhttp.mode = v; refresh(); }}>
{MODE_OPTIONS.map((m) => <Select.Option key={m} value={m}>{m}</Select.Option>)}
</Select>
</Form.Item>
{ib.stream.xhttp.mode === 'packet-up' && (
<>
<Form.Item label="Max Buffered Upload"><InputNumber value={ib.stream.xhttp.scMaxBufferedPosts} onChange={(v) => { ib.stream.xhttp.scMaxBufferedPosts = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Max Upload Size (Byte)"><Input value={ib.stream.xhttp.scMaxEachPostBytes} onChange={(e) => { ib.stream.xhttp.scMaxEachPostBytes = e.target.value; refresh(); }} /></Form.Item>
</>
)}
{ib.stream.xhttp.mode === 'stream-up' && (
<Form.Item label="Stream-Up Server"><Input value={ib.stream.xhttp.scStreamUpServerSecs} onChange={(e) => { ib.stream.xhttp.scStreamUpServerSecs = e.target.value; refresh(); }} /></Form.Item>
)}
<Form.Item label="Server Max Header Bytes"><InputNumber value={ib.stream.xhttp.serverMaxHeaderBytes} min={0} placeholder="0 (default)" onChange={(v) => { ib.stream.xhttp.serverMaxHeaderBytes = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Padding Bytes"><Input value={ib.stream.xhttp.xPaddingBytes} onChange={(e) => { ib.stream.xhttp.xPaddingBytes = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Uplink HTTP Method">
<Select value={ib.stream.xhttp.uplinkHTTPMethod || ''} onChange={(v) => { ib.stream.xhttp.uplinkHTTPMethod = v; refresh(); }}>
<Select.Option value="">Default (POST)</Select.Option>
<Select.Option value="POST">POST</Select.Option>
<Select.Option value="PUT">PUT</Select.Option>
<Select.Option value="GET" disabled={ib.stream.xhttp.mode !== 'packet-up'}>GET (packet-up only)</Select.Option>
</Select>
</Form.Item>
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
<Form.Item label="Padding Obfs Mode"><Switch checked={!!ib.stream.xhttp.xPaddingObfsMode} onChange={(v) => { ib.stream.xhttp.xPaddingObfsMode = v; refresh(); }} /></Form.Item>
{ib.stream.xhttp.xPaddingObfsMode && (
<>
<Form.Item label="Padding Key"><Input value={ib.stream.xhttp.xPaddingKey} placeholder="x_padding" onChange={(e) => { ib.stream.xhttp.xPaddingKey = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Padding Header"><Input value={ib.stream.xhttp.xPaddingHeader} placeholder="X-Padding" onChange={(e) => { ib.stream.xhttp.xPaddingHeader = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Padding Placement">
<Select value={ib.stream.xhttp.xPaddingPlacement} onChange={(v) => { ib.stream.xhttp.xPaddingPlacement = v; refresh(); }}>
<Select.Option value="">Default (queryInHeader)</Select.Option>
<Select.Option value="queryInHeader">queryInHeader</Select.Option>
<Select.Option value="header">header</Select.Option>
<Select.Option value="cookie">cookie</Select.Option>
<Select.Option value="query">query</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Padding Method">
<Select value={ib.stream.xhttp.xPaddingMethod} onChange={(v) => { ib.stream.xhttp.xPaddingMethod = v; refresh(); }}>
<Select.Option value="">Default (repeat-x)</Select.Option>
<Select.Option value="repeat-x">repeat-x</Select.Option>
<Select.Option value="tokenish">tokenish</Select.Option>
</Select>
</Form.Item>
</>
)}
<Form.Item label="Session Placement">
<Select value={ib.stream.xhttp.sessionPlacement} onChange={(v) => { ib.stream.xhttp.sessionPlacement = v; refresh(); }}>
<Select.Option value="">Default (path)</Select.Option>
<Select.Option value="path">path</Select.Option>
<Select.Option value="header">header</Select.Option>
<Select.Option value="cookie">cookie</Select.Option>
<Select.Option value="query">query</Select.Option>
</Select>
</Form.Item>
{ib.stream.xhttp.sessionPlacement && ib.stream.xhttp.sessionPlacement !== 'path' && (
<Form.Item label="Session Key"><Input value={ib.stream.xhttp.sessionKey} placeholder="x_session" onChange={(e) => { ib.stream.xhttp.sessionKey = e.target.value; refresh(); }} /></Form.Item>
)}
<Form.Item label="Sequence Placement">
<Select value={ib.stream.xhttp.seqPlacement} onChange={(v) => { ib.stream.xhttp.seqPlacement = v; refresh(); }}>
<Select.Option value="">Default (path)</Select.Option>
<Select.Option value="path">path</Select.Option>
<Select.Option value="header">header</Select.Option>
<Select.Option value="cookie">cookie</Select.Option>
<Select.Option value="query">query</Select.Option>
</Select>
</Form.Item>
{ib.stream.xhttp.seqPlacement && ib.stream.xhttp.seqPlacement !== 'path' && (
<Form.Item label="Sequence Key"><Input value={ib.stream.xhttp.seqKey} placeholder="x_seq" onChange={(e) => { ib.stream.xhttp.seqKey = e.target.value; refresh(); }} /></Form.Item>
)}
{ib.stream.xhttp.mode === 'packet-up' && (
<Form.Item label="Uplink Data Placement">
<Select value={ib.stream.xhttp.uplinkDataPlacement} onChange={(v) => { ib.stream.xhttp.uplinkDataPlacement = v; refresh(); }}>
<Select.Option value="">Default (body)</Select.Option>
<Select.Option value="body">body</Select.Option>
<Select.Option value="header">header</Select.Option>
<Select.Option value="cookie">cookie</Select.Option>
<Select.Option value="query">query</Select.Option>
</Select>
</Form.Item>
)}
{ib.stream.xhttp.mode === 'packet-up' && ib.stream.xhttp.uplinkDataPlacement && ib.stream.xhttp.uplinkDataPlacement !== 'body' && (
<Form.Item label="Uplink Data Key"><Input value={ib.stream.xhttp.uplinkDataKey} placeholder="x_data" onChange={(e) => { ib.stream.xhttp.uplinkDataKey = e.target.value; refresh(); }} /></Form.Item>
)}
<Form.Item label="No SSE Header"><Switch checked={!!ib.stream.xhttp.noSSEHeader} onChange={(v) => { ib.stream.xhttp.noSSEHeader = v; refresh(); }} /></Form.Item>
</>
)}
<Form.Item label="External Proxy">
<Switch checked={externalProxyOn} onChange={setExternalProxy} />
{externalProxyOn && (
<Button size="small" type="primary" style={{ marginLeft: 10 }}
onClick={() => { ib.stream.externalProxy.push({ forceTls: 'same', dest: '', port: 443, remark: '', sni: '', fingerprint: '', alpn: [] }); refresh(); }}>
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
<PlusOutlined />
</Button>
)}
</Form.Item>
{externalProxyOn && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.externalProxy as { forceTls: string; dest: string; port: number; remark: string; sni?: string; fingerprint?: string; alpn?: string[] }[]).map((row, idx) => (
<div key={`ep-${idx}`} style={{ margin: '8px 0' }}>
<Space.Compact block>
<Tooltip title="Force TLS">
<Select value={row.forceTls} style={{ width: '20%' }} onChange={(v) => { row.forceTls = v; refresh(); }}>
<Select.Option value="same">{t('pages.inbounds.same')}</Select.Option>
<Select.Option value="none">{t('none')}</Select.Option>
<Select.Option value="tls">TLS</Select.Option>
</Select>
</Tooltip>
<Input style={{ width: '30%' }} value={row.dest} placeholder={t('host')}
onChange={(e) => { row.dest = e.target.value; refresh(); }} />
<Tooltip title={t('pages.inbounds.port')}>
<InputNumber value={row.port} style={{ width: '15%' }} min={1} max={65535}
onChange={(v) => { row.port = Number(v) || 0; refresh(); }} />
</Tooltip>
<Input style={{ width: '25%' }} value={row.remark} placeholder={t('pages.inbounds.remark')}
onChange={(e) => { row.remark = e.target.value; refresh(); }} />
<InputAddon onClick={() => { ib.stream.externalProxy.splice(idx, 1); refresh(); }}>
<MinusOutlined />
</InputAddon>
</Space.Compact>
{row.forceTls === 'tls' && (
<Space.Compact style={{ marginTop: 6 }} block>
<Input style={{ width: '30%' }} value={row.sni || ''} placeholder="SNI (defaults to host)"
onChange={(e) => { row.sni = e.target.value; refresh(); }} />
<Select value={row.fingerprint || ''} style={{ width: '30%' }} placeholder="Fingerprint"
onChange={(v) => { row.fingerprint = v; refresh(); }}>
<Select.Option value="">Default</Select.Option>
{FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
</Select>
<Select mode="multiple" value={row.alpn || []} style={{ width: '40%' }} placeholder="ALPN"
onChange={(v) => { row.alpn = v; refresh(); }}>
{ALPNS.map((alpn) => <Select.Option key={alpn} value={alpn}>{alpn}</Select.Option>)}
</Select>
</Space.Compact>
)}
</div>
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
))}
</Form.Item>
)}
<Form.Item label="Sockopt"><Switch checked={!!ib.stream.sockoptSwitch} onChange={(v) => { ib.stream.sockoptSwitch = v; refresh(); }} /></Form.Item>
{ib.stream.sockoptSwitch && ib.stream.sockopt && (
<>
<Form.Item label="Route Mark"><InputNumber value={ib.stream.sockopt.mark} min={0} onChange={(v) => { ib.stream.sockopt.mark = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="TCP Keep Alive Interval"><InputNumber value={ib.stream.sockopt.tcpKeepAliveInterval} min={0} onChange={(v) => { ib.stream.sockopt.tcpKeepAliveInterval = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="TCP Keep Alive Idle"><InputNumber value={ib.stream.sockopt.tcpKeepAliveIdle} min={0} onChange={(v) => { ib.stream.sockopt.tcpKeepAliveIdle = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="TCP Max Seg"><InputNumber value={ib.stream.sockopt.tcpMaxSeg} min={0} onChange={(v) => { ib.stream.sockopt.tcpMaxSeg = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="TCP User Timeout"><InputNumber value={ib.stream.sockopt.tcpUserTimeout} min={0} onChange={(v) => { ib.stream.sockopt.tcpUserTimeout = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="TCP Window Clamp"><InputNumber value={ib.stream.sockopt.tcpWindowClamp} min={0} onChange={(v) => { ib.stream.sockopt.tcpWindowClamp = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Proxy Protocol"><Switch checked={!!ib.stream.sockopt.acceptProxyProtocol} onChange={(v) => { ib.stream.sockopt.acceptProxyProtocol = v; refresh(); }} /></Form.Item>
<Form.Item label="TCP Fast Open"><Switch checked={!!ib.stream.sockopt.tcpFastOpen} onChange={(v) => { ib.stream.sockopt.tcpFastOpen = v; refresh(); }} /></Form.Item>
<Form.Item label="Multipath TCP"><Switch checked={!!ib.stream.sockopt.tcpMptcp} onChange={(v) => { ib.stream.sockopt.tcpMptcp = v; refresh(); }} /></Form.Item>
<Form.Item label="Penetrate"><Switch checked={!!ib.stream.sockopt.penetrate} onChange={(v) => { ib.stream.sockopt.penetrate = v; refresh(); }} /></Form.Item>
<Form.Item label="V6 Only"><Switch checked={!!ib.stream.sockopt.V6Only} onChange={(v) => { ib.stream.sockopt.V6Only = v; refresh(); }} /></Form.Item>
<Form.Item label="Domain Strategy">
<Select value={ib.stream.sockopt.domainStrategy} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.domainStrategy = v; refresh(); }}>
{DOMAIN_STRATEGIES.map((d) => <Select.Option key={d} value={d}>{d}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label="TCP Congestion">
<Select value={ib.stream.sockopt.tcpcongestion} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.tcpcongestion = v; refresh(); }}>
{TCP_CONGESTIONS.map((c) => <Select.Option key={c} value={c}>{c}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label="TProxy">
<Select value={ib.stream.sockopt.tproxy} style={{ width: '50%' }} onChange={(v) => { ib.stream.sockopt.tproxy = v; refresh(); }}>
<Select.Option value="off">Off</Select.Option>
<Select.Option value="redirect">Redirect</Select.Option>
<Select.Option value="tproxy">TProxy</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Dialer Proxy"><Input value={ib.stream.sockopt.dialerProxy} onChange={(e) => { ib.stream.sockopt.dialerProxy = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Interface Name"><Input value={ib.stream.sockopt.interfaceName} onChange={(e) => { ib.stream.sockopt.interfaceName = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Trusted X-Forwarded-For">
<Select mode="tags" value={ib.stream.sockopt.trustedXForwardedFor} style={{ width: '100%' }}
tokenSeparators={[',']}
onChange={(v) => { ib.stream.sockopt.trustedXForwardedFor = v; refresh(); }}>
<Select.Option value="CF-Connecting-IP">CF-Connecting-IP</Select.Option>
<Select.Option value="X-Real-IP">X-Real-IP</Select.Option>
<Select.Option value="True-Client-IP">True-Client-IP</Select.Option>
<Select.Option value="X-Client-IP">X-Client-IP</Select.Option>
</Select>
</Form.Item>
</>
)}
{ib.protocol === Protocols.HYSTERIA && (
<>
<Form.Item label={<Tooltip title="Hysteria protocol version. Currently must be 2.">Version</Tooltip>}>
<InputNumber value={ib.stream.hysteria.version} min={2} max={2} onChange={(v) => { ib.stream.hysteria.version = Number(v) || 2; refresh(); }} />
</Form.Item>
<Form.Item label={<Tooltip title="Idle timeout (seconds) for a single QUIC native UDP connection.">UDP idle timeout</Tooltip>}>
<InputNumber value={ib.stream.hysteria.udpIdleTimeout} min={0} onChange={(v) => { ib.stream.hysteria.udpIdleTimeout = Number(v) || 0; refresh(); }} />
</Form.Item>
<Form.Item label="Masquerade">
<Switch checked={!!ib.stream.hysteria.masqueradeSwitch} onChange={(v) => { ib.stream.hysteria.masqueradeSwitch = v; refresh(); }} />
</Form.Item>
{ib.stream.hysteria.masqueradeSwitch && (
<>
<Form.Item label="Type">
<Select value={ib.stream.hysteria.masquerade.type} style={{ width: '50%' }} onChange={(v) => { ib.stream.hysteria.masquerade.type = v; refresh(); }}>
<Select.Option value="proxy">Proxy</Select.Option>
<Select.Option value="file">File</Select.Option>
<Select.Option value="string">String</Select.Option>
</Select>
</Form.Item>
{ib.stream.hysteria.masquerade.type === 'proxy' && (
<>
<Form.Item label="URL"><Input value={ib.stream.hysteria.masquerade.url} placeholder="https://example.com" onChange={(e) => { ib.stream.hysteria.masquerade.url = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Rewrite Host"><Switch checked={!!ib.stream.hysteria.masquerade.rewriteHost} onChange={(v) => { ib.stream.hysteria.masquerade.rewriteHost = v; refresh(); }} /></Form.Item>
<Form.Item label="Insecure"><Switch checked={!!ib.stream.hysteria.masquerade.insecure} onChange={(v) => { ib.stream.hysteria.masquerade.insecure = v; refresh(); }} /></Form.Item>
</>
)}
{ib.stream.hysteria.masquerade.type === 'file' && (
<Form.Item label="Directory"><Input value={ib.stream.hysteria.masquerade.dir} placeholder="/path/to/www" onChange={(e) => { ib.stream.hysteria.masquerade.dir = e.target.value; refresh(); }} /></Form.Item>
)}
{ib.stream.hysteria.masquerade.type === 'string' && (
<>
<Form.Item label="Content"><TextArea value={ib.stream.hysteria.masquerade.content} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.hysteria.masquerade.content = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Status Code"><InputNumber value={ib.stream.hysteria.masquerade.statusCode} min={100} max={599} placeholder="200" onChange={(v) => { ib.stream.hysteria.masquerade.statusCode = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Headers">
<Button size="small" onClick={() => { ib.stream.hysteria.masquerade.addHeader('', ''); refresh(); }}>
<PlusOutlined />
</Button>
</Form.Item>
{(ib.stream.hysteria.masquerade.headers || []).length > 0 && (
<Form.Item wrapperCol={{ span: 24 }}>
{(ib.stream.hysteria.masquerade.headers as { name: string; value: string }[]).map((h, idx) => (
<Space.Compact key={`mh-${idx}`} className="mb-8" block>
<InputAddon>{String(idx + 1)}</InputAddon>
<Input value={h.name} placeholder="Name"
onChange={(e) => { h.name = e.target.value; refresh(); }} />
<Input value={h.value} placeholder="Value"
onChange={(e) => { h.value = e.target.value; refresh(); }} />
<Button onClick={() => { ib.stream.hysteria.masquerade.removeHeader(idx); refresh(); }}>
<MinusOutlined />
</Button>
</Space.Compact>
))}
</Form.Item>
)}
</>
)}
</>
)}
</>
)}
</Form>
<FinalMaskForm stream={ib.stream} protocol={ib.protocol} onChange={refresh} />
</>
);
};
const renderSecurityTab = () => (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
<Form.Item label={t('pages.inbounds.securityTab')}>
<Radio.Group value={ib.stream.security} buttonStyle="solid" disabled={!canEnableTls}
onChange={(e) => setSecurity(e.target.value)}>
<Radio.Button value="none">none</Radio.Button>
<Radio.Button value="tls">tls</Radio.Button>
{canEnableReality && <Radio.Button value="reality">reality</Radio.Button>}
</Radio.Group>
</Form.Item>
{ib.stream.security === 'tls' && ib.stream.tls && (
<>
<Form.Item label="SNI"><Input value={ib.stream.tls.sni} placeholder="Server Name Indication" onChange={(e) => { ib.stream.tls.sni = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Cipher Suites">
<Select value={ib.stream.tls.cipherSuites} onChange={(v) => { ib.stream.tls.cipherSuites = v; refresh(); }}>
<Select.Option value="">Auto</Select.Option>
{CIPHER_SUITES.map(([label, val]) => <Select.Option key={val} value={val}>{label}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label="Min/Max Version">
<Space.Compact block>
<Select value={ib.stream.tls.minVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.minVersion = v; refresh(); }}>
{TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
</Select>
<Select value={ib.stream.tls.maxVersion} style={{ width: '50%' }} onChange={(v) => { ib.stream.tls.maxVersion = v; refresh(); }}>
{TLS_VERSIONS.map((v) => <Select.Option key={v} value={v}>{v}</Select.Option>)}
</Select>
</Space.Compact>
</Form.Item>
<Form.Item label="uTLS">
<Select value={ib.stream.tls.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.tls.settings.fingerprint = v; refresh(); }}>
<Select.Option value="">None</Select.Option>
{FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label="ALPN">
<Select mode="multiple" value={ib.stream.tls.alpn} style={{ width: '100%' }} tokenSeparators={[',']}
onChange={(v) => { ib.stream.tls.alpn = v; refresh(); }}>
{ALPNS.map((a) => <Select.Option key={a} value={a}>{a}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label="Reject Unknown SNI"><Switch checked={!!ib.stream.tls.rejectUnknownSni} onChange={(v) => { ib.stream.tls.rejectUnknownSni = v; refresh(); }} /></Form.Item>
<Form.Item label="Disable System Root"><Switch checked={!!ib.stream.tls.disableSystemRoot} onChange={(v) => { ib.stream.tls.disableSystemRoot = v; refresh(); }} /></Form.Item>
<Form.Item label="Session Resumption"><Switch checked={!!ib.stream.tls.enableSessionResumption} onChange={(v) => { ib.stream.tls.enableSessionResumption = v; refresh(); }} /></Form.Item>
Migrate frontend models/api/utils to TypeScript and modernize AntD theming (#4563) * refactor(frontend): port api/* and reality-targets to TypeScript Phase 1 of the JS→TS migration: convert three small, isolated files (axios-init, websocket, reality-targets) to typed sources so future phases can lean on their interfaces. - api/axios-init.ts: typed CSRF cache, interceptors, request retry - api/websocket.ts: typed listener map, message envelope guard, reconnect timer - models/reality-targets.ts: RealityTarget interface, readonly list - env.d.ts: minimal qs module shim (stringify/parse) - consumers: drop ".js" extension from @/api imports * refactor(frontend): port utils/index to TypeScript Phase 2 of the JS→TS migration: convert the 858-line utility module that 30+ pages and hooks depend on. - Msg<T = any> generic with success/msg/obj shape preserved - HttpUtil get/post/postWithModal generic over response shape - RandomUtil, Wireguard, Base64 fully typed - SizeFormatter/CPUFormatter/TimeFormatter/NumberFormatter typed - ColorUtils.usageColor returns 'green'|'orange'|'red'|'purple' union - LanguageManager.supportedLanguages readonly typed - IntlUtil.formatDate/formatRelativeTime accept null/undefined - ObjectUtil.clone/deepClone/cloneProps/equals kept as `any`-shaped to preserve the prior JS contract used by class-instance callers (AllSetting.cloneProps(this, data), etc.) * refactor(frontend): port models/outbound to TypeScript (hybrid typing) Phase 4 of the JS→TS migration: rename outbound.js to outbound.ts and make it compile under strict mode with a minimal hybrid type pass. - Enum-like constants kept as typed objects (Protocols, SSMethods, …) - Top-level DNS helpers strictly typed - CommonClass gets [key: string]: any so all subclasses can keep their loose this.foo = bar assignments without per-field declarations - Constructor / fromJson / toJson signatures typed as any to preserve the prior JS contract used by consumers and parsers - Outbound declares static fields for the dynamically-attached Settings subclasses (Settings, FreedomSettings, VmessSettings, …) - urlParams.get() results that feed parseInt now use the non-null assertion since the surrounding has() check already guards them - File-level eslint-disable for no-explicit-any/no-var/prefer-const to keep the JS-derived code building without churn * refactor(frontend): port models/inbound to TypeScript (hybrid typing) Phase 5 of the JS→TS migration. Same hybrid approach as outbound.ts: constants typed strictly, classes get [key: string]: any from XrayCommonClass, constructor / fromJson / toJson signatures use any. - XrayCommonClass gains [key: string]: any plus typed static helpers (toJsonArray, fallbackToJson, toHeaders, toV2Headers) - TcpStreamSettings/TlsStreamSettings/RealityStreamSettings/Inbound declare static fields for their dynamically-attached subclasses (TcpRequest, TcpResponse, Cert, Settings, ClientBase, Vmess/VLESS/ Trojan/Shadowsocks/Hysteria/Tunnel/Mixed/Http/Wireguard/TunSettings) - All gen*Link, applyXhttpExtra*, applyExternalProxyTLS*, applyFinalMask* and related helpers explicitly any-typed - Constructor positional client-args (email, limitIp, totalGB, …) typed as optional any across Vmess/VLESS/Trojan/Shadowsocks/Hysteria.VMESS| VLESS|Trojan|Shadowsocks|Hysteria - File-level eslint-disable for no-explicit-any/prefer-const/ no-case-declarations/no-array-constructor to silence churn without changing behavior * refactor(frontend): port models/dbinbound to TypeScript Phase 6 — final phase of the JS→TS migration. Frontend src/ no longer contains any *.js files. - DBInbound declares all fields explicitly (id, userId, up, down, total, …, nodeId, fallbackParent) with proper types - _expiryTime getter/setter typed against dayjs.Dayjs - coerceInboundJsonField takes unknown, returns any - Private cache fields (_cachedInbound, _clientStatsMap) declared - Consumers (InboundFormModal, InboundsPage, useInbounds): drop ".js" extension from @/models/dbinbound imports * refactor(frontend): drop .js extensions from TS-resolved imports Cleanup after the JS→TS migration: - All consumers that imported @/models/{inbound,outbound,dbinbound}.js now drop the .js extension (TS module resolution lands on the .ts file automatically) - eslint.config.js: remove the **/*.js block since the only remaining JS file under src/ is endpoints.js (build-script consumed only) and js.configs.recommended already covers it correctly * refactor(frontend): tighten inbound.ts cleanup wins Checkpoint before the full any → typed pass: - Wrap 15 case bodies in braces (no-case-declarations) - Convert 14 let → const in genLink helpers (prefer-const) - new Array() → [] for shadowsocks passwords (no-array-constructor) - XrayCommonClass: HeaderEntry, FallbackEntry, JsonObject interfaces; fromJson/toV2Headers/toHeaders typed against them; static methods return JsonObject / HeaderEntry[] instead of any - Reduce file-level eslint-disable scope from 4 rules to just no-explicit-any (the only one still needed) * refactor(frontend): drop eslint-disable from models/dbinbound Replace `any` with explicit domain types: - `coerceInboundJsonField` returns `Record<string, unknown>` (settings/streamSettings/sniffing are always objects). - Add `RawJsonField`, `ClientStats`, `FallbackParentRef`, `DBInboundInit` types. - `_cachedInbound: Inbound | null`, `toInbound(): Inbound`. - `getClientStats(email): ClientStats | undefined`. - `genInboundLinks(): string` (matches actual return from Inbound.genInboundLinks). - Constructor now accepts `DBInboundInit`. * refactor(frontend): drop eslint-disable from InboundsPage Type all callbacks against DBInbound from @/models/dbinbound: - state setters use DBInbound | null - helpers (projectChildThroughMaster, checkFallback, findClientIndex, exportInboundLinks, etc.) take DBInbound - drop `(dbInbounds as any[])` casts; useInbounds already returns DBInbound[] - introduce ClientMatchTarget for findClientIndex's `client` param - tighten DBInbound.clientStats to ClientStats[] (default []) - single boundary cast at <InboundList onRowAction=> to bridge InboundList's narrower DBInboundRecord (cleanup belongs with InboundList) * refactor(frontend): drop file-level eslint-disable from utils/index - ObjectUtil.clone/deepClone become generic <T> - cloneProps/delProps accept `object` (cast internally to AnyRecord) - equals accepts `unknown` with proper narrowing - ColorUtils.usageColor narrows data/threshold to `number`; total widened to `number | { valueOf(): number } | null | undefined` so Dayjs works - Utils.debounce replaces `const self = this` with lexical arrow closure (no-this-alias clean) - InboundList._expiryTime narrowed from `unknown` to `{ valueOf(): number } | null` - Single-line eslint-disable remains on `Msg<T = any>` and HttpUtil generic defaults (idiomatic API envelope; changing default to unknown cascades through 34 consumer files) * refactor(frontend): drop eslint-disable from OutboundFormModal field section Replace `type OB = any` with `type OB = Outbound`. Body code still sees protocol fields as `any` via Outbound's inherited [key: string]: any index signature (CommonClass) — that escape hatch will narrow as Phase 6 tightens outbound.ts itself. The intentional `// eslint-disable-next-line` on `useRef<any>(null)` at line 72 stays — out of scope per plan. * refactor(frontend): drop file-level eslint-disable from InboundFormModal Add minimal local interfaces for protocol-specific shapes the form reads: - StreamLike, TlsCert, VlessClient, ShadowsocksClient, HttpAccount, WireguardPeer (replace with real exports from inbound.ts as Phase 7 exports them). - Props typed as DBInbound | null + DBInbound[]. - Drop unnecessary `(Inbound as any).X`, `(RandomUtil as any).X`, `(Wireguard as any).X`, `(DBInbound as any)(...)` casts — they are already typed classes; only `Inbound.Settings`/`Inbound.HttpSettings` remain `any` via static field on Inbound (will tighten in Phase 7). - inboundRef/dbFormRef retain single-line `// eslint-disable-next-line` for `useRef<any>(null)` — nullable narrowing across ~30 callsites exceeds Phase 5 scope. - payload locals typed Record<string, unknown>; setAdvancedAllValue parses JSON into a narrowed object instead of `let parsed: any`. * refactor(frontend): narrow outbound.ts eslint-disable to no-explicit-any only - Fix all 36 prefer-const violations: convert never-reassigned `let` to `const`; for mixed-mutability destructuring (fromParamLink, fromHysteriaLink) split into separate `const`/`let` declarations by index instead of destructuring. - Fix both no-var violations: `var stream` / `var settings` → `let`. - File still carries `/* eslint-disable @typescript-eslint/no-explicit-any */` because tightening 223 `any` uses requires removing CommonClass's `[key: string]: any` escape hatch and reshaping ~30 dynamically-attached subclass patterns into named classes — multi-hour architectural work tracked as Phase 7's twin for outbound. * refactor(frontend): align sub page chrome with login + AntD defaults - Theme + language buttons now both use AntD `<Button shape="circle" size="large" className="toolbar-btn">` with TranslationOutlined and the SVG theme icon — identical hover/border behaviour. - Language popover content switched from hand-rolled `<ul.lang-list>` to AntD `<Menu mode="vertical" selectable />`; gains native hover/keyboard nav + active highlight. - Drop `.info-table` `!important` border overrides (8 selectors) so Descriptions inherits the AntD theme border colour. - Drop `.qr-code` padding/background/border-radius overrides; only `cursor: pointer` remains (QRCode handles padding/bg itself). - Remove now-unused `.theme-cycle`, `.lang-list`, `.lang-item*`, `.lang-select`, `.settings-popover` rules. * refactor(frontend): drop CustomStatistic wrapper, move overrides to theme tokens - Delete `<CustomStatistic>` (a pass-through wrapper over <Statistic>) and its unscoped global `.ant-statistic-*` CSS overrides; consumers (IndexPage, ClientsPage, InboundsPage, NodesPage) now import AntD `<Statistic>` directly. - Add Statistic component tokens to ConfigProvider so the title (11px) and content (17px) font sizes still apply, without `!important` global selectors. - Move dark / ultra-dark card border colours from `body.dark .ant-card` + `html[data-theme='ultra-dark'] .ant-card` selectors into Card `colorBorderSecondary` tokens; page-cards.css now only carries the custom radius/shadow/transition that has no token equivalent. - Simplify XrayStatusCard badge: remove the custom `xray-pulse` dot keyframe and per-state ring-colour overrides; AntD `<Badge status="processing" color={…}>` already pulses the ring in the same colour, no extra CSS needed. * refactor(frontend): modernize login page with AntD primitives - Theme cycle button switched from `<button.theme-cycle>` + custom CSS to AntD `<Button shape="circle" className="toolbar-btn">` (matches sub page chrome already established). - Theme icons switched from hand-rolled inline SVG (sun, moon, moon+star) to AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the three light / dark / ultra-dark states. - Language popover content switched from `<ul.lang-list>` + `<button.lang-item>` to AntD `<Menu mode="vertical" selectable />` with `selectedKeys=[lang]`; native hover / keyboard nav / active highlight come for free. - Drop CSS for `.theme-cycle`, `.lang-list`, `.lang-item*` (now unused). `.toolbar-btn` retained since it sizes both circular buttons. * refactor(frontend): switch sub page theme icons to AntD primitives Replace the three hand-rolled SVG theme icons (sun, moon, moon+star) with AntD `<SunOutlined />`, `<MoonOutlined />`, `<MoonFilled />` for the light / dark / ultra-dark states. Switch the theme `<Button>` to use the `icon` prop instead of children so it renders the same way as the language button. Drop `.toolbar-btn svg` CSS — no longer needed once the icon comes from AntD. * refactor(frontend): drop !important overrides from pages CSS (Clients + Log modals + Settings tabs) - ClientsPage: pagination size-changer `min-width !important` removed; the 3-level selector specificity already beats AntD's defaults. Scope `body.dark .client-card` to `.clients-page.is-dark .client-card` (avoid leaking into other pages). - LogModal + XrayLogModal: move the mobile full-screen tweaks (`top: 0`, `padding-bottom: 0`, `max-width: 100vw`) from `!important` class rules to the Modal's `style` prop; keep `.ant-modal-content` / `.ant-modal-body` overrides as plain CSS via the className. - SubscriptionFormatsTab: drop `display: block !important` on `.nested-block` — div is already block by default. - TwoFactorModal: drop `padding/background/border-radius !important` on `.qr-code`; AntD QRCode handles those itself. * refactor(frontend): scope dark overrides and switch list borders to AntD CSS variables Scope page-level dark overrides: - inbounds/InboundList: scope `.ant-table` border-radius rules and the mobile @media `.ant-card-*` tweaks to `.inbounds-page` (were global and leaked into other pages); scope `.inbound-card` dark variant to `.inbounds-page.is-dark`. - nodes/NodeList: scope `.node-card` dark to `.nodes-page.is-dark`. - xray/RoutingTab, OutboundsTab: scope `.rule-card`, `.criterion-chip`, `.criterion-more`, `.address-pill` dark to `.xray-page.is-dark`. Modernize list borders to use AntD CSS vars instead of body.dark forks: - index/BackupModal, PanelUpdateModal, VersionModal: replace hard-coded `rgba(5,5,5,0.06)` + `body.dark`/`html[data-theme]` override pairs with `var(--ant-color-border-secondary)`; replace custom text colours with `var(--ant-color-text)` / `var(--ant-color-text-tertiary)`. - xray/DnsPresetsModal: same border-color treatment. - xray/NordModal, WarpModal: collapse `.row-odd` light + `body.dark` pair into a single neutral `rgba(128,128,128,0.06)` that works on both themes; scope under `.nord-data-table` / `.warp-data-table`. * refactor(frontend): switch shared components CSS to AntD CSS variables Replace body.dark / html[data-theme] forks with AntD CSS variables in shared components (work in both light and dark, scale to ultra): - SettingListItem: borders + text colours via `--ant-color-border-secondary`, `--ant-color-text`, `--ant-color-text-tertiary`. - InputAddon: bg/border/text via `--ant-color-fill-tertiary`, `--ant-color-border`, `--ant-color-text`. - JsonEditor: host border/bg via `--ant-color-border`, `--ant-color-bg-container`; focus border via `--ant-color-primary`. - Sparkline (SVG): grid/text colours via `--ant-color-text*` and `--ant-color-border-secondary`; only the tooltip drop-shadow retains a body.dark fork (filter opacity needs explicit value). * refactor(frontend): swap custom Sparkline SVG for Recharts AreaChart Replace the 368-line hand-rolled SVG sparkline (with manual ResizeObserver, gradient/shadow/glow filters, grid + ticks + tooltip, custom Y-axis label thinning) with a thin Recharts `<AreaChart>` wrapper that keeps the same prop API. - Preserved props: data, labels, height, stroke, strokeWidth, maxPoints, showGrid, fillOpacity, showMarker, markerRadius, showAxes, yTickStep, tickCountX, showTooltip, valueMin, valueMax, yFormatter, tooltipFormatter. - Dropped: `vbWidth`, `gridColor`, `paddingLeft/Right/Top/Bottom` — Recharts' ResponsiveContainer handles width, and margins are wired to whether axes are visible. Removed the unused `vbWidth` prop from SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel callsites. - Tooltip, grid, and axis text now use AntD CSS variables for automatic light/dark adaptation; replaced the SVG body.dark forks in Sparkline.css with a single 5-line stylesheet. - Bundle: vendor +~100KB gzip (Recharts + its d3 deps), trade-off for less custom chart code to maintain and a more standard API for future charts (multi-series, brush, etc.). * build(frontend): split Recharts + d3 deps into vendor-recharts chunk Pulls Recharts (~75KB gzip) and its d3-shape/array/color/path/scale + victory-vendor deps out of the catch-all vendor chunk so they load on demand on the three pages that use Sparkline (SystemHistoryModal, XrayMetricsModal, NodeHistoryPanel) and cache independently from the rest of the panel JS. * refactor(frontend): drop body.dark forks in favor of AntD CSS variables - ClientInfoModal/InboundInfoModal: link-panel-text and link-panel-anchor now use var(--ant-color-fill-tertiary) and color-mix on --ant-color-primary, removing the body.dark light/dark background pair. - InboundFormModal: advanced-panel uses --ant-color-border-secondary and --ant-color-fill-quaternary; body.dark/html[data-theme='ultra-dark'] pair gone. - CustomGeoSection: custom-geo-count, custom-geo-ext-code, custom-geo-copyable:hover use --ant-color-fill-tertiary/-secondary; body.dark forks gone. - SystemHistoryModal: cpu-chart-wrap collapsed from three theme-specific gradients into one using color-mix on --ant-color-primary and --ant-color-fill-quaternary. - page-cards.css: body.dark / html[data-theme='ultra-dark'] selectors renamed to page-scoped .is-dark / .is-dark.is-ultra, keeping the same shadow tuning but consistent with the page-scoping convention used elsewhere. * refactor(sidebar): modernize AppSidebar with AntD CSS variables and icons - Replace hardcoded rgba(0,0,0,X) colors with var(--ant-color-text) and var(--ant-color-text-secondary) so light/dark adapt automatically. - Replace rgba(128,128,128,0.15) borders with var(--ant-color-border-secondary) and rgba(128,128,128,0.18) backgrounds with var(--ant-color-fill-tertiary). - Drop all body.dark/html[data-theme='ultra-dark'] color forks for .drawer-brand, .sider-brand, .drawer-close, .sidebar-theme-cycle, .sidebar-donate (CSS variables already adapt). - Drop the body.dark Drawer background !important pair; AntD's colorBgElevated token from the dark algorithm handles it now. - Replace inline sun/moon SVGs in ThemeCycleButton with AntD's SunOutlined/MoonOutlined/MoonFilled to match LoginPage/SubPage. - Convert .sidebar-theme-cycle hover and the menu item selected/hover highlights from hardcoded #4096ff to color-mix on --ant-color-primary, keeping !important on menu rules to beat AntD's CSS-in-JS specificity. * refactor(frontend): swap hardcoded AntD palette colors for CSS variables The dot/badge/pill styles still hardcoded AntD's default palette values (#52c41a, #1677ff, #ff4d4f, #fa8c16, #ff4d4f). Replace each with its semantic --ant-color-* equivalent so they auto-adapt to any theme customization through ConfigProvider. - ClientsPage: .dot-green/.dot-blue/.dot-red/.dot-orange/.dot-gray now use --ant-color-success / -primary / -error / -warning / -text-quaternary. .bulk-count / .client-card / .client-card.is-selected backgrounds use color-mix on --ant-color-primary and --ant-color-fill-quaternary, which also let the body-dark .client-card fork go away. - XrayMetricsModal: .obs-dot is-alive/is-dead and its pulse keyframe now build their box-shadow tint via color-mix on --ant-color-success and --ant-color-error instead of rgba literals. - IndexPage: .action-update warning color uses --ant-color-warning. - OutboundsTab: .outbound-card border, .address-pill background, and .mode-badge tint now use AntD CSS variables; the .xray-page.is-dark .address-pill fork is gone. - InboundFormModal/InboundsPage/ClientBulkAddModal: drop the stale `, #1677ff`/`, #1890ff` fallbacks on var(--ant-color-primary), and switch .danger-icon to --ant-color-error. The teal/cyan brand colors (#008771, #3c89e8, #e04141) used by traffic and pill rows are intentionally kept hardcoded — they are brand-specific shades, not AntD palette colors. * refactor(frontend): swap neutral gray rgba literals for AntD CSS variables Across 12 files the same neutral grays kept reappearing — rgba(128,128,128, 0.06|0.08|0.12|0.15|0.18|0.2|0.25) for borders, dividers, and subtle backgrounds. Each maps cleanly to an AntD CSS variable that already adapts to light/dark and to any theme customization through ConfigProvider: - 0.12–0.18 borders → var(--ant-color-border-secondary) - 0.2–0.25 borders → var(--ant-color-border) - 0.06–0.08 backgrounds → var(--ant-color-fill-tertiary) - 0.02–0.03 card surfaces → var(--ant-color-fill-quaternary) Card surfaces (InboundList .inbound-card, NodeList .node-card) had a light/dark fork pair — the variable covers both, so the .is-dark .card override is gone. RoutingTab .rule-card.drop-before/after used hardcoded #1677ff for the inset focus shadow; replaced with var(--ant-color-primary) so reordering indicators follow the theme primary. ClientsPage bucketBadgeColor returned hex literals (#ff4d4f, #fa8c16, #52c41a, rgba gray) for a Badge color prop. Switched to status="error"| "warning"|"success"|"default" so the dot color now comes from AntD's semantic palette directly. * refactor(xray): collapse RoutingTab dark forks into AntD CSS variables - .criterion-more bg light/dark fork → var(--ant-color-fill-tertiary) - .xray-page.is-dark .rule-card and .criterion-chip overrides removed; the rules already use --bg-card and --ant-color-fill-tertiary that adapt to the theme on their own. * refactor(frontend): inline style hex literals and Alert icon redundancy - FinalMaskForm: five DeleteOutlined icons used rgb(255,77,79) inline; swap for var(--ant-color-error) so they follow theme customization. - NodesPage: CheckCircleOutlined / CloseCircleOutlined statistic prefixes switch to var(--ant-color-success) / -error. - NodeList: ExclamationCircleOutlined warning icons (two callsites) now use var(--ant-color-warning). - BasicsTab: four <Alert type="warning"> blocks shipped a custom ExclamationCircleFilled icon styled to match the warning palette — exactly the icon and color AntD Alert renders for type="warning" by default. Replace the icon prop with showIcon and drop the now-unused ExclamationCircleFilled import. - JsonEditor: focus-within box-shadow tint now uses color-mix on --ant-color-primary instead of an rgba(22,119,255,0.1) literal. * refactor(logs): collapse log-container dark forks to AntD CSS variables LogModal and XrayLogModal each had a body.dark fork that overrode the log container's background, border-color, and text color in addition to the --log-* severity tokens. Background/border/color all map cleanly to var(--ant-color-fill-tertiary) / var(--ant-color-border) / var(--ant-color-text) which already adapt to the theme, so only the severity color tokens remain inside the dark/ultra-dark blocks. * refactor(xray): drop stale --ant-primary-color fallbacks and hex literals - RoutingTab .drop-before/.drop-after box-shadow: #1677ff → var(--ant-color-primary) - OutboundFormModal .random-icon: drop the --ant-primary-color/#1890ff pair (the old AntD v4 token name with stale fallback) for the v6 --ant-color-primary; .danger-icon hex #ff4d4f → var(--ant-color-error). - XrayPage .restart-icon: same drop of the --ant-primary-color fallback. These were all leftovers from the AntD v4 → v6 rename — the v6 --ant-color-primary is already populated by ConfigProvider, so the fallback hex was dead code that would only trigger if AntD wasn't mounted. * refactor(frontend): consolidate margin utility classes into one stylesheet Page CSS files each carried their own copies of the same atomic margin utilities (.mt-4, .mt-8, .mb-12, .ml-8, .my-10, ...). The definitions were identical everywhere they appeared, with each file holding only the subset it happened to need. Move all of them into a single styles/utils.css imported once from main.tsx, and delete the per-page copies from InboundFormModal, CustomGeoSection, PanelUpdateModal, VersionModal, BasicsTab, NordModal, OutboundFormModal, and WarpModal. The classes are available globally on the panel app; login.tsx and subpage.tsx entries do not consume any of them so they stay untouched. * refactor(frontend): consolidate shared page-shell rules into one stylesheet Every panel page CSS file repeated the same wrapper boilerplate — the --bg-page/--bg-card token triples for light/dark/ultra-dark, the min-height + background root rule, the .ant-layout transparent reset, the .content-shell transparent reset, and the .loading-spacer min-height. That's ~30 identical lines duplicated across IndexPage, ClientsPage, InboundsPage, XrayPage, SettingsPage, NodesPage, and ApiDocsPage. Move all of it into styles/page-shell.css and import it once from main.tsx alongside utils.css and page-cards.css. Each page CSS file now only contains genuinely page-specific rules (content-area padding overrides, page-specific tokens like ApiDocs's Swagger --sw-* set). Also drop the per-page `import '@/styles/page-cards.css'` statements from the 7 page tsx files now that main.tsx loads it globally. Net: -211 deleted, +6 inserted in the touched files, plus the new page-shell.css. .zero-margin (Divider override used by Nord/Warp modals) folded into utils.css alongside the margin classes. * refactor(frontend): move default content-area padding to page-shell.css After page-shell.css landed, six of the seven panel pages still kept an identical `.X-page .content-area { padding: 24px }` desktop rule, plus three of them kept an identical `padding: 8px` mobile rule. Hoist both defaults into page-shell.css under a single 6-page selector group and delete the per-page copies. What stays page-specific: - IndexPage keeps its mobile override (padding 12px + padding-top: 64px for the fixed drawer handle clearance). - ApiDocsPage keeps its tighter desktop padding (16px) and its own mobile padding-top: 56px. Settings .ldap-no-inbounds also switches from #999 to var(--ant-color-text-tertiary) for theme adaptation. * refactor(frontend): hoist .header-row, .icons-only, .summary-card to page-shell.css Settings and Xray pages both carried identical .header-row / .header-actions / .header-info rules and an identical six-rule .icons-only block that styles tabbed page navigation. Clients, Inbounds, and Nodes all carried identical .summary-card padding rules with the same mobile reduction. None of these are page-specific. Consolidate: - .header-row family → page-shell scoped to .settings-page, .xray-page - .icons-only family → page-shell global (the class is a deliberate opt-in marker, no scope needed) - .summary-card → page-shell scoped to .clients-page, .inbounds-page, .nodes-page (also fixes InboundsPage's missing scope — its rule was global and would have matched stray .summary-card uses elsewhere) InboundsPage.css and NodesPage.css became empty after the move so the files and their per-page imports are deleted. * refactor(frontend): hoist .random-icon to utils.css Three form modals each carried identical .random-icon styles (small primary-tinted icon next to randomizable inputs): ClientBulkAddModal, InboundFormModal, OutboundFormModal Single definition lives in utils.css now. ClientBulkAddModal.css was just this one rule, so the file and its import are deleted along the way. .danger-icon is left per file — the margin-left differs slightly between InboundFormModal (6px) and OutboundFormModal (8px), so it stays as a page-local rule rather than getting averaged into utils.css. * refactor(frontend): hoist .danger-icon to utils.css and use it everywhere InboundFormModal (margin-left 6px) and OutboundFormModal (margin-left 8px) each carried their own .danger-icon, and FinalMaskForm wrote the same color/cursor/marginLeft trio inline five times. Unify on a single .danger-icon in utils.css with margin-left: 8px — matching the more generous OutboundFormModal value — and: - Drop the per-file .danger-icon copies from InboundFormModal.css and OutboundFormModal.css. - Replace the five inline style props in FinalMaskForm.tsx with className="danger-icon". The visible change is a 2px wider gap to the right of the delete icons on InboundFormModal's protocol/peer dividers.
2026-05-25 12:34:53 +00:00
{(ib.stream.tls.certs || []).map((cert: TlsCert, idx: number) => (
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
<div key={`cert-${idx}`}>
<Form.Item label={t('certificate')}>
<Radio.Group value={cert.useFile} buttonStyle="solid" onChange={(e) => { cert.useFile = e.target.value; refresh(); }}>
<Radio.Button value={true}>{t('pages.inbounds.certificatePath')}</Radio.Button>
<Radio.Button value={false}>{t('pages.inbounds.certificateContent')}</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label=" ">
<Space>
{idx === 0 && (
<Button type="primary" size="small" onClick={() => { ib.stream.tls.addCert(); refresh(); }}>
<PlusOutlined />
</Button>
)}
{ib.stream.tls.certs.length > 1 && (
<Button type="primary" size="small" onClick={() => { ib.stream.tls.removeCert(idx); refresh(); }}>
<MinusOutlined />
</Button>
)}
</Space>
</Form.Item>
{cert.useFile ? (
<>
<Form.Item label={t('pages.inbounds.publicKey')}>
<Input value={cert.certFile} onChange={(e) => { cert.certFile = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.privatekey')}>
<Input value={cert.keyFile} onChange={(e) => { cert.keyFile = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" disabled={!defaultCert && !defaultKey} onClick={() => setDefaultCertData(idx)}>
{t('pages.inbounds.setDefaultCert')}
</Button>
</Form.Item>
</>
) : (
<>
<Form.Item label={t('pages.inbounds.publicKey')}>
<TextArea value={cert.cert} autoSize={{ minRows: 3, maxRows: 8 }}
onChange={(e) => { cert.cert = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.privatekey')}>
<TextArea value={cert.key} autoSize={{ minRows: 3, maxRows: 8 }}
onChange={(e) => { cert.key = e.target.value; refresh(); }} />
</Form.Item>
</>
)}
<Form.Item label="One Time Loading"><Switch checked={!!cert.oneTimeLoading} onChange={(v) => { cert.oneTimeLoading = v; refresh(); }} /></Form.Item>
<Form.Item label="Usage Option">
<Select value={cert.usage} style={{ width: '50%' }} onChange={(v) => { cert.usage = v; refresh(); }}>
{USAGES.map((u) => <Select.Option key={u} value={u}>{u}</Select.Option>)}
</Select>
</Form.Item>
{cert.usage === 'issue' && (
<Form.Item label="Build Chain"><Switch checked={!!cert.buildChain} onChange={(v) => { cert.buildChain = v; refresh(); }} /></Form.Item>
)}
</div>
))}
<Form.Item label="ECH key"><Input value={ib.stream.tls.echServerKeys} onChange={(e) => { ib.stream.tls.echServerKeys = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="ECH config"><Input value={ib.stream.tls.settings.echConfigList} onChange={(e) => { ib.stream.tls.settings.echConfigList = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={getNewEchCert}>Get New ECH Cert</Button>
<Button danger onClick={clearEchCert}>Clear</Button>
</Space>
</Form.Item>
</>
)}
{ib.stream.security === 'reality' && ib.stream.reality && (
<>
<Form.Item label="Show"><Switch checked={!!ib.stream.reality.show} onChange={(v) => { ib.stream.reality.show = v; refresh(); }} /></Form.Item>
<Form.Item label="Xver"><InputNumber value={ib.stream.reality.xver} min={0} onChange={(v) => { ib.stream.reality.xver = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="uTLS">
<Select value={ib.stream.reality.settings.fingerprint} style={{ width: '100%' }} onChange={(v) => { ib.stream.reality.settings.fingerprint = v; refresh(); }}>
{FINGERPRINTS.map((fp) => <Select.Option key={fp} value={fp}>{fp}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label={<>Target <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /></>}>
<Input value={ib.stream.reality.target} onChange={(e) => { ib.stream.reality.target = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={<>SNI <SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /></>}>
<Input value={ib.stream.reality.serverNames} onChange={(e) => { ib.stream.reality.serverNames = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="Max Time Diff (ms)"><InputNumber value={ib.stream.reality.maxTimediff} min={0} onChange={(v) => { ib.stream.reality.maxTimediff = Number(v) || 0; refresh(); }} /></Form.Item>
<Form.Item label="Min Client Ver"><Input value={ib.stream.reality.minClientVer} placeholder="25.9.11" onChange={(e) => { ib.stream.reality.minClientVer = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label="Max Client Ver"><Input value={ib.stream.reality.maxClientVer} placeholder="25.9.11" onChange={(e) => { ib.stream.reality.maxClientVer = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={<>Short IDs <SyncOutlined className="random-icon" onClick={randomizeShortIds} /></>}>
<TextArea value={ib.stream.reality.shortIds} autoSize={{ minRows: 1, maxRows: 4 }} onChange={(e) => { ib.stream.reality.shortIds = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="SpiderX"><Input value={ib.stream.reality.settings.spiderX} onChange={(e) => { ib.stream.reality.settings.spiderX = e.target.value; refresh(); }} /></Form.Item>
<Form.Item label={t('pages.inbounds.publicKey')}>
<TextArea value={ib.stream.reality.settings.publicKey} autoSize={{ minRows: 1, maxRows: 4 }}
onChange={(e) => { ib.stream.reality.settings.publicKey = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.privatekey')}>
<TextArea value={ib.stream.reality.privateKey} autoSize={{ minRows: 1, maxRows: 4 }}
onChange={(e) => { ib.stream.reality.privateKey = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={genRealityKeypair}>Get New Cert</Button>
<Button danger onClick={clearRealityKeypair}>Clear</Button>
</Space>
</Form.Item>
<Form.Item label="mldsa65 Seed">
<TextArea value={ib.stream.reality.mldsa65Seed} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.reality.mldsa65Seed = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label="mldsa65 Verify">
<TextArea value={ib.stream.reality.settings.mldsa65Verify} autoSize={{ minRows: 2, maxRows: 6 }} onChange={(e) => { ib.stream.reality.settings.mldsa65Verify = e.target.value; refresh(); }} />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={genMldsa65}>Get New Seed</Button>
<Button danger onClick={clearMldsa65}>Clear</Button>
</Space>
</Form.Item>
</>
)}
</Form>
);
const renderSniffingTab = () => (
<Form colon={false} labelCol={{ sm: { span: 8 } }} wrapperCol={{ sm: { span: 14 } }}>
<Form.Item label={t('enable')}>
<Switch checked={!!ib.sniffing.enabled} onChange={(v) => { ib.sniffing.enabled = v; refresh(); }} />
</Form.Item>
{ib.sniffing.enabled && (
<>
<Form.Item wrapperCol={{ span: 24 }}>
<Checkbox.Group value={ib.sniffing.destOverride} onChange={(v) => { ib.sniffing.destOverride = v; refresh(); }}>
{Object.entries(SNIFFING_OPTION).map(([key, value]) => (
<Checkbox key={key} value={value}>{key}</Checkbox>
))}
</Checkbox.Group>
</Form.Item>
<Form.Item label={t('pages.inbounds.sniffingMetadataOnly')}>
<Switch checked={!!ib.sniffing.metadataOnly} onChange={(v) => { ib.sniffing.metadataOnly = v; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.sniffingRouteOnly')}>
<Switch checked={!!ib.sniffing.routeOnly} onChange={(v) => { ib.sniffing.routeOnly = v; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.sniffingIpsExcluded')}>
<Select mode="tags" value={ib.sniffing.ipsExcluded} tokenSeparators={[',']}
placeholder="IP/CIDR/geoip:*/ext:*" style={{ width: '100%' }}
onChange={(v) => { ib.sniffing.ipsExcluded = v; refresh(); }} />
</Form.Item>
<Form.Item label={t('pages.inbounds.sniffingDomainsExcluded')}>
<Select mode="tags" value={ib.sniffing.domainsExcluded} tokenSeparators={[',']}
placeholder="domain:*/ext:*" style={{ width: '100%' }}
onChange={(v) => { ib.sniffing.domainsExcluded = v; refresh(); }} />
</Form.Item>
</>
)}
</Form>
);
const renderAdvancedTab = () => {
const advancedTabItems = [
{
key: 'all',
label: t('pages.inbounds.advanced.all'),
children: (
<>
<div className="advanced-editor-meta">{t('pages.inbounds.advanced.allHelp')}</div>
<JsonEditor value={advancedAllValue} onChange={setAdvancedAllValue} minHeight="340px" maxHeight="560px" />
</>
),
},
{
key: 'settings',
label: t('pages.inbounds.advanced.settings'),
children: (
<>
<div className="advanced-editor-meta">
{t('pages.inbounds.advanced.settingsHelp')} <code>{'{ settings: { ... } }'}</code>.
</div>
<JsonEditor value={wrappedConfigValue('settings', 'settings')}
onChange={(v) => setWrappedConfigValue('settings', 'settings', 'Settings', v)}
minHeight="320px" maxHeight="540px" />
</>
),
},
{
key: 'sniffingSection',
label: t('pages.inbounds.advanced.sniffing'),
children: (
<>
<div className="advanced-editor-meta">
{t('pages.inbounds.advanced.sniffingHelp')} <code>{'{ sniffing: { ... } }'}</code>.
</div>
<JsonEditor value={wrappedConfigValue('sniffing', 'sniffing')}
onChange={(v) => setWrappedConfigValue('sniffing', 'sniffing', 'Sniffing', v)}
minHeight="240px" maxHeight="420px" />
</>
),
},
];
if (canEnableStream) {
advancedTabItems.push({
key: 'streamSection',
label: t('pages.inbounds.advanced.stream'),
children: (
<>
<div className="advanced-editor-meta">
{t('pages.inbounds.advanced.streamHelp')} <code>{'{ streamSettings: { ... } }'}</code>.
</div>
<JsonEditor value={wrappedConfigValue('streamSettings', 'stream')}
onChange={(v) => setWrappedConfigValue('streamSettings', 'stream', 'Stream', v)}
minHeight="320px" maxHeight="540px" />
</>
),
});
}
return (
<div className="advanced-shell">
<div className="advanced-panel">
<div className="advanced-panel__header">
<div>
<div className="advanced-panel__title">{t('pages.inbounds.advanced.title')}</div>
<div className="advanced-panel__subtitle">{t('pages.inbounds.advanced.subtitle')}</div>
</div>
</div>
<Tabs activeKey={advancedSectionKey} onChange={setAdvancedSectionKey} items={advancedTabItems} className="advanced-inner-tabs" />
</div>
</div>
);
};
const tabItems = [
{ key: 'basic', label: t('pages.xray.basicTemplate'), children: renderBasicsTab() },
];
if (hasProtocolTabContent) {
tabItems.push({ key: 'protocol', label: t('pages.inbounds.protocol'), children: renderProtocolTab() });
}
if (canEnableStream) {
tabItems.push({ key: 'stream', label: t('pages.inbounds.streamTab'), children: renderStreamTab() });
tabItems.push({ key: 'security', label: t('pages.inbounds.securityTab'), children: renderSecurityTab() });
}
tabItems.push({ key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: renderSniffingTab() });
tabItems.push({ key: 'advanced', label: t('pages.xray.advancedTemplate'), children: renderAdvancedTab() });
return (
<>
{messageContextHolder}
<Modal
open={open}
title={title}
okText={okText}
cancelText={t('close')}
confirmLoading={saving}
mask={{ closable: false }}
width={780}
onOk={submit}
onCancel={onClose}
destroyOnHidden
>
<Tabs activeKey={activeTabKey} onChange={handleTabChange} items={tabItems} />
</Modal>
</>
);
}