mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
60 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0208396802
|
feat(frontend): migrate DNS + Routing to Zod, align with xray docs
Adds first-class Zod schemas for the xray-core DNS block and routing sub-objects (Balancer, Rule) matching the documented shape at https://xtls.github.io/config/dns.html and https://xtls.github.io/config/routing.html, then wires the DnsServerModal and BalancerFormModal up to those schemas. schemas/dns.ts (new): - DnsQueryStrategySchema enum (UseIP/UseIPv4/UseIPv6/UseSystem) - DnsHostsSchema record(string -> string | string[]) - DnsServerObjectInnerSchema + DnsServerObjectSchema (with preprocess to migrate legacy `expectIPs` -> `expectedIPs` alias) - DnsServerEntrySchema = string | DnsServerObject (xray accepts both) - DnsObjectSchema with all documented fields and defaults schemas/routing.ts (new): - RuleProtocolSchema enum (http/tls/quic/bittorrent) - RuleWebhookSchema (url/deduplication/headers) - RuleObjectSchema covering every documented field (domain/ip/port/ sourcePort/localPort/network/sourceIP/localIP/user/vlessRoute/ inboundTag/protocol/attrs/process/outboundTag/balancerTag/ruleTag/ webhook) with type=literal('field').default('field') - BalancerStrategyTypeSchema enum (random/roundRobin/leastPing/leastLoad) - BalancerCostObjectSchema {regexp,match,value} - BalancerStrategySettingsSchema (expected/maxRTT/tolerance/baselines/costs) - BalancerStrategySchema + BalancerObjectSchema schemas/xray.ts: - routing.rules: was loose 3-field object, now z.array(RuleObjectSchema) - routing.balancers: was z.array(z.unknown()), now z.array(BalancerObjectSchema) - dns: was 2-field loose, now full DnsObjectSchema - BalancerFormSchema: strategy now BalancerStrategyTypeSchema (enum) instead of z.string(); fallbackTag defaults to ''; settings? added for leastLoad DnsServerModal (full Pattern A rewrite): - useState/DnsForm interface -> Form.useForm<DnsServerForm>() - manual domain/expectedIP/unexpectedIP list -> Form.List - antdRule on address/port/timeoutMs for inline validation - preserves legacy collapse-to-bare-string behavior on submit BalancerFormModal: - Adds conditional leastLoad sub-form (Expected/MaxRTT/Tolerance/ Baselines/Costs) wired to BalancerStrategySettingsSchema - Strategy options derived from schema enum - Cost rows with regexp/literal switch + match + value - required prop on Tag and Selector for red asterisk visual BalancersTab: - BalancerRecord interface -> type alias to BalancerObject - onConfirm now propagates strategy.settings to wire when leastLoad - Removes useMemo wrapping `columns` array. The memo had deps [t, isMobile] (with an eslint-disable) so the column render functions kept their original closure over `openEdit`. Once a balancer was created and the user clicked the edit button, the stale openEdit fired with empty `rows`, so rows[idx] was undefined and the modal opened blank. Columns are cheap to rebuild each render, so dropping the memo is the right fix. DnsTab + RoutingTab: switch ad-hoc interfaces to schema-derived types. translations (en-US, fa-IR): add the previously-missing pages.xray.balancerTagRequired and pages.xray.balancerSelectorRequired keys so antdRule surfaces a real message instead of the raw i18n key. |
||
|
|
0442be5078
|
feat(frontend): align finalmask + sockopt with xray docs, add golden fixtures
Schema fixes per https://xtls.github.io/config/transports/finalmask.html and https://xtls.github.io/config/transports/sockopt.html: finalmask: - QuicCongestionSchema: remove non-doc 'cubic', keep reno/bbr/brutal/force-brutal - Add BbrProfileSchema (conservative/standard/aggressive) and bbrProfile field - brutalUp/brutalDown: number -> string per docs (units like '60 mbps') - Tighten ranges: maxIdleTimeout 4-120, keepAlivePeriod 2-60, maxIncomingStreams min 8 - UdpMaskTypeSchema: add missing 'sudoku' - udpHop.interval stays as preprocessed string-range per intentional B19 divergence sockopt: - tcpFastOpen: boolean -> union(boolean, number) per docs (number tunes queue size) - mark: drop min(0) (can be any int) - domainStrategy default: 'UseIP' -> 'AsIs' per docs - tcpKeepAlive Interval/Idle defaults: 0/300 -> 45/45 per docs (outbound) - Add AddressPortStrategySchema enum (7 values) + addressPortStrategy field - Add HappyEyeballsSchema (tryDelayMs/prioritizeIPv6/interleave/maxConcurrentTry) - Add CustomSockoptSchema (system/type/level/opt/value) + customSockopt array Bug fixes: - options.ts: Address_Port_Strategy values were lowercase ('srvportonly'); xray-core requires camelCase ('SrvPortOnly'). Fixed all 6 entries. - OutboundFormModal: domainStrategy Select was mistakenly populated from ADDRESS_PORT_STRATEGY_OPTIONS; now uses DOMAIN_STRATEGY_OPTION. - OutboundFormModal: inline sockopt defaults (hardcoded {acceptProxyProtocol: false, domainStrategy: 'UseIP', ...}) replaced with SockoptStreamSettingsSchema.parse({}) so schema is the single source. Form additions (both InboundFormModal + OutboundFormModal): - Address+port strategy Select - Happy Eyeballs Switch + sub-form (tryDelayMs/prioritizeIPv6/interleave/maxConcurrentTry) - Custom sockopt Form.List (system/type/level/opt/value) - FinalMaskForm: BBR Profile Select (visible when congestion='bbr'), Brutal Up/Down placeholders updated to string format Golden fixtures (8 new + 4 xhttp extras): - finalmask/{tcp-mask, udp-mask, quic-params, combined}.json — cover all TCP mask types, 7 UDP mask types including new sudoku, full QUIC params shape - sockopt/{defaults, tcp-tuning, tproxy, full}.json — full sockopt knobs - stream/xhttp-{basic, extra-padding, extra-placement, extra-tuning}.json — cover the extra-blob fields bundled into share-link extra=<json> Tests now at 312 (up from 300); typecheck/lint clean. |
||
|
|
3fdd9765a7
|
fix(frontend): xhttp form binding + drop empty strings from JSON (B23)
uplinkHTTPMethod was wrapped Form.Item -> Form.Item(shouldUpdate) -> Select, which broke AntD's value/onChange injection (AntD only clones the immediate child). Restructured so shouldUpdate is the outer wrapper and Form.Item(name) directly wraps the Select. Also drop empty-string fields from xhttpSettings in the wire payload — fields like uplinkHTTPMethod, sessionPlacement, seqPlacement, xPaddingKey default to '' meaning "use server default", so they shouldn't appear in JSON as "field": "". Adds placeholder text to the 3 xhttp Selects so the form reflects the current value after JSON paste. |
||
|
|
bb20cf506b
|
fix(frontend): blur active element on every tab switch path (B21 follow-up)
The previous B21 patch only blurred on user-initiated tab clicks via
onTabChange. Two other paths still set activeKey while a JSON-tab
input retained focus:
- importLink: after a successful share-link parse, setActiveKey('1')
switched to the form tab while the user's focus was still on the
Input.Search they just pressed Enter in. Chrome logged the same
"Blocked aria-hidden" warning because the panel they were leaving
became aria-hidden synchronously, with their input still focused.
- onTabChange entering the JSON tab: also did a bare setActiveKey
with no blur, so going from a focused form input INTO the JSON
tab could trip the warning in reverse.
Fix: centralized switchTab(key) that blurs document.activeElement
sync before calling setActiveKey. Every internal tab transition
(importLink, onTabChange both directions) now routes through it.
The single setActiveKey('1') in the open-modal useEffect is left as
a plain setter because there's no focused input at modal-open time.
|
||
|
|
d2f5f530e0
|
fix(frontend): Outbound submit crash on non-mux protocols + tab a11y (B21)
Two issues surfaced on Outbound save: 1. Crash: `Cannot read properties of undefined (reading 'enabled')` at formValuesToWirePayload. The modal hides the Mux switch entirely for non-stream protocols (dns/freedom/blackhole/loopback) and for stream protocols when isMuxAllowed gates it out (xhttp, vless+flow). With the field never registered, validateFields() returns no `mux` key — `values.mux.enabled` then dereferences undefined. Fix: optional chain `values.mux?.enabled` so missing mux skips the mux clause silently. Documented why mux can be absent. 2. Chrome a11y warning: "Blocked aria-hidden on an element because its descendant retained focus" — when the user has an input focused inside one Tab panel and switches to another tab, AntD marks the outgoing panel aria-hidden while focus is still inside. The browser warns, but the focused control is now invisible to AT users. Fix: blur the active element before setActiveKey in onTabChange. |
||
|
|
f92f07e8f2
|
refactor(frontend): retire class-based xray models (Step 5)
Delete models/inbound.ts (3,359 lines) and outbound.ts (2,405).
The Inbound/Outbound classes and ~50 sub-classes are replaced by
Zod-typed data + pure functions in lib/xray/*.
Consumer migration off dbInbound.toInbound():
- useInbounds: isSSMultiUser({protocol, settings}) directly
- QrCodeModal: genWireguardConfigs/Links/AllLinks from lib/xray
- InboundList: derives tags from streamSettings raw fields
- InboundsPage: clone via raw JSON, fallback projection via
schema-shape stream object, exports via genInboundLinks
- InboundInfoModal: builds an InboundInfo facade locally from
raw streamSettings (host/path/serverName/serviceName per
network), canEnableTlsFlow + isSS2022 from lib/xray
New helper: lib/xray/inbound-from-db.ts exposes
inboundFromDb(raw) converting a raw DBInbound row into a
schema-typed Inbound for the link-generation orchestrators.
DBInbound trimmed: drops toInbound, isMultiUser, hasLink,
genInboundLinks, _cachedInbound. Imports Protocols from
@/schemas/primitives now that ./inbound is gone.
Bundled Phase 2 fixes:
- Outbound modal: Form.useWatch with preserve: true so the
stream block doesn't gate itself out when network is unmounted
- Inbound form adapter: pruneEmpty preserves empty objects;
per-protocol client field projection via Zod safeParse;
sniffing collapse to {enabled:false}
- useClients invalidateAll also invalidates inbounds.root()
- IndexPage Config modal top/maxHeight polish
Tests: 283/283 pass. typecheck/lint clean.
|
||
|
|
5a90f7e348
|
refactor(frontend): align hysteria with new docs + drop hysteria2 protocol
Phase 2 smoke fixes on the Inbound add flow surfaced that hysteria2 was
modeled as a separate top-level protocol when it's really just hysteria
v2. The xray transports/hysteria.html docs also pin the hysteria stream
to a minimal shape (version/auth/udpIdleTimeout/masquerade) — the
previous schema carried legacy congestion/up/down/udphop/window knobs
that aren't part of the wire contract.
Hysteria2 removal:
- Drop 'hysteria2' from ProtocolSchema enum and Protocols const
- Drop hysteria2 branches from inbound/outbound discriminated unions
- Drop createDefaultHysteria2InboundSettings / OutboundSettings
- Delete schemas/protocols/inbound/hysteria2.ts and outbound/hysteria2.ts
- Drop hysteria2 case in getInboundClients / genLink (fell through to
the hysteria handler anyway)
- Update client form modals' MULTI_CLIENT_PROTOCOLS sets
- Remove hysteria2-basic fixture + snapshot entries (14 capability
cases, 1 protocols fixture, 1 inbound-defaults factory)
- Keep parseHysteria2Link() outbound parser since hysteria2:// is the
share-link URI prefix for hysteria v2
Hysteria stream alignment with xtls docs:
- HysteriaStreamSettingsSchema reduced to version/auth/udpIdleTimeout/
masquerade per transports/hysteria.html
- Masquerade type adds '' (default 404 page) and defaults to it
- Outbound form drops Congestion/Upload/Download/UDP hop/Max idle/
Keep alive/Disable Path MTU controls and the receive-window note
- newStreamSlice('hysteria') in OutboundFormModal mirrors the trimmed
shape; outbound-link-parser emits the trimmed shape too
- InboundFormModal Masquerade Select gains the default option
New TUN inbound schema:
- Add schemas/protocols/inbound/tun.ts with name/mtu/gateway/dns/
userLevel/autoSystemRoutingTable/autoOutboundsInterface
- Wire into ProtocolSchema enum, InboundSettingsSchema discriminated
union, createDefaultInboundSettings dispatcher
Other Phase 2 smoke fixes folded in:
- Tunnel portMap UI swaps Form.List for HeaderMapEditor v1 — wire
shape is Record<string,string> and the List was producing arrays
- Hysteria onValuesChange seeds full TLS schema defaults + one
empty certificate row (Cipher Suites/Min/Max Version/uTLS/ALPN
were undefined before)
- HTTP/Mixed accounts Add button auto-fills user/pass with
RandomUtil.randomLowerAndNum
- Hysteria security tab gates the 'none' radio out — TLS only
- Hysteria stream tab drops the inbound Auth password field (xray
inbound auth is per-user via 'users', not stream-level)
- Reality onSecurityChange auto-randomizes target/serverNames/
shortIds and fetches an X25519 keypair
- Tag and DB-side fields (up/down/total/expiryTime/
lastTrafficResetTime/clientStats/security) gain hidden Form.Items
so validateFields keeps them in the wire payload (rc-component
form strips unregistered fields)
- WireGuard inbound auto-seeds one peer with generated keypair,
allowedIPs ['10.0.0.2/32'], keepAlive 0 — matches legacy
- WireGuard peer rows separated by Divider with the Peer N title
and a small inline remove button (titlePlacement="center")
|
||
|
|
e978428ca3
|
feat(frontend): FinalMaskForm rewrite to Pattern A + wire into both modals
Rewrite FinalMaskForm.tsx from a class-coupled component (mutated stream.finalmask.tcp[] via .addTcpMask/.delTcpMask methods, notified parent via onChange callback) into a Pattern A sub-form: takes a NamePath base, a FormInstance, and the surrounding network/protocol, then composes Form.List + Form.Item at absolute paths under that base. All array structures use nested Form.List — tcp/udp mask arrays, the clients/servers groups in header-custom (Form.List of Form.List of ItemEditor), and the noise list. Type Selects use onChange to reset the settings sub-object via form.setFieldValue, mirroring the legacy changeMaskType behavior. The kcp.mtu side effect on xdns type change is preserved. Wired into both InboundFormModal and OutboundFormModal stream tabs, placed after the sockopt section. The component is the first Pattern A consumer of nested Form.List inside another Form.List, so it stands as the reference for future nested-array sub-forms. |
||
|
|
9f84859ff6
|
feat(frontend): outbound TCP HTTP camouflage parity with inbound
Add method/version inputs, request header map, and full response sub-section (version/status/reason/headers) to OutboundFormModal so the outbound side can configure the same HTTP-1.1 obfuscation knobs the inbound side already exposed. |
||
|
|
a7166988ca
|
feat(frontend): complete outbound sockopt section with remaining knobs
Add the four remaining SockoptStreamSettings fields that were edit-via-JSON-only after the initial outbound modal rewrite: - TCP keep-alive idle (s) — tcpKeepAliveIdle, time before sending the first probe on an idle TCP connection. - TCP max segment — tcpMaxSeg, override the default MSS. - TCP window clamp — tcpWindowClamp, cap the TCP receive window. - Trusted X-Forwarded-For — trustedXForwardedFor, list of trusted proxy hostnames/CIDRs whose XFF headers Xray will honor. The outbound sockopt section now exposes all 17 SockoptStreamSettings fields from the schema. The InboundFormModal's sockopt section has its own field list (closer to the legacy class) and is unchanged. |
||
|
|
9de527b35f
|
feat(frontend): link import on outbound modal (vmess/vless/trojan/ss/hy2)
The legacy outbound modal could import a vmess://, vless://, trojan://, ss://, or hysteria2:// share link via a Convert button on the JSON tab. Restore that UX with a focused pure-function parser. lib/xray/outbound-link-parser.ts: - parseVmessLink: base64 JSON, maps net/tls + per-network params onto the discriminated stream branch. - parseVlessLink: standard URL with type/security/sni/pbk/sid/fp/flow query params, dispatches transport via buildStream + applies security params via applySecurityParams. - parseTrojanLink: same URL pattern, defaults security to tls. - parseShadowsocksLink: both modern (base64 userinfo@host:port) and legacy (base64 of whole thing) ss:// formats. - parseHysteria2Link: accepts both hysteria2:// and hy2:// schemes, uses the hysteria stream branch with version=2 + TLS h3. - parseOutboundLink dispatcher returns the first non-null parser result, or null when no scheme matches. test/outbound-link-parser.test.ts: - 13 cases covering happy paths for each protocol family plus malformed input, ss:// dual-format handling, hy2:// alias. OutboundFormModal.tsx: - Import button on the JSON tab Input.Search; on success, parsed payload flows through rawOutboundToFormValues, the form is reset, and we switch back to the Basic tab. - Tag is preserved when the parsed link does not carry one. Out of scope: advanced fields the legacy parser handled (xmux, padding obfs, reality short IDs, finalmask from fm= param). Power users can finish the import in the form after the basics land. |
||
|
|
e01acae843
|
feat(frontend): XHTTP advanced fields on outbound modal
Replace the 'edit via JSON' deferred-features hint with the full XHTTP sub-form matching the legacy modal's XhttpFields helper. schemas/protocols/stream/xhttp.ts: - New XHttpXmuxSchema: 6 connection-multiplexing knobs (maxConcurrency, maxConnections, cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, hKeepAlivePeriod). - XHttpStreamSettingsSchema gains 5 outbound-only fields and one UI-only toggle: scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader, xmux, enableXmux. outbound-form-adapter.ts: - New stripUiOnlyStreamFields() drops xhttpSettings.enableXmux on the way to wire so the panel never embeds the UI toggle into the saved config. xray-core ignores unknown fields anyway, but the panel reads back its own emitted JSON, so a clean wire shape matters. OutboundFormModal.tsx: - Headers editor (HeaderMapEditor v1) for xhttpSettings.headers. - Padding obfs Switch + 4 conditional fields (key/header/placement/ method) when on. - Uplink HTTP method Select with GET disabled outside packet-up. - Session placement + session key (key shown when placement != path). - Sequence placement + sequence key (same pattern). - packet-up mode: scMinPostsIntervalMs, scMaxEachPostBytes, uplink data placement + key + chunk size (key/chunk-size shown when placement != body). - stream-up / stream-one mode: noGRPCHeader Switch. - XMUX Switch + 6 nested fields when on. |
||
|
|
19204f9e04
|
feat(frontend): Hysteria stream sub-form (schema branch + outbound UI)
Add the 7th branch to NetworkSettingsSchema for Hysteria transport.
schemas/protocols/stream/hysteria.ts:
- HysteriaStreamSettingsSchema covers the full wire shape: version=2,
auth, congestion (''|'brutal'), up/down bandwidth strings, optional
udphop sub-object for port-hopping, receive-window tuning fields,
maxIdleTimeout, keepAlivePeriod, disablePathMTUDiscovery.
schemas/protocols/stream/index.ts:
- NetworkSchema gains 'hysteria'.
- NetworkSettingsSchema gains the 7th branch
{ network: 'hysteria', hysteriaSettings: HysteriaStreamSettingsSchema }.
OutboundFormModal.tsx:
- NETWORK_OPTIONS keeps the 6 standard transports for non-hysteria
protocols; when protocol === 'hysteria', a 7th option is appended
(matches the legacy [...NETWORKS, 'hysteria'] gate).
- newStreamSlice handles the 'hysteria' case with sensible defaults
matching the legacy HysteriaStreamSettings constructor.
- New sub-form when network === 'hysteria': 8 common fields (auth,
congestion, up, down, udphop Switch + 3 nested fields when on,
maxIdleTimeout, keepAlivePeriod, disablePathMTUDiscovery).
- Receive-window tuning fields are still edit-via-JSON (rarely
touched + would clutter the form).
|
||
|
|
7442486a58
|
feat(frontend): HeaderMapEditor reusable component + wire WS/HTTPUpgrade headers
Add a single reusable header-map editor that handles the two wire
shapes Xray uses:
- v1: { name: 'value' } — used by WS / HTTPUpgrade / Hysteria
masquerade. One value per name.
- v2: { name: ['value1', 'value2'] } — used by TCP HTTP camouflage.
Each header can repeat (RFC 7230 §3.2.2).
Internal state is always a flat list of {name, value} rows regardless
of mode; conversion to/from the wire shape happens at the value /
onChange boundary so consumers bind straight to a Form.Item with no
extra transforms.
Wired into:
- InboundFormModal: WS Headers, HTTPUpgrade Headers
- OutboundFormModal: WS Headers, HTTPUpgrade Headers
XHTTP headers are already in a list-of-rows wire shape (different
from these two), so they keep their bespoke editor. Hysteria
masquerade is still deferred until the Hysteria stream sub-form
lands.
|
||
|
|
e62ad84bb7
|
feat(frontend): symmetric TCP HTTP host/path + extra sockopt knobs
OutboundFormModal: - Sockopt section gains 5 common-but-rarely-tweaked knobs: acceptProxyProtocol, tproxy (off/redirect/tproxy), tcpcongestion (bbr/cubic/reno), V6Only, tcpUserTimeout. The remaining sockopt fields (tcpKeepAliveIdle, tcpMaxSeg, tcpWindowClamp, trustedXForwardedFor) are still edit-via-JSON; they are deeply tunable and not commonly touched. InboundFormModal: - TCP HTTP camouflage gains host + path inputs symmetric to the outbound side. Switch ON seeds request with sensible defaults (version 1.1, method GET, path ['/'], empty headers). The two inputs use the same normalize/getValueProps comma-string ↔ string[] dance the outbound side uses, so the wire shape stays identical to what xray-core expects. |
||
|
|
ad3d3937b0
|
feat(frontend): OutboundFormModal deferred features (Vision seed / TCP host+path / WG pubKey derive)
Three small wins from the post-atomic-swap deferred list: - VLESS Vision testpre + testseed: shown only when flow === 'xtls-rprx-vision' (mirrors the legacy canEnableVisionSeed gate). testseed binds to a Select mode='tags' with a normalize() that coerces strings to positive integers and drops invalid entries. - TCP HTTP camouflage host + path: when the TCP HTTP camouflage Switch is on, surface two inputs that read/write directly into streamSettings.tcpSettings.header.request.headers.Host and .path. Both fields are string[] on the wire; normalize + getValueProps translate to/from comma-joined strings in the UI (one entry per host or path the user wants camouflaged). - Wireguard pubKey auto-derive: Form.useWatch on settings.secretKey + useEffect that runs Wireguard.generateKeypair(secret).publicKey on every change and writes the result into the disabled pubKey display field. Matches the legacy modal's per-keystroke derive. |
||
|
|
eac50b4e80
|
feat(frontend): atomic swap OutboundFormModal to Pattern A
Delete the legacy 1473-line class-based OutboundFormModal.tsx and replace
it with the new Pattern A modal (Form.useForm + antdRule + per-protocol
discriminated-union form values + wire adapter).
Net diff: legacy file gone, function renamed from OutboundFormModalNew
to OutboundFormModal so the existing OutboundsTab import resolves
unchanged.
What is migrated:
- All 12 protocols (vmess/vless/trojan/ss/socks/http/wireguard/
hysteria/freedom/blackhole/dns/loopback)
- Stream tab with TCP/KCP/WS/gRPC/HTTPUpgrade + partial XHTTP
- Security tab with TLS + Reality + Flow gating
- Sockopt + Mux sections (gated by isMuxAllowed)
- JSON tab with bidirectional bridge to form state
- Tag uniqueness check
- VLESS reverse-sniffing slice
- Freedom fragment/noises/finalRules
- DNS rewrite + rules list
- Wireguard peers + nested allowedIPs sub-list
- Wireguard secret/public key regeneration
Deferred to follow-up commits (still accessible via the JSON tab):
- XHTTP advanced fields (xmux, sequence/session placement, padding obfs)
- Hysteria stream transport sub-form
- TCP HTTP camouflage host/path body
- WS/HTTPUpgrade/XHTTP headers map editor
- Remaining sockopt knobs (tcpUserTimeout, tcpcongestion, tcpKeepAliveIdle,
tcpMaxSeg, tcpWindowClamp, V6Only, trustedXForwardedFor, tproxy,
acceptProxyProtocol)
- VLESS Vision testpre/testseed
- Reality API helpers (random target, x25519/mldsa65 generate-import)
- Link import (vmess:// vless:// etc → outbound)
- FinalMaskForm hookup (deferred from inbound rewrite too)
|
||
|
|
7765fb39fe
|
feat(frontend): OutboundFormModal.new.tsx sockopt + mux sections
- Sockopts: Switch toggles streamSettings.sockopt between undefined and a populated default object (17 fields with sane bbr/UseIP defaults). Only the 8 most-used fields are rendered (dialer proxy, domain strategy, keep alive interval, TFO, MPTCP, penetrate, mark, interface). The remaining sockopt knobs (acceptProxyProtocol, tcpUserTimeout, tcpcongestion, tcpKeepAliveIdle, tcpMaxSeg, tcpWindowClamp, V6Only, trustedXForwardedFor, tproxy) are still in the wire payload — edit them via the JSON tab. - Mux: gated by isMuxAllowed(protocol, flow, network) — VMess/VLESS/ Trojan/SS/HTTP/SOCKS, no flow set, no xhttp transport. Sub-fields (concurrency / xudpConcurrency / xudpProxyUDP443) only render when enabled is true. - Sockopt section visible only when streamAllowed AND network is set — non-stream protocols (freedom/blackhole/dns/loopback) still edit sockopt via the JSON tab. |
||
|
|
bfc9c12c05
|
feat(frontend): OutboundFormModal.new.tsx security tab (TLS + Reality + Flow)
- onSecurityChange cascade: swaps tlsSettings/realitySettings sub-key matching the DU branch, seeding the new sub-form with empty/default fields so the UI does not reference undefined values. - Flow Select rendered when canEnableTlsFlow is true (VLESS + TCP + TLS/Reality). Moved from the basic VLESS section so it only appears in the relevant security context — matches the legacy modal UX. - Security Radio (none / TLS / Reality) gated by canEnableTls and canEnableReality pure-function predicates from lib/xray/protocol-capabilities. - TLS sub-form: 6 outbound-specific fields (SNI/uTLS/ALPN/ECH/ verifyPeerCertByName/pinnedPeerCertSha256) matching the legacy TlsStreamSettings flat shape (no certificates list — outbound is client-side). - Reality sub-form: 6 fields (SNI/uTLS/shortId/spiderX/publicKey/ mldsa65Verify). publicKey + mldsa65Verify get TextAreas to handle the long base64 strings. |
||
|
|
8e9c82f56b
|
feat(frontend): OutboundFormModal.new.tsx stream tab (TCP/KCP/WS/gRPC/HTTPUpgrade)
Wire the stream sub-form into the Pattern A modal:
- newStreamSlice(network) helper bootstraps the per-network DU branch
with Xray defaults (mtu=1350, tti=20, uplinkCapacity=5, etc.).
- streamSettings is seeded once when the protocol supports streams
but the form has no slice yet (new outbound + protocol switch).
- onNetworkChange swaps the sub-key and preserves security when the
new network still supports it, else snaps back to 'none'.
- Per-network sub-forms wired:
TCP: HTTP camouflage Switch (sets header.type = 'http' / 'none')
KCP: 6 numeric tuning fields
WS: host + path + heartbeat
gRPC: service name + authority + multi-mode switch
HTTPUpgrade: host + path
XHTTP: host + path + mode + padding bytes (advanced fields via JSON)
Security radio, TLS/Reality sub-forms, sockopt, and mux still pending.
|
||
|
|
e8721a207c
|
feat(frontend): OutboundFormModal.new.tsx DNS + Freedom + VLESS reverse-sniffing
- DNS: rewriteNetwork (udp/tcp Select) + rewriteAddress + rewritePort + userLevel + rules Form.List (action/qtype/domain). - Freedom: domainStrategy + redirect + Fragment Switch with conditional 4-field sub-block (legacy 'enable Fragment' UX preserved — Switch sets all four fields to populated defaults, off-state empties them all out so the adapter strips them on submit) + Noises Form.List (rand/base64/ str/hex types, packet/delay/applyTo per row) + Final Rules Form.List with conditional block-delay sub-field. - VLESS reverse-sniffing slice: rendered only when reverseTag is set (matches the legacy modal's nested conditional). All six fields wired to the form state with appropriate widgets (Switch / Select multi / Select tags). |
||
|
|
b6d996d1b1
|
feat(frontend): OutboundFormModal.new.tsx socks/http/hysteria/loopback/blackhole/wireguard sections
- SOCKS / HTTP: user + pass at settings root. - Hysteria: read-only version=2 (the actual transport knobs live on stream.hysteria, added with the stream tab). - Loopback: inboundTag. - Blackhole: response type Select with empty/none/http options. - Wireguard: address (csv) + secretKey (with regenerate icon) + derived pubKey + domain strategy + MTU + workers + no-kernel-tun + reserved (csv) + peers Form.List with nested allowedIPs sub-list. Wireguard regenerate icon uses Wireguard.generateKeypair() and writes both keys to the form via setFieldValue — preserves the legacy UX of the SyncOutlined inline-icon next to the privateKey label. |
||
|
|
a3857cff6a
|
feat(frontend): OutboundFormModal.new.tsx vmess/vless/trojan/ss sections
- Shared connect-target sub-block (address + port) for the six protocols whose form schema carries them flat at settings root. - VMess: id + security Select (USERS_SECURITY). - VLESS: id + encryption + flow + reverseTag (reverse-sniffing slice and Vision testpre/testseed come in a later commit). - Trojan: password. - Shadowsocks: password + method Select (SSMethodSchema) + UoT switch + UoT version. onValuesChange cascade: when the user picks a different protocol, the adapter re-seeds the settings sub-object to the new protocol's defaults so leftover fields from the previous protocol do not bleed through. |
||
|
|
e64d1a9bef
|
feat(frontend): OutboundFormModal.new.tsx skeleton (Pattern A)
Sibling .new.tsx file with the Modal shell, Tabs (Basic/JSON), Form.useForm hydration via rawOutboundToFormValues, and the submit pipeline that calls formValuesToWirePayload before onConfirm. Tag uniqueness check is wired in. Protocol-specific sub-forms, stream, security, sockopt, and mux sections are deferred to subsequent commits — accessible via the JSON tab in the meantime. The InboundsPage continues to render the legacy modal until the atomic swap at the end. Also: rawOutboundToFormValues now returns streamSettings as undefined when the wire payload omits it, so Form.useForm doesn't receive a value that does not match the NetworkSettings discriminated union. |
||
|
|
2d74dbe7ad
|
refactor(frontend): lift outbound option dictionaries to schemas/primitives
Adds schemas/primitives/options.ts with UTLS_FINGERPRINT, ALPN_OPTION, SNIFFING_OPTION, USERS_SECURITY, MODE_OPTION (all identical between models/inbound.ts and models/outbound.ts) plus the outbound-only WireguardDomainStrategy, Address_Port_Strategy, and DNSRuleActions. OutboundFormModal now pulls 9 consts from primitives. Only `Outbound` (the class) and `SSMethods` (whose inbound/outbound versions diverge by 2 legacy aliases — keep the picker open for the Pattern A rewrite) still come from @/models/outbound. Drops three stale `as string[]` casts on what are now readonly tuples. |
||
|
|
40ca58d42e
|
refactor(frontend): lift OutboundProtocols + OutboundDomainStrategies to schemas/primitives
Moves the two outbound-side consts out of models/outbound.ts and into schemas/primitives/outbound-protocol.ts. Renames the export to OutboundProtocols to disambiguate from the inbound Protocols const (different key casing — PascalCase vs ALL CAPS — and partly different member set, so they cannot share a single const). OutboundsTab.tsx keeps its 15+ Protocols.X call sites by aliasing the import. FinalMaskForm.tsx and BasicsTab.tsx swap directly. Drops a stale `as string[]` cast in BasicsTab that no longer fits the new readonly-tuple typing. After this commit only the two big form modals (InboundFormModal/OutboundFormModal) plus three intentional parity tests still import from @/models/. |
||
|
|
31845fa8f6
|
refactor(frontend): tighten HttpUtil generics from any to unknown
Switch the class-level default on Msg<T> and the per-method defaults on HttpUtil.get/post/postWithModal from `any` to `unknown`, so callers that don't pass an explicit T get a narrowed response that must be schema- checked or type-cast before its shape is trusted. Drops the four file-level eslint-disable comments these defaults required. Fixes the nine direct `.obj.field` consumers that surfaced (IndexPage, XrayMetricsModal, NordModal, WarpModal, LogModal, VersionModal, XrayLogModal, CustomGeoSection) by giving each call site the explicit T it should have had from the start — typically a small ad-hoc shape, sometimes a string for the JSON-text-in-Msg.obj pattern used by NordModal/WarpModal/Xray nord/warp endpoints. PR3 of the planned Zod end-to-end rollout — schemas/inbound.ts and schemas/client.ts loose() removal stays parked until the protocol schemas land in Phase 3 to avoid silently dropping fields. |
||
|
|
9cf35234a5
|
feat(frontend): schema-guard Inbound and Outbound form submits
The two largest forms in the panel send to the backend without ever
checking their own port range or required-ness. Schema-gate the
top-level fields so obviously bad payloads stop at the client.
InboundFormModal: InboundFormSchema (port 1-65535 int, non-empty
protocol, the rest of the keys present) runs as a safeParse just
before the HttpUtil.post in submit(). The 2000+ lines of protocol-
specific subform code stay untouched - that's a separate effort and
the existing per-protocol logic (e.g. canEnableStream, isFallbackHost)
already gates most of the structural correctness.
OutboundFormModal: OutboundTagSchema (trim + min 1) replaces the
hand-rolled `if (!ob.tag?.trim()) messageApi.error('Tag is required')`
check. The duplicateTag check stays inline because it needs the
existingTags prop.
Both schemas emit i18n keys for messages with a defaultValue fallback,
matching the pattern in BalancerFormModal and SettingsPage.
|
||
|
|
a3012daa8f
|
feat(frontend): migrate five secondary form modals to Zod schemas
Apply the schema + safeParse-on-submit pattern (introduced for
ClientFormModal / ClientBulkAddModal) to five more forms:
- ClientBulkAdjustModal: ClientBulkAdjustFormSchema enforces 'at least
one of addDays / addGB is non-zero' via .refine(), replacing the
ad-hoc days+gb check.
- BalancerFormModal: BalancerFormSchema covers tag and selector
required-ness; the duplicate-tag check stays inline since it needs
the otherTags prop. Per-field validateStatus now reads from the
parsed issues map.
- RuleFormModal: RuleFormSchema captures the form shape (no required
fields - every property is optional by design). safeParse short-
circuits if anything is structurally wrong.
- CustomGeoFormModal: CustomGeoFormSchema folds the regex alias rule
and the http(s) URL validation (including URL parse) into the
schema, replacing a 20-line validate() function.
- TwoFactorModal: TotpCodeSchema (z.string().regex(/^\d{6}$/)) drives
both the disabled-state of the OK button and the safeParse gate
before the TOTP comparison.
Schemas live alongside the matching API schemas:
- ClientBulkAdjustFormSchema in schemas/client.ts
- BalancerFormSchema / RuleFormSchema / CustomGeoFormSchema in schemas/xray.ts
- TotpCodeSchema in schemas/login.ts (next to LoginFormSchema)
No UX change for valid inputs.
|
||
|
|
d00ddc3f58
|
feat(frontend): extend Zod validation to remaining query/mutation hooks
Adds Zod schemas for client/inbound/xray/node-probe endpoints and wires useNodeMutations, useClients, useInbounds, useXraySetting, useDatepicker through parseMsg. Drops the duplicated per-file ApiMsg<T> interfaces and the local ClientRecord / OutboundTrafficRow / XraySettingsValue / DefaultsPayload declarations in favour of schema-inferred types re-exported from the new src/schemas/ modules. API boundary now validates: clients list/paged, clients onlines, clients lastOnline, clients get/hydrate, inbounds slim, inbounds get, inbounds options, defaultSettings, xray config, xray outbounds traffic, xray testOutbound, xray getXrayResult, getDefaultJsonConfig, nodes probe, nodes test. Mutation responses that consume obj (bulkAdjust, delDepleted, nodes probe / test) get response validation; pass-through mutations stay agnostic. NodeFormModal type-aligned to Msg<ProbeResult>. |
||
|
|
dc37f9b731
|
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.
|
||
|
|
cfe1b25ca0
|
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.
|
||
|
|
edf0f36940
|
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
|
||
|
|
5f318f3b16
|
Add SockOpt.Mark and SockOpt.Interface parameters for Outbound stream (#4480)
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
|
||
|
|
d7f47d8b6a
|
fix(xray): allow private-IP destinations via freedom finalRules
Xray-core v26.4.17 added a default policy that blocks private IPs in the freedom outbound for vless/vmess/trojan/hysteria/wireguard inbounds, even when the panel's routing rules send traffic to direct (#4420). The legacy ipsBlocked override was deprecated in the same release. Default template now seeds the direct outbound with a finalRules entry that explicitly allows geoip:private, so users who intentionally remove the geoip:private->blocked routing rule actually regain LAN access. Defense in depth is preserved: the routing rule still blocks private IPs by default, so unmodified configs keep the same behavior. OutboundFormModal exposes a Final Rules editor under the Freedom section: per-rule action (allow/block), network, port, IP/CIDR/geoip tags, and an optional blockDelay for block actions. |
||
|
|
85e2ded0e1
|
Feat/multi inbound clients (#4469)
* feat(clients): add shadow tables for first-class client promotion
Introduces three new GORM-backed tables (clients, client_inbounds,
inbound_fallback_children) and a populate-only seeder that backfills
them from each inbound's existing settings.clients JSON. Duplicate
emails across inbounds auto-merge under one client row, with each
field conflict logged. Existing services are unchanged and continue
reading from settings.clients — this commit is groundwork only.
* feat(clients): make clients+client_inbounds the runtime source of truth
Adds ClientService.SyncInbound that reconciles the new tables from
each inbound's clients list whenever existing service paths mutate
settings.clients. Wires it into AddInbound, UpdateInbound,
AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and
the timestamp-backfill path in adjustTraffics, plus DetachInbound
on DelInbound.
GetXrayConfig now builds settings.clients from the new tables before
writing config.json, and getInboundsBySubId joins through them
instead of JSON_EACH on settings JSON. Live Xray config and
subscription endpoints are now driven by the relational view;
settings.clients JSON stays in step as a side effect of every write.
* feat(clients): add top-level Clients tab and CRUD API
Adds /panel/api/clients endpoints (list, get, add, update, del,
attach, detach) backed by ClientService methods that orchestrate
the per-inbound Add/Update/Del flows so a single client row is
created once and attached to many inbounds in one operation.
The frontend gains a dedicated Clients page (frontend/clients.html
+ src/pages/clients/) with an AntD table, multi-inbound attach
modal, and full CRUD. Axios interceptor learns to honour
Content-Type: application/json so the JSON endpoints work
alongside the legacy form-encoded ones.
The legacy per-inbound client modal stays untouched in this PR —
both flows now write to the same source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds): add Port-with-Fallback inbound type
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns
The Clients page table gains:
- Online column — green/grey tag driven by /panel/api/inbounds/onlines,
polled every 10s.
- Remaining column — bytes-remaining tag, coloured green/orange/red
against quota, purple infinity when unlimited.
- Action icons per row: QR, Info, Reset traffic, Edit, Delete.
ClientInfoModal shows the full client detail (uuid/password/auth,
traffic ↑/↓ + remaining + all-time, expiry absolute + relative,
attached inbounds chip list, online + last-online).
ClientQrModal fetches links for the client's subId via
/panel/api/inbounds/getSubLinks/:subId and renders each one through
the existing QrPanel component.
Reset Traffic confirms then calls the existing per-inbound endpoint
on the client's first attached inbound (the traffic row is keyed on
email globally, so any attached inbound resets the shared counter).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): expose Attached inbounds in edit mode
The multi-select was gated on add-only, so editing a client had no way
to change which inbounds it belonged to. The picker now shows in both
modes, and on submit the modal diffs the picked set against the
original attachedIds — additions go through the /attach endpoint,
removals through /detach, both after the field update lands so the
new attachments get the latest values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(traffic): drop all-time traffic tracking
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): mobile cards, multi-select, bulk add
Adds the same row-card layout the inbounds page uses on mobile: the
table is suppressed under the mobile breakpoint and each client renders
as a compact card with a status dot, email, Info button, Enable switch,
and overflow menu. All the per-client detail (traffic, remaining,
expiry, attached inbounds, flow, created/updated, URL, subscription)
opens through the existing info modal.
Multi-select with bulk delete wires AntD row-selection on desktop and
a per-card checkbox on mobile; a Delete (N) button appears in the
toolbar when anything is selected.
Bulk add reuses the five email-generation modes from the inbound bulk
modal but takes a multi-inbound picker so one bulk run can attach to
several inbounds at once. Submits client-by-client through the
existing /panel/api/clients/add endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): remove legacy per-inbound client UI
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the
inbounds page, but as a first-class /panel/api/clients/delDepleted
endpoint backed by ClientService. The new path goes through
ClientService.Delete for each depleted email, so the new clients +
client_inbounds + xray_client_traffic tables stay consistent.
Adds a danger-styled toolbar button on the Clients page (next to
Reset all client traffic) with a confirm dialog and a toast
reporting the deleted count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): move every client-shaped endpoint off /inbounds onto /clients
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): switch tgbot + ldap callers to ClientService
Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and
rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService
directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for
add, clientsToJSON/clientToJSON helpers) that callers previously fed to
InboundService.AddInboundClient/DelInboundClient.
ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail
per email instead of trying to coerce AddInboundClient into doing the
update — the old path would have failed duplicate-email validation for
existing clients anyway.
The legacy InboundService.AddInboundClient/UpdateInboundClient/
DelInboundClient methods stay in place; they are now only used internally
by ClientService Create/Update/Delete/Attach. Inlining + deleting them
follows in a separate commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(service): move all client mutation methods to ClientService
Moves the client mutation surface out of InboundService and into
ClientService. These methods all operate on a single client (identity
fields, traffic limits, expiry, ip limit, enable state, telegram tg id)
and didn't belong on the inbound aggregate.
Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient,
DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID,
checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail,
ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail,
ResetClientTrafficLimitByEmail.
Each method now takes an explicit *InboundService for the helpers that
legitimately stay on InboundService (GetInbound, GetClients, runtimeFor,
AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs /
UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs,
GetClientInboundByEmail / GetClientInboundByTrafficID,
GetClientTrafficByEmail).
Stays on InboundService: ResetClientTrafficByEmail and
ResetClientTraffic(id, email) — these mutate xray_client_traffic rows,
not client identity, so they're inbound-side bookkeeping.
Callers updated: tgbot (6 calls), ldap_sync_job (1 call),
InboundService internal (writeBackClientSubID, CopyInboundClients,
AddInbound's email-uniqueness check), ClientService Create/Update/
Delete/Attach/Detach.
Also removes a dead resetAllClientTraffics controller handler whose
route was already gone after the previous /clients API migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): finish migrating to ClientService + tidy IP routes
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(inbounds,clients): clean up inbound modal + enrich client modal
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(inbounds): drop manual Fallbacks UI from inbound modal
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
* feat(clients,inbounds): move search/filter to Clients page + small fixes
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): add Reverse tag field for VLESS-attached clients
Mirrors the Flow field's pattern: a Reverse tag input appears in the
Add/Edit Client modal whenever at least one selected inbound is VLESS
or PortFallback. The value rides over the wire as
client.reverse = { tag: '...' } so it lands directly in model.Client's
*ClientReverse field; an empty value omits the reverse key entirely.
On edit the field is hydrated from props.client.reverse?.tag, and the
showReverseTag watcher clears the field if the user drops the last
VLESS-like inbound from the selection.
* fix(xray): emit only protocol-relevant fields per client entry
The Xray config synthesizer was writing every identifier field (id,
password, flow, auth, security/method, reverse) on every client entry
regardless of the inbound's protocol. Xray ignores unknown fields, so
the config worked, but it diverged from the spec and leaked secrets
across protocols when one client was attached to multiple inbounds —
a VLESS inbound's generated config carried the same client's Trojan
password and Hysteria auth alongside its uuid.
Switch on inbound.Protocol when building each entry:
- VLESS / PortFallback: id, flow, reverse
- VMess: id, security
- Trojan: password, flow
- Shadowsocks: password, method
- Hysteria / Hysteria2: auth
email is emitted for every protocol.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): restore auto-disable kick under new schema
disableInvalidClients still resolved (inbound_tag, email) pairs via
JSON_EACH(inbounds.settings.clients), which is empty after migrating
to the clients + client_inbounds tables. Result: xrayApi.RemoveUser
never ran for depleted clients, clients.enable stayed true so the UI
showed them as active, and only xray_client_traffic.enable got flipped
- making "Restart Xray After Auto Disable" only half-work.
Resolve the targets via a JOIN through the new schema, flip clients.enable
so the Clients page reflects the state, and drop the legacy JSON
write-back plus the subId cascade workaround (email is unique now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): live WebSocket updates + Ended status surfacing
ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(nodes): per-node client roll-up and panel version
Added transient inboundCount / clientCount / onlineCount /
depletedCount fields to model.Node, populated by NodeService.GetAll
via aggregated queries (one join across inbounds + client_inbounds,
one over client_traffics intersected with the in-memory online
emails). The Nodes list renders these as colored chips on a new
"Clients" column so an operator can see at a glance how many users
each node carries and how many are currently online or depleted.
Also exposes the remote panel's version. The central panel adds
panelVersion to its /api/server/status payload (sourced from
config.GetVersion). Probe reads that field and persists it on the
node row, mirroring how xrayVersion already flows. NodesPage gets
a new column next to Xray Version, in both desktop and mobile
views, with English and Persian strings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): stop node sync from resurrecting deleted clients
Several related issues around node-managed clients:
- Remote runtime: drop the per-inbound resetAllClientTraffics path
and point traffic/onlines/lastOnline fetches at the new
/panel/api/clients/* routes.
- Delete from master: always push the updated inbound to the node
even when the client was already disabled or depleted, so the
node actually loses the user instead of silently keeping it.
- setRemoteTraffic: mirror remote clients into the central tables
only on first discovery of a node inbound. Matched inbounds let
the master own the join table, so a stale snap can no longer
re-create a ClientRecord (and join row) for a client that was
just deleted on the master.
- ClientService.Delete: route through submitTrafficWrite so deletes
serialize with node traffic merges, and switch the final
ClientRecord delete to an explicit Where("id = ?") clause.
- setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on
inserts and email-keyed UPDATEs for client_traffics, so mirroring
a snap doesn't trip the unique email index.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(clients): switch client API endpoints from id to email
All client-scoped routes now use the unique email as the path key
(get, update, del, attach, detach, links). Email is the stable,
protocol-independent identifier — UUIDs don't exist for trojan or
shadowsocks, and internal numeric ids leaked panel implementation
detail into the public API.
Removed the redundant /traffic/byId/:id endpoint (covered by
/traffic/:email) and collapsed /links/:id/:email into /links/:email,
which now returns links across every attached inbound for the client.
Frontend selection, bulk delete, and toggle state are now keyed by
email as well, dropping the id→email lookup workaround.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(server): move cached state and helpers into ServerService
ServerController had grown to hold its own status cache, version-list
TTL cache, history-bucket whitelist, and the loop that drove all three
— concerns that belong in the service layer. Pull them out:
- lastStatus + the @2s refresh become ServerService.RefreshStatus and
ServerService.LastStatus; the controller's cron now just orchestrates
the cross-service side effects (xrayMetrics sample, websocket broadcast).
- The 15-minute Xray-versions cache (with stale-on-error fallback) moves
into ServerService.GetXrayVersionsCached, collapsing the controller
handler to a single call.
- The freedom/blackhole outbound-tag walk used by /xraylogs becomes
ServerService.GetDefaultLogOutboundTags.
- The allowed-history-bucket whitelist moves to package-level
service.IsAllowedHistoryBucket, so both NodeController and
ServerController validate against the same list.
Net result: web/controller/server.go drops from 458 to 365 lines and
contains only HTTP wiring + presentation-y side effects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(api): emit JSON-text columns as nested objects
Inbound, ClientRecord, and InboundClientIps store settings /
streamSettings / sniffing / reverse / ips as JSON-text in the DB. The
API was passing that text through verbatim, so every consumer had to
JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so
the wire format is a real nested object, while still accepting the
legacy escaped-string shape on write. Frontend dbinbound.js gets a
matching coerceInboundJsonField helper for the same dual-shape read
path, and inbound.js toJson stops emitting empty/placeholder fields
(externalProxy [], sniffing destOverride when disabled, etc.) so the
new normalised JSON stays terse. api-docs and the inbound-clone path
are updated to the new shape. Controller route lists are regrouped so
all GETs sit above POSTs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and
ClientWithAttachments embeds it to add inboundIds and traffic. Go
promotes the embedded MarshalJSON to the outer struct, so the encoder
was calling ClientRecord.MarshalJSON for the whole value and silently
dropping the extras. The frontend reads row.inboundIds / row.traffic
from /clients/list, so attached inbounds didn't render and newly added
clients looked like they hadn't saved. Add an explicit MarshalJSON on
ClientWithAttachments that splices the extras in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown
Legacy panel hid the IP Log section when access logging was off; the
Vue 3 migration left it gated on isEdit only, so the section showed
even when xray's access log was 'none' and nothing was being recorded.
Restore the ipLimitEnable gate on the edit modal's IP Log form-item.
While here, clean up the Xray Settings access-log dropdown: previously
two 'none' entries appeared (an empty value labelled with t('none') and
the literal 'none' from the options array). Drop the empty option for
access log (the literal 'none' covers it) and relabel the empty option
for error log / mask address to t('empty') so they're distinguishable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(nodes): route per-client ops through node clients API + orphan sweep
Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master
mutates clients on a node via /panel/api/clients/{add,update,del} rather
than pushing the whole inbound. The previous rt.UpdateInbound path made
the node DelInbound+AddInbound on every single-client change, briefly
cycling every other user on the same inbound.
DelInbound no longer filters by enable=true, so a disabled node inbound
actually gets removed from the node instead of being resurrected by the
next snap.
setRemoteTrafficLocked now sweeps any ClientRecord with zero
ClientInbound rows after SyncInbound rebuilds the attachments, which is
how a node-side delete propagates back to master instead of leaving a
detached ghost. ClientService.Delete tombstones the email first so a
snap arriving mid-delete can't re-create the record.
WebSocket broadcasts an "invalidate(clients)" message on every client
mutation so the Clients page refreshes without manual reload.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin
Drops the random/roundRobin gate on the Fallback field in
BalancerFormModal so every strategy can pick a fallback outbound.
syncObservatories now feeds burstObservatory from leastLoad +
random + roundRobin balancers (was leastLoad only), matching how
leastPing feeds observatory.
Fix the JsonEditor "Unexpected end of JSON input" that appeared
when switching a balancer between leastPing and another strategy:
the obsView watcher was gated on showObsEditor (a boolean OR of
the two flags) and missed the case where one observatory
swapped for the other in the same tick. Watch the individual
flags instead so obsView flips to the surviving editor and the
getter stops pointing at a deleted key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): use sortedInbounds for mobile empty-state check
InboundList referenced an undefined visibleInbounds in the mobile
card list's empty-state guard, throwing "Cannot read properties of
undefined (reading 'length')" and breaking the entire mobile render.
* feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList
uses, wrapping filteredClients in sortedClients so sort composes with
the existing search/filter pipeline. Sortable: enable, email,
inboundIds (attachment count), traffic, remaining, expiryTime;
actions and online stay unsorted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers
The Add Client flow on shadowsocks inbounds was producing xray configs
that failed to start:
- 2022-blake3-* ciphers need a base64-encoded key of an exact byte
length per cipher. fillProtocolDefaults was assigning a uuid-style
string, which xray rejects as "bad key". Now the password is
generated (or replaced if invalid) via random.Base64Bytes(n) sized
to the chosen cipher.
- Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a
per-client method field in multi-user mode; model.Client has no
Method, so settings.clients was stored without one and xray failed
with "unsupported cipher method:". applyShadowsocksClientMethod
now injects the top-level method into each client on add/update,
and healShadowsocksClientMethods backfills it at xray-config-build
time so existing inbounds heal on the next start.
- xray/api.go ssCipherType switch was missing aes-256-gcm, which
fell through to ss2022 path.
- SSMethods dropdown now offers aes-256-gcm.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols
Replace the global orphan sweep in setRemoteTrafficLocked with a
per-inbound diff cleanup: only delete a ClientRecord whose email
disappeared from a snap-tracked inbound (i.e. a node-side delete).
Inbounds that vanished entirely from the snap (e.g. admin deleted
the inbound on master) aren't iterated, so a client whose last
attachment came from that inbound is now left alone instead of
being deleted alongside the inbound.
ClientFormModal and ClientBulkAddModal now filter the Attached
inbounds dropdown to protocols that actually support multiple
clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2,
and portfallback (which routes through VLESS settings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): make empty-state text readable on dark/ultra themes
The "No clients yet" empty state had a hardcoded black color
(rgba(0,0,0,0.45)) that vanished against the dark backgrounds.
Drop the inline color, let it inherit from the AntD theme, and
fade with opacity like the mobile card empty state already does.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options
- tgbot: drop legacy per-protocol Add Client UI in favour of a client-first
multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker
let an admin pick one or more inbounds and submit a single client; per-
protocol secrets are now generated server-side via fillProtocolDefaults.
Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases
and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a
setTGUser button + awaiting_tg_id state so the bot can set Client.TgID
during Add.
- clients UI: add Telegram user ID input to ClientFormModal (0 = none).
Hide IP Limit field entirely when ipLimitEnable is off — disabled fields
still take layout space, this collapses Auth(Hysteria) to full width.
- inbounds API: new GET /panel/api/inbounds/options that returns just
{id, remark, protocol, port, tlsFlowCapable}. Used by the clients page
pickers so the dropdown payload stays small on panels with thousands of
clients (drops settings JSON, clientStats, streamSettings). Server-side
TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer
needs to parse streamSettings client-side.
- clientInfoMsg now shows attached inbound remarks, and getInboundUsages
reports the attached client count per inbound.
- api-docs: document the new /options endpoint and add tgId / flow to the
clients add/update bodies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(inbounds): keep Node column visible for node-attached inbounds
The Node column was bound to hasActiveNode, so disabling every node hid
the column even when inbounds were still attached to those nodes — the
admin lost the visual cue that those inbounds belonged to a node and
would come back when it was re-enabled. Combine hasActiveNode with a
new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so
the column survives node-disable.
* fix(api-docs): accept functional-component icons in EndpointSection
AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional
components, so the icon prop's type: Object validator was rejecting
them with a "Expected Object, got Function" warning at runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service
Adds ~110 unit tests across previously untested packages. Focus on
pure-logic and concurrency surfaces where regressions would silently
affect users:
- util/crypto, util/random: password hashing round-trip, ss2022 key
generation, alphabet/length invariants.
- util/netsafe: IsBlockedIP edge cases, NormalizeHost validation,
SSRF guard with AllowPrivate context bypass.
- util/common, util/json_util: traffic formatter, Combine nil-skip,
RawMessage empty-as-null and copy-on-unmarshal.
- sub: splitLinkLines, searchKey/searchHost, kcp share fields,
finalmask normalization, buildVmessLink round-trip.
- xray: Config.Equals and InboundConfig.Equals field-by-field,
getRequiredUserString/getOptionalUserString type checks.
- web/websocket: hub registration, throttling, slow-client eviction,
nil-receiver safety, concurrent register/unregister.
- web/service: NodeService.normalize validation, normalizeBasePath,
HeartbeatPatch.ToUI mapping.
- web/job: atomicBool concurrent set/takeAndReset semantics.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its
four modals through the i18n layer so localized panels stop leaking
English. New keys live under pages.clients (auth, hysteriaAuth, uuid,
flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId,
telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the
root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure
toasts. Also switches the add-client modal's primary button from "Add"
to "Create" for consistency with other create flows.
The bulk-add Random/Random+Prefix/... email-method options stay
hard-coded by request - they're identifier-shaped strings.
* i18n: backfill 99 missing keys across all 12 non-English locales
Brings every translation file up to parity with en-US.json so the
Clients page, the fallback-children inbound section, the new refresh
verb, the Nodes panel-version label and a handful of older holes stop
falling through to the English fallback. New strings span:
- pages.clients.* (labels, confirmations, toasts, emailMethods)
- pages.inbounds.portFallback.* (Reality fallback inbound section)
- pages.nodes.panelVersion, menu.clients, refresh
Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally
left untranslated since they correspond to xray-core field names.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* i18n: drop stale pages.client block duplicated in every non-English locale
Every non-English locale carried a pages.client (singular) section with
30 entries that duplicated pages.clients (plural). The plural namespace
is what the Vue code actually consumes; the singular one was dead
weight from an older rename that never got cleaned up in the
non-English files. Removing it brings every locale to exactly 984
keys, matching en-US.json.
* chore: apply modernize analyzer fixes across codebase
Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
* feat(database): add PostgreSQL as an optional backend alongside SQLite
Lets operators with large client counts or multi-node setups pick PostgreSQL
at install time without breaking the existing SQLite default. Backend is
selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps
the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db`
subcommand copies SQLite data into PostgreSQL in FK-aware order.
* fix(inbounds): gate node selector to multi-node-capable protocols
Hide the Deploy-To selector and clear nodeId when switching to a
protocol that can't run on a remote node. Also:
- subs: return 404 (not 400) when subId matches no inbounds, so VPN
clients distinguish "deleted/unknown" from a server error
- hysteria link gen: use the inbound's resolved address so node-managed
inbounds advertise the node host instead of the central panel
- shadowsocks: default network to 'tcp' (udp was causing issues for some
clients on first-create)
- vite dev proxy: rewrite migrated-route bypass against the live base
path instead of a hardcoded single-segment regex
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.
Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
Settings) that did a stale RMW can't resurrect a just-deleted client with a
fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
missing row doesn't abort the delete with a noisy ErrRecordNotFound log.
Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
alongside the existing Expiry Date picker; on edit-mode open a negative
expiryTime is decoded back to delayed mode + days; on submit the payload
sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
row, so orphans in settings.clients no longer inflate the inbound's count.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(windows): clean shutdown, working panel restart, harden kernel32 load
Three Windows-specific issues addressed:
1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's
"Stop" sends TerminateProcess to the Go binary, which is uncatchable
— our signal handlers never run, so xrayService.StopXray() is skipped
and xray is left dangling. Spawn xray as a child of a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our
handle to the job is closed (which happens even on TerminateProcess).
Also trap os.Interrupt in main so Ctrl+C in the terminal runs the
graceful path.
2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not
supported by windows" because Windows can't deliver arbitrary signals.
Add a restart hook in web/global; main registers it to push SIGHUP
into its own signal channel, and RestartPanel calls the hook before
falling back to the (Unix-only) signal path. Same restart-loop code
runs in both cases.
3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the
kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents
DLL hijacking by a planted DLL next to the binary). Local filetime
type replaced with windows.Filetime, and the unreliable
syscall.GetLastError() fallback replaced with a type assertion on the
errno captured at call time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sys): correct CPU/connection accounting on linux + darwin
util/sys/sys_linux.go:
- GetTCPCount/GetUDPCount were counting the column header row in
/proc/net/{tcp,udp}[6] as a connection, inflating the reported total
by 1 per non-empty file (so the panel status line always showed 2
more connections than actually existed). Replace getLinesNum +
safeGetLinesNum with a single bufio.Scanner-based countConnections
that skips the header.
- CPUPercentRaw now opens HostProc("stat") instead of a hardcoded
/proc/stat so HOST_PROC overrides apply, matching the connection
counters in the same file.
- Simplify CPU field unpacking: pad nums to 8 once instead of guarding
every assignment with a len check.
util/sys/sys_darwin.go:
- Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order
is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's
cpu_darwin_nocgo.go reads the same layout. The previous code used
out[3] as idle and out[4] as intr, so busy = total - dIdle was
actually subtracting interrupt time, making the panel report CPU
usage close to 100% on macOS regardless of actual load.
- Collapse the per-field delta math into a single loop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(xray): rotate crash reports into log folder, prevent overwrites
writeCrashReport had two flaws: it wrote to the bin folder (alongside the
xray binary) which conflates artifacts, and the second-precision timestamp
meant a tight restart-loop crash burst overwrote prior reports. Write to
the log folder with nanosecond precision and keep the last 10 reports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* revert(inbounds): drop unreleased portfallback protocol
The Port-with-Fallback inbound (commit
|
||
|
|
5cf8a08540
|
fix: disable balancer fallbackTag for random / roundRobin strategies
Xray-core's RandomStrategy and RoundRobinStrategy register a pending dependency on the Observatory feature whenever fallbackTag is non-empty. Since the panel only provisions observatory for leastPing / leastLoad balancers, picking roundRobin with a fallbackTag caused xray to fail boot with "not all dependencies are resolved". Disable the fallback field for the two strategies that cannot resolve it, and strip fallbackTag from the wire balancer as a defensive backstop for users who edit the JSON template directly. |
||
|
|
e7035b56fe
|
fix: sync advancedJson before tab switch in convertLink
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
|
||
|
|
21058eb63c
|
fix(routing): make rule drag-and-drop work on mobile cards
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
The pointermove handler looked up the drop target via
el.closest('tr[data-row-key]'). That selector only matches the
desktop a-table rows; the mobile branch renders each rule as a
<div class="rule-card" data-row-key>, so on phones the lookup
always returned null, dropTargetIndex stayed pinned to the start
index, and the eventual drop was a no-op. Loosened the selector
to [data-row-key] so both DOM shapes resolve.
|
||
|
|
194de8869e
|
feat(panel): add 'Edit' button to tables and enhance layout (#4355)
- Move 'Edit' button from dropdown to the table since it's the most used action. Only for desktop. - Increase column widths for action keys in Inbounds, Balancers, Outbounds and Routing tables. - Slightly enhance layout for consistency. |
||
|
|
ce4c42e09c
|
feat(json): swap raw textareas for a CodeMirror 6 JsonEditor
A new JsonEditor.vue component wraps CodeMirror 6 + lang-json with line numbers, JSON syntax highlighting, bracket matching, code folding, search (Ctrl+F), undo/redo, lint (red squiggle and gutter icon on invalid JSON), tab indent, and line wrapping. It is wired into the four raw-JSON spots that previously used <a-textarea class="json-editor">: the Xray Advanced Template tab, the Outbound JSON tab, the Balancer Observatory pane, and the Inbound Advanced tab (settings / streamSettings / sniffing). Chrome colors are driven by EditorView.theme so they win the specificity fight cleanly against CodeMirror's own injected styles. A single buildDarkTheme() factory yields a Dark+ palette (#1e1e1e background, #252526 active line, #2d2d30 panels) for the regular dark mode and a near-black variant (#0a0a0a / #141414 / #1f1f1f border) for ultra-dark — both pair with oneDarkHighlightStyle for the syntax colors. Light mode stays on basicSetup's default. CodeMirror lazy-loads as a ~17 kB gzipped chunk that only appears on the Xray/Inbounds bundles. |
||
|
|
18614bd6ea
|
feat(tabs): collapse settings and xray tab bars to evenly-spread icons
On phones the five Settings tabs and six Xray tabs overflowed the viewport. Now the tab labels are stripped (v-if="!isMobile"), the nav-list stretches to full width via display:flex + width:100%, and each tab claims an equal share with flex:1 1 0 so the icons spread across the row instead of bunching. Icons bumped to 18px with a tooltip carrying the original label for discoverability. |
||
|
|
adc262a238
|
fix(warp): set license against Cloudflare API and surface errors inline
The license update was always failing because the Cloudflare response has no `success` field — the check rejected every successful PUT. On real errors (e.g. "Too many connected devices."), the toast leaked the raw URL + JSON body. Now the WARP API's error envelope is parsed into a clean message and shown inline next to the Update button. |
||
|
|
5543466fcc
|
fix(forms): validate JSON tabs before applying or saving
InboundFormModal: switching out of the Advanced tab now parses the three JSON textareas and rebuilds the structured Inbound via Inbound.fromJson, so the Basic tab reflects what was pasted. Invalid JSON keeps the user on Advanced with a specific parse error. XrayPage: Save now parses xraySetting upfront and snaps the user back to the Advanced tab on invalid JSON instead of letting the backend reject a generic blob. |
||
|
|
6c6b40e063
|
fix(outbound): accept JSON-only configs and sync JSON to basic form on tab switch
Pasting a JSON config and clicking OK failed with "Something went wrong" because validation read the empty form-side tag input instead of the JSON's tag. Switching from the JSON tab to Basic also discarded any JSON the user had pasted. - onOk now validates and submits from the JSON tab using the parsed JSON - Tab switch JSON→Basic deserializes the JSON back into the structured form - Invalid JSON keeps the user on the JSON tab with a clear parse error - Empty form-tag / duplicate-tag errors are now specific, not generic |
||
|
|
b97ff40ad6
|
feat(api-tokens): manage multiple named tokens; add tab/section anchor URLs
Replace the single regenerable API token with a named-token list: - New ApiToken model + service with constant-time auth matching - Seeder migrates the legacy `apiToken` setting into a "default" row - Security tab gets create/enable/delete UI; api-docs page links to it - Dedicated "API Tokens" section in the in-panel docs URL anchors now reflect the active tab/section on Settings, Xray, and API Docs pages, so deep links like `/panel/settings#security` work. Translations for the 8 new SecurityTab strings added across all locales. |
||
|
|
46b6f8c66c
|
feat(routing): drag-reorder rules, split balancer column, mobile card layout
- Grip-handle drag-and-drop on the # cell to reorder rules, built on Pointer Events so the same code works for mouse, touch, and pen (HTML5 drag doesn't fire from touch on iOS Safari). 5px threshold keeps quick taps from triggering a reorder; up/down arrow menu items stay as a keyboard/a11y fallback. Drop indicator is a 2px blue line on the target edge; dragged row fades to 40%. - Split the old combined target column into Outbounds and Balancer columns. Each row now has exactly one populated cell — green outbound tag or purple balancer tag. - Mobile drops the a-table (520px+ of column widths overflowed every phone) for a stacked card layout: # + grip + actions on top, an "Inbound → Outbound/Balancer" flow row in the middle, and criteria chips (domain, IP, port, src IP/port, L4, protocol, user, VLESS) below for whichever fields are actually set. Multi-value chips collapse to "first +N" with full value on hover. |
||
|
|
428f1333ac
|
Security hardening: sessions, SSRF, CSP nonce, CSRF logout, trusted proxies (#4275)
* refactor(session): store user ID in session instead of full struct
Replaces storing the full User object in the session cookie with just
the user ID. GetLoginUser now re-fetches the user from the database on
every request so credential/permission changes take effect immediately
without requiring a re-login. Includes a backward-compatible migration
path for existing sessions that still carry the old struct payload.
* feat(auth): block panel with default admin/admin credentials and guide credential change
checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
* feat(settings): redact secrets in AllSettingView and add TrustedProxyCIDRs
Introduces AllSettingView which strips tgBotToken, twoFactorToken,
ldapPassword, apiToken and warp/nord secrets before sending them to
the browser, replacing them with boolean hasFoo presence flags. A new
/panel/setting/secret endpoint allows updating individual secrets by
key. Secrets that arrive blank on a save are preserved from the DB
rather than overwritten. Adds TrustedProxyCIDRs as a configurable
setting (defaults to localhost CIDRs). URL fields are validated before
save.
* fix(security): SSRF prevention, trusted-proxy header gating, CSP nonce, HTTP timeouts
Adds SanitizeHTTPURL / SanitizePublicHTTPURL to reject private-range
and loopback targets before any outbound HTTP request (node probe,
xray download, outbound test, external traffic inform, tgbot API
server, panel updater). Forwarded headers (X-Real-IP, X-Forwarded-For,
X-Forwarded-Host) are now only trusted when the direct connection
arrives from a CIDR in TrustedProxyCIDRs. CSP policy is tightened with
a per-request nonce. HTTP server gains read/write/idle timeouts. Panel
updater downloads the script to a temp file instead of piping curl into
shell. Xray archive download adds a size cap and response-code check.
backuptotgbot is changed from GET to POST.
* feat(nodes): add allow-private-address toggle per node
Adds AllowPrivateAddress to the Node model (DB default false). When
enabled it bypasses the SSRF private-range check for that node's probe
URL, allowing nodes hosted on RFC-1918 or loopback addresses (e.g.
a private VPN or LAN setup).
* chore: frontend UX improvements, CI pipeline, and dev tooling
- AppSidebar: logout via POST /logout instead of navigating to GET
- InboundList: persist filter state (search, protocol, node) to
localStorage across page reloads; add protocol and node filter dropdowns
- IndexPage: add health status strip (Xray, CPU, Memory, Update) with
quick-action buttons
- dependabot: weekly go mod and npm update schedule
- ci.yml: add GitHub Actions workflow for build and vet
- .nvmrc: pin Node 22 for local development
- frontend: bump package.json and package-lock.json
- SubPage, DnsPresetsModal, api-docs: minor fixes
* fix(ci): stub web/dist before go list to satisfy go:embed at compile time
* chore(ui): remove health-strip bar from dashboard top
* Revert "feat(auth): block panel with default admin/admin credentials and guide credential change"
This reverts commit
|
||
|
|
5f3e9ed0ea
|
feat(xray/nord): searchable server list + colored load tag, surface API errors
Frontend (NordModal.vue):
- Server selector gets show-search with the option label set to
`${cityName} ${name} ${hostname}` so admins can find a specific
server inside a 100+ entry country list by typing.
- Each option renders the load as a colored a-tag (green <30%,
orange 30-70%, red >70%) instead of plain text — quicker visual
scan when sorting through servers in the dropdown.
Backend (nord.go):
- GetCountries / GetServers now check resp.StatusCode and return
"NordVPN API error: <status>" on non-200, matching the pattern
GetCredentials already used. Previously a 4xx/5xx body was
returned as a "success" string and the frontend silently failed
to parse it, surfacing only as an empty "No servers found".
- GetCredentials drops its own ad-hoc 10s http.Client and reuses
the shared nordHTTPClient (15s) — one client, one timeout.
|
||
|
|
e20d73ba7e
|
add loopback and dns servers tag to inbound lists in RuleFormModal (#4244)
* add loopback and dns servers tag to inbound lists in RuleFormModal * fix: remove clientIp from dns section when its empty |