mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-30 00:49:34 +00:00
bf7074358931bf3d707b6f701af7ee1cb39fddca
2485 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bf70743589 |
feat(frontend): basic tab on InboundFormModal.new.tsx (Pattern A)
First real section of the sibling-file rewrite. Wires AntD Form.Items to InboundFormValues paths for the basic tab — enable, remark, deployTo (when protocol is node-eligible), protocol, listen, port, totalGB, trafficReset, expireDate. The port input gets a per-field antdRule against InboundFormBaseSchema.shape.port — the spec's Pattern A reference. The intersection-typed InboundFormSchema has no .shape accessor, so per-field rules pull from the underlying ZodObject components. totalGB and expireDate are bytes/timestamp on the wire but a GB number / dayjs picker in the UI. Both use shouldUpdate-closure children that read form state and call setFieldValue on user input — no transient form-only fields, no DU-shape surprises at submit time. Protocol-change cascade lives in Form's onValuesChange: pick a new protocol and the settings DU branch is reset to createDefaultInboundSettings(next); a non-node-eligible protocol also clears nodeId. Modal still renders a single-tab Tabs container. Sniffing tab is next. |
||
|
|
b10e0d0acd |
feat(frontend): InboundFormModal.new.tsx skeleton (Pattern A)
First commit of the sibling-file modal rewrite. The new modal mounts Form.useForm<InboundFormValues>, hydrates via rawInboundToFormValues on open (edit) or buildAddModeValues (add), runs validateFields + safeParse on submit, and posts the formValuesToWirePayload result. No tabs yet — the modal body shows a WIP placeholder. The file is not imported anywhere; the existing InboundFormModal.tsx remains the one InboundsPage renders. Build, lint, and 280 tests stay green. Subsequent commits add the basic / sniffing / protocol / stream / security / advanced / fallbacks sections; the atomic import swap in InboundsPage.tsx lands last. |
||
|
|
e2784fcf3f |
feat(frontend): outbound settings factories + dispatcher
Adds lib/xray/outbound-defaults.ts parallel to inbound-defaults.ts: 13 createDefault*OutboundSettings factories (one per outbound protocol) plus the createDefaultOutboundSettings(protocol) dispatcher mirroring Outbound.Settings.getSettings's contract — non-null on each known protocol, null otherwise. The factory output matches the legacy `new Outbound.<X>Settings()` start state: required-by-schema fields the user fills in via the form (address, port, password, id, peer publicKey/endpoint) come back as empty stubs. Wireguard alone seeds secretKey via the X25519 generator; the rest expose blank fields. This is the same behavior the OutboundFormModal relies on for protocol-change resets. Shadowsocks defaults to 2022-blake3-aes-128-gcm rather than the legacy undefined — the Select snaps to the first option anyway, so the coherent default keeps the modal from rendering an empty picker. Tests cover three layers: - exact-shape snapshots per factory (13 cases) - Zod schema acceptance after sensible stub fill-in (13 cases) - dispatcher non-null per known protocol + null for the unknown (14 cases) |
||
|
|
142ed97cc0 |
feat(frontend): protocol capability predicates as pure functions
Adds lib/xray/protocol-capabilities.ts with the seven predicates the modals call: canEnableTls, canEnableReality, canEnableTlsFlow, canEnableStream, canEnableVisionSeed, isSS2022, isSSMultiUser. Each takes a minimal slice of an InboundFormValues, no class instance. The legacy isSSMultiUser returns true on non-shadowsocks protocols too (method getter resolves to "" which != blake3-chacha20-poly1305). The new function preserves this quirk and documents it inline; callers all narrow on protocol === shadowsocks before checking, so the surprising return value never surfaces. Parity harness in test/protocol-capabilities.test.ts crosses each of the 10 golden fixtures with 14 stream configurations (network × security) and asserts each predicate matches the legacy class method — 140 cases, all green. |
||
|
|
629567db72 |
feat(frontend): adapter between raw inbound rows and InboundFormValues
Adds lib/xray/inbound-form-adapter.ts with rawInboundToFormValues and formValuesToWirePayload. The pair is the data boundary the upcoming Pattern A modal will use: it consumes the DB row shape (settings et al. as string OR object — coerced internally), hands the modal typed InboundFormValues, and on submit reverses the trip to a wire payload with the three JSON-stringified slices the Go endpoints expect. No dependency on the legacy Inbound/DBInbound classes — the coerce step is inlined so the adapter survives the eventual models/ deletion. Adds 10 Vitest cases covering string vs object inputs, the optional streamSettings/nodeId fields, trafficReset coercion, and a raw-to-payload -to-raw round-trip equality. |
||
|
|
d2f3a7baa7 |
feat(frontend): InboundFormValues schema for Pattern A rewrite
Foundation for the InboundFormModal rewrite. Mirrors the wire Inbound shape (intersection of core fields + protocol settings DU + stream/security DUs) plus the DB-side fields (up/down/total/trafficReset/nodeId/...) that flow through DBInbound rather than the xray config slice. InboundStreamFormSchema is exported separately so individual sub-form sections can rule against just the stream portion when needed. FallbackRowSchema is co-located here even though fallbacks save via a distinct endpoint after the main POST — they belong to the same form state from the user's perspective. No modal changes in this commit. Foundation only; subsequent turns swap the modal's `inboundRef`/`dbFormRef` mutable-class state for Form.useForm<InboundFormValues>(). |
||
|
|
f79e486f9f |
refactor(frontend): swap InboundFormModal option dicts to schemas/primitives
Extends primitives/options.ts with the five inbound-only option dicts (TLS_VERSION_OPTION, TLS_CIPHER_OPTION, USAGE_OPTION, DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION) and lifts InboundFormModal off @/models/inbound for 10 of its 12 imports. Only the Inbound class and SSMethods (inbound vs outbound versions diverge by 2 entries) still come from @/models/. Widens NODE_ELIGIBLE_PROTOCOLS Set element type to string since the new primitives const exposes a narrow literal union that `.has(arbitraryString)` would otherwise reject. |
||
|
|
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/. |
||
|
|
4ce2503c1e |
refactor(frontend): lift Protocols + TLS_FLOW_CONTROL consts to schemas/primitives
Step 4b. The Protocols and TLS_FLOW_CONTROL enums on models/inbound.ts
were dragging five page files into that 3,300-line module just to read
literal string constants. Lifting them to schemas/primitives lets those
pages drop the @/models/inbound import entirely.
- schemas/primitives/protocol.ts now exports a Protocols const map
alongside the existing ProtocolSchema. TUN stays in the const for
parity (legacy panel deployments may have saved TUN inbounds) even
though the Go validator no longer accepts it as a new write.
- schemas/primitives/flow.ts now exports TLS_FLOW_CONTROL. The
empty-string default isn't keyed because the legacy never had a
NONE entry — call sites compare against the two real flow values.
Updated five consumers:
- useInbounds.ts: TRACKED_PROTOCOLS now annotated readonly string[]
so .includes(string) keeps narrowing through the array literal
- QrCodeModal.tsx, InboundInfoModal.tsx: Protocols
- ClientFormModal.tsx, ClientBulkAddModal.tsx: TLS_FLOW_CONTROL
Suite: 89 tests across 8 files; typecheck + lint clean.
models/inbound.ts is now imported by:
- InboundFormModal.tsx (heavy use of Inbound class + getSettings)
- test/inbound-link.test.ts + test/shadow.test.ts + test/headers.test.ts
(intentional — these are parity tests against the legacy class)
OutboundFormModal still imports from models/outbound. Both form modals
are the multi-day Pattern A rewrites the plan scopes separately.
|
||
|
|
bd03f1a117 |
refactor(frontend): swap InboundsPage clone fallback off Inbound.Settings.getSettings
First Step 4 call-site swap. createDefaultInboundSettings(protocol) lands
in lib/xray/inbound-defaults — a protocol-aware dispatch over the 10
per-protocol settings factories already in this module. Returns a Zod-
parsable plain object instead of a class instance, so callers that just
need the wire-shape JSON can drop the class hierarchy without touching
the broader form modals.
InboundsPage's clone path used Inbound.Settings.getSettings(p).toString()
as the fallback when settings JSON parsing failed. That's now
createDefaultInboundSettings + JSON.stringify, with a final '{}' guard
for unknown protocols (legacy returned null and .toString() crashed —
we just emit empty settings instead). The Inbound import on this file
is now unused and removed.
The 2 remaining getSettings call sites in InboundFormModal aren't safe
to swap in isolation — the form mutates the returned class instance
through methods like .addClient() and .toJson() across ~2000 lines of
JSX. Those land with the full Pattern A rewrite of InboundFormModal,
which the plan budgets at multiple days on its own.
Suite: 89 tests across 8 files; typecheck + lint clean.
|
||
|
|
5d07185438 |
refactor(frontend): extract share-link orchestrator to lib/xray/inbound-link
Last slice of Step 3d. Five orchestrator exports compose the per-
protocol generators into the public surface the panel consumes:
- resolveAddr(inbound, hostOverride, fallbackHostname): picks the
address that goes into share/sub URLs. Browser `location.hostname`
is no longer a hidden dependency — callers pass it in (or any other
fallback they want).
- getInboundClients(inbound): protocol-aware clients accessor.
Mirrors the legacy `Inbound.clients` getter, including the SS
quirk where 2022-blake3-chacha20 single-user inbounds report null
(no client loop) and everything else returns the clients array.
- genLink: per-protocol dispatcher matching legacy Inbound.genLink.
- genAllLinks: per-client fanout. Builds the remarkModel-formatted
remark (separator + 'i'/'e'/'o' field picker) and iterates
streamSettings.externalProxy when present.
- genInboundLinks: top-level \r\n-joined link block. Loops per
client for clientful protocols, single-shots SS for non-multi-user,
and delegates to genWireguardConfigs for wireguard. Returns ''
for http/mixed/tunnel (no share URL at all).
Plus genWireguardLinks / genWireguardConfigs fanouts which iterate
peers and append index-suffixed remarks.
Parity test exercises every full-inbound fixture against legacy
Inbound.genInboundLinks. Skips hysteria2 (no legacy dispatch case;
that bridge belongs in a separate intentional commit alongside the
form modal swap). Suite: 89 tests across 8 files; typecheck + lint
clean.
Next: Step 4 form modal migrations. Forms can now drop
`new Inbound.Settings.getSettings(protocol)` in favor of the
createDefault*InboundSettings factories, and InboundsPage clone can
swap to genInboundLinks. Models/ deletion follows in Step 5 once all
call sites are off the class.
|
||
|
|
a7ca8c5b10 |
refactor(frontend): extract genHysteriaLink + Wireguard link/config to lib/xray
Fifth and sixth link generators. genHysteriaLink builds the v1/v2 share URL (scheme picked from settings.version), copying TLS knobs into the query, surfacing the salamander obfs password from finalmask.udp[type=salamander] when present, and writing the broader finalmask payload under `fm` like the other links. Legacy parity note: the old genHysteriaLink read stream.tls.settings.allowInsecure, which isn't a field on TlsStreamSettings.Settings — the guard always evaluated false and the `insecure` param never made it into the URL. We omit it here to stay byte-stable. genWireguardLink and genWireguardConfig take a typed WireguardInboundSettings + peer index and: - link: wireguard://<peerPriv>@host:port?publickey=&address=&mtu=#remark - config: the .conf text WireGuard clients consume directly Both derive the server pubKey from settings.secretKey via Wireguard.generateKeypair at call time — Zod stores only secretKey on the wire (pubKey is computed). The Wireguard utility is pure JS (X25519 over Float64Array), so it runs fine under node + the window polyfill we added with the vmess extraction. Two new full-inbound fixtures (hysteria-v1-tls, wireguard-server) plus matching parity tests bring the suite to 78 tests across 8 files; typecheck + lint clean. Hysteria2 (protocol literal) parity stays deferred — the legacy class has no HYSTERIA2 dispatch case, so it can't round-trip a hysteria2 fixture without a protocol remap. Same trick the shadow harness uses; revisit in the orchestrator commit. |
||
|
|
1e2845306c |
refactor(frontend): extract genTrojanLink + genShadowsocksLink to lib/xray
Third and fourth link generators. genTrojanLink mirrors genVlessLink's
shape (URLSearchParams + network/security branches + remark hash) minus
the encryption/flow VLESS-isms. genShadowsocksLink shares the same query
construction but base64-encodes the userinfo portion as method:password
or method:settingsPw:clientPw depending on whether SS-2022 is in
single-user or multi-user mode.
Three reusable helpers move out of the per-protocol functions:
- writeNetworkParams: the per-network switch that all param-style
links share (tcp http header / kcp mtu+tti / ws path+host /
grpc serviceName+authority / httpupgrade / xhttp extras)
- writeTlsParams: fingerprint/alpn/ech/sni
- writeRealityParams: pbk/sid/spx/pqv (preserves the SNI-omission
legacy parity quirk noted in the genVlessLink commit)
genVmessLink stays with its inline switch — it builds a JSON obj instead
of URLSearchParams and has per-network quirks (kcp emits mtu+tti at
the obj root, grpc maps multiMode to obj.type='multi') that don't
factor cleanly through the shared writer.
Two new full-inbound fixtures (trojan-ws-tls, shadowsocks-tcp-2022)
plus matching parity tests bring the suite to 74 tests across 8 files;
typecheck + lint clean.
|
||
|
|
79c076ee11 |
refactor(frontend): extract genVlessLink to lib/xray/inbound-link
Second link generator. genVlessLink builds the
vless://<uuid>@<host>:<port>?<query>#<remark> share URL from a typed
Inbound + client args, dispatching on streamSettings.network for the
network-specific knobs and on streamSettings.security for the
TLS/Reality knobs. Three param-style helpers move alongside the obj-
style ones already in this file:
- applyXhttpExtraToParams — writes path/host/mode/x_padding_bytes and
the JSON extra blob into URLSearchParams
- applyFinalMaskToParams — writes the fm payload when shareable
- applyExternalProxyTLSParams — overrides sni/fp/alpn when an external
proxy entry is supplied and security is tls
A vless-tcp-reality fixture lands alongside the existing vless-ws-tls
one, so the parity test now exercises both security branches.
Discovered a latent legacy bug while writing parity: the old class
stored realitySettings.serverNames as a comma-joined string and gated
SNI on `!ObjectUtil.isArrEmpty(serverNames)`, which always returns true
for strings — so SNI was never written into Reality share URLs.
Existing clients rely on the omission (they pull SNI from
realitySettings.target instead). We preserve the omission here to keep
this extraction byte-stable; an inline comment marks the spot for a
separate intentional fix.
Suite: 70 tests across 8 files; typecheck + lint clean.
|
||
|
|
5cdb71ec7d | test(frontend): refresh inbound-full snapshot with vmess-tcp-tls fixture | ||
|
|
24c5c80bc3 |
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a typed Inbound + client args and returns the base64-encoded vmess:// URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj, applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask, hasShareableFinalMaskValue, externalProxyAlpn) port across from XrayCommonClass — same logic, rewritten to read the Zod schemas' Record<string, string> headers instead of the legacy HeaderEntry[]. Parity test (inbound-link.test.ts) loads each vmess fixture in golden/fixtures/inbound-full, parses it with InboundSchema for the new pure fn AND constructs LegacyInbound.fromJson(raw) for the class method, then asserts the URLs match byte-for-byte. Drift between the two impls fails here before the call sites in pages/inbounds/* get swapped. Adds a small test setup file that aliases globalThis.window to globalThis so Base64.encode's window.btoa works under Node — keeps the test env at 'node' and avoids pulling jsdom as a new dep. A first vmess-tcp-tls full-inbound fixture pins the round-trip path. Suite: 67 tests across 8 files; typecheck + lint clean. Five more link generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator (toShareLink, genAllLinks) follow in subsequent turns. |
||
|
|
d14eb6923f |
feat(frontend): stream extras + full InboundSchema with DU intersection
Step 3d's last scaffolding piece before link generators. Three new
stream-extras schemas land alongside the network/security DUs:
- finalmask: TcpMask[] + UdpMask[] + QuicParams. Mask `settings` stays
record<string, unknown> for now — there are 13 UDP mask types and 3
TCP mask types with distinct per-type setting shapes, and modeling
them all as DUs would dwarf the rest of stream/ without buying
anything the shadow harness doesn't already catch. Tightened in
Step 6.
- sockopt: 17 socket-tuning knobs (TCP keepalive, TFO, mark, tproxy,
mptcp, dialer proxy, IPv6-only, congestion). `interfaceName` field
matches the panel class naming; serializers rename to `interface` on
the wire.
- external-proxy: rows ship per inbound describing edge fronts (CDN
mirrors). Used by link generators to fan out share URLs.
schemas/api/inbound.ts composes the top-level wire shape with
intersection-of-DUs:
StreamSettingsSchema = NetworkSettingsSchema
.and(SecuritySettingsSchema)
.and(StreamExtrasSchema)
InboundSchema = InboundCoreSchema.and(InboundSettingsSchema)
A fixture (vless-ws-tls.json) exercises the full shape — protocol DU,
network DU, security DU, and TLS cert file branch in one round trip.
The snapshot pins the canonical parsed form so the upcoming link
extractor consumes typed input with no class hierarchy underneath.
Suite: 65 tests across 7 files; typecheck + lint clean. Zod 4
intersection-of-DUs works.
|
||
|
|
c4f5d841b0 |
refactor(frontend): add getHeaderValue wire-shape lookup to lib/xray/headers
Tiny piece of the toShareLink scaffold. The legacy Inbound.getHeader(obj, name) iterated the panel's internal HeaderEntry[] form; the new getHeaderValue reads the Record<string, string|string[]> map our Zod schemas store on the wire. Case-insensitive, returns '' on miss to match the legacy fallback so link-generator call sites stay simple. For repeated-name maps (TCP/WS-style string[] values) the first value wins — matches the legacy iteration order so the share URL's Host hint stays deterministic. Five unit tests cover undefined/null/empty inputs, case folding, string-valued and array-valued matches, empty-array edge case, and missing-key fallback. Suite: 64 tests across 6 files; typecheck + lint clean. This unblocks the next slice: per-protocol link generators (genVmessLink etc.) take a typed inbound + client and call getHeaderValue against the ws/httpupgrade/xhttp/tcp.request header maps. |
||
|
|
e79ca42407 |
refactor(frontend): add createDefault*InboundSettings factories for all 10 protocols
Round out Step 3d's settings factory set. Ten plain-object factories
(vless / vmess / trojan / shadowsocks / hysteria / hysteria2 / http /
mixed / tunnel / wireguard) replace the legacy
`new Inbound.<X>Settings(protocol)` constructors. Each returns a Zod-
parsable wire shape with schema defaults applied — no class instance.
Forms (Step 4) and InboundsPage clone (Step 5) call these factories
directly once the swap lands.
Three factories take a seed for random fields:
- shadowsocks: method-dependent password length via
RandomUtil.randomShadowsocksPassword(method)
- hysteria: explicit `version` override (defaults to 2, matching
the legacy panel constructor — v1 is opt-in)
- wireguard: secretKey from Wireguard.generateKeypair().privateKey
Tests double-verify each factory the same way as the client factories:
snapshot the shape, then Zod parse round-trip to confirm no missing
defaults or stray fields.
Suite: 59 tests across 6 files; typecheck + lint clean. Outbound
factories and the toShareLink extraction follow next.
|
||
|
|
8d5d11cafc |
refactor(frontend): extract createDefault*Client factories to lib/xray
Next Step 3d slice. Five plain-object factories — Vless, Vmess, Trojan, Shadowsocks, Hysteria — replace the legacy `new Inbound.<Protocol>Settings.<Protocol>(...)` constructor chain and the ClientBase XrayCommonClass machinery. Each factory takes an optional seed; missing random fields (id, password, auth, email, subId) fall through to RandomUtil at call time. Forms can hand-pick a UUID; tests pass deterministic seeds so the suite never touches window.crypto. Tests double-verify each factory: a snapshot locks the exact shape, and the matching Zod ClientSchema.parse(out) must equal `out` — no missing defaults, no stray fields, type-narrowed end-to-end. Discovered: VmessClientSchema and VlessClientSchema enforce z.uuid() format, so the test seeds use real-shape UUIDs. Suite: 49 tests across 6 files; typecheck + lint clean. Outbound and inbound-settings factories follow in subsequent turns alongside the toShareLink extraction. |
||
|
|
922a442264 |
refactor(frontend): extract toHeaders + toV2Headers to lib/xray/headers.ts
First Step 3d extraction. The XrayCommonClass static helpers toHeaders/toV2Headers are pure data shape conversions with no class hierarchy needs, so they move to a standalone module that callers can import without dragging in models/inbound.ts. The new module exports HeaderEntry + V2HeaderMap as named types so consumers stop reaching into the legacy class for type shapes. A new test file (headers.test.ts) asserts byte-equality with the legacy XrayCommonClass.toHeaders / .toV2Headers across 18 cases — null / undefined / primitive inputs, single-string headers, array-valued headers, duplicate names, empty-name and empty-value filtering, both arr=true (TCP request/response shape) and arr=false (WS / xHTTP / sockopt shape). Drift between the legacy and new impls fails these tests, so the follow-up call-site swap stays safe. Callers (TcpStreamSettings, WsStreamSettings, HTTPUpgradeStreamSettings, TunnelSettings, etc.) still go through XrayCommonClass for now — those swaps land alongside class-method extractions in subsequent turns. Suite is now 44 tests across 5 files; typecheck + lint clean. |
||
|
|
a7a8041b13 |
test(frontend): shadow-parse harness asserting legacy class and Zod converge
Add Step 3c's safety net: for every inbound golden fixture, run the raw
payload through both pipelines —
legacy: Inbound.Settings.fromJson(protocol, raw.settings).toJson()
zod: InboundSettingsSchema.parse(raw).settings
— canonicalize each (recursively sort keys, drop empty arrays / null /
undefined), and assert byte-equality. This locks the wire shape across the
upcoming class-to-pure-function extraction in Step 3d. Any normalization
drift introduced by the rewrite trips an assertion here before it can
reach users.
Two ergonomic wrinkles handled inline:
- The legacy class lumps hysteria + hysteria2 onto a single
HysteriaSettings (no hysteria2 case in the dispatch table); the test
routes hysteria2 fixtures through the HYSTERIA branch.
- Empty arrays in Zod's output (e.g. fallbacks: [] from a .default([]))
are treated as equivalent to the legacy class's omit-when-empty
behavior. Same wire state, different syntactic surface.
All 26 tests across 4 test files pass on first run.
|
||
|
|
2176e816f0 |
test(frontend): broaden golden coverage to remaining inbounds + stream + security DUs
Round out Step 3b. Four more inbound fixtures complete the protocol set (http with two accounts, mixed with socks-style auth, tunnel with a port map, hysteria v1). Two parallel test files cover the other DUs: stream.test.ts walks tcp/ws/grpc fixtures through NetworkSettingsSchema, and security.test.ts walks none/tls/reality through SecuritySettingsSchema. Snapshot count is now 16 across three test files. The reality fixture locks in the array form of serverNames/shortIds (the panel class stores them comma-joined internally but they ship as arrays on the wire). The TLS fixture pins the file-vs-inline cert DU on the file branch. Stream coverage for httpupgrade/xhttp/kcp and security mixed-with-stream combos follow in the next turn, alongside the shadow harness. |
||
|
|
a9359e921b |
test(frontend): vitest harness with golden-file fixtures for inbound protocols
Stand up Phase 3 safety net before the models/ rewrite. The harness loads JSON fixtures via Vite's import.meta.glob, parses each through InboundSettingsSchema (the tagged-wrapper DU), and snapshots the canonical parsed shape. Snapshots stay byte-stable across the upcoming class-to- pure-function extraction, catching any normalization drift. Six representative inbound fixtures cover the high-traffic protocols: vless, vmess, trojan, shadowsocks (2022-blake3 multi-user), wireguard, hysteria2. Stream and security branches plus the remaining protocols (http, mixed, tunnel, hysteria) follow in subsequent turns. Uses /// <reference types="vite/client" /> instead of @types/node so we avoid pulling in another type package; import.meta.glob is enough to walk the fixtures directory at compile time. Adds vitest 4.1.7 as the only new dev dependency. test/test:watch scripts land in package.json; a standalone vitest.config.ts keeps the production vite.config.js (which reads from sqlite via DatabaseSync) out of the test runner. |
||
|
|
9721dae2b6 |
feat(frontend): stream and security Zod families with discriminated unions
Stand up the remaining Step 2 families. NetworkSettingsSchema is a 6-branch DU on `network` covering tcp/kcp/ws/grpc/httpupgrade/xhttp, with asymmetric per-network wire keys (tcpSettings, wsSettings, ...) preserved exactly so fixtures round-trip byte-identical. SecuritySettingsSchema is a 3-branch DU on `security` covering none/tls/reality. TLS certs use a file-vs-inline union; uTLS fingerprints are shared between TLS and Reality via a single primitive enum. Hysteria-as-network, finalmask, and sockopt are not in the plan's Step 2 inventory and are deferred to Step 6 (Tighten) - they're orthogonal extras on the stream root, not network-discriminated branches. Resolves a Security identifier collision in protocols/index.ts by re-exporting the type alias as SecurityKind (the `Security` name is taken by the namespace re-export). |
||
|
|
8d45cd8c68 |
feat(frontend): protocol-leaf Zod schemas with discriminated unions
Stand up schemas/primitives (Port, Flow, Protocol, Sniffing) and per-protocol
leaf schemas for all 10 inbound and 13 outbound xray protocols. The leaves
omit any inner `protocol` literal — the discriminator lives at the parent
level so consumers narrow on `.protocol` without redundant projection. Wire
shape is preserved per protocol: vmess outbound stays in `vnext[]`, trojan
and shadowsocks outbound in `servers[]`, vless outbound flat, http/socks
outbound in `servers[].users[]`.
Cross-protocol atoms (port, flow, sniffing dest, protocol enum) live in
primitives. Protocol-specific enums (vmess security, ss method/network,
hysteria version, freedom domain strategy, dns rule action) stay with their
leaves. Tagged-wrapper `z.discriminatedUnion('protocol', [...])` composes
both InboundSettingsSchema and OutboundSettingsSchema; existing class-based
models in src/models/ are untouched and will be retired in Step 3 once the
golden-file safety net is in place.
|
||
|
|
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. |
||
|
|
7bd281d26d |
feat(codegen): Go-first tool emitting Zod schemas and TS types
Add tools/openapigen — a single-binary Go program that walks the
exported structs in database/model, web/entity, and xray via go/parser
and emits two committed artifacts under frontend/src/generated:
- zod.ts shared Zod schemas keyed off `validate:` tags (ports get
.min(1).max(65535), Inbound.protocol becomes a z.enum,
Node.scheme too, etc.)
- types.ts plain TS interfaces inferred from the same walk, so
consumers can import Inbound without dragging Zod along
The walker flattens embedded structs (AllSettingView.AllSetting),
honors json:"-" and omitempty, and accepts per-struct overrides so
the JSON-string-inside-JSON columns (Inbound.Settings/StreamSettings/
Sniffing, ClientRecord.Reverse, InboundClientIps.Ips) render as
z.unknown() instead of leaking the DB-storage type into the API
contract. Type aliases like model.Protocol are emitted as TS aliases
and Zod schemas in their own right.
Wires `npm run gen:zod` in frontend/package.json so the generator can
be re-run without leaving the frontend tree. The existing openapi.json
build (gen:api) is left alone for now; migrating the OpenAPI surface
to this generator is a follow-up.
PR2 of the planned Zod end-to-end rollout.
|
||
|
|
7fda988fb2 |
feat(backend): gate request bodies with go-playground/validator
Add a generic BindAndValidate helper in web/middleware that wraps gin's
content-aware binder with an explicit validator.Struct call and emits a
structured `entity.Msg{Obj: ValidationPayload{Issues...}}` on failure so
the frontend can map each issue to an i18n key.
Tag the user-facing fields on model.Inbound, model.Node, and
entity.AllSetting with the range/enum constraints they were previously
relying on hand-rolled CheckValid logic (or nothing) to enforce, and
wire the helper into the inbound/node/settings controllers that bind
those structs directly. Promotes validator/v10 from indirect to direct
require, plus six unit tests covering valid payloads, range violations,
enum violations, malformed JSON, in-place binding, and JSON-only strict
mode.
This is PR1 of a planned end-to-end Zod rollout — controllers using
local form structs (custom_geo, setEnable, fallbacks, client) keep
their existing handling and will be migrated as their schemas firm up.
|
||
|
|
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.
|
||
|
|
4ecbb0e55f |
feat(frontend): block invalid settings saves with Zod pre-save check
Tighten AllSettingSchema with the actual valid ranges and patterns: - webPort / subPort / ldapPort: integer 1-65535 - pageSize: integer 1-1000 - sessionMaxAge: integer >= 1 - tgCpu: integer 0-100 (percentage) - subUpdates: integer 1-168 (hours) - expireDiff / trafficDiff / ldapDefault*: non-negative integers - webBasePath / subPath / subJsonPath / subClashPath: must start with / The existing useAllSettings save path runs AllSettingSchema.partial() through safeParse and logs drift without blocking. SettingsPage now adds a stronger gate before the mutation: run the full schema against the draft and, on failure, surface the first issue (field path + message) via the existing messageApi.error so the user actually sees what's wrong instead of silently sending bad data to the backend. Use cases caught: port out of range, negative quota, sub path missing leading slash, page size set to 0, tgCpu > 100. |
||
|
|
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.
|
||
|
|
2d55b3b663 |
fix(vite): bypass es-toolkit CJS shim for recharts deep imports
The Nodes page (and any other recharts-using route) crashed in dev and prod with TypeError: require_isUnsafeProperty is not a function. Root cause: es-toolkit's package.json exports './compat/*' only via a default condition pointing at the CJS shims under compat/<name>.js. Those shims use a require_X.Y access pattern that Vite's optimizer (Rolldown in Vite 8) and the production Rolldown build both mishandle, losing the named-export accessor and calling the namespace object as a function. recharts imports a dozen of these subpaths with default- import syntax, so every chart path tripped the bug. The matching ESM build at dist/compat/<category>/<name>.mjs is fine, but it only carries a named export. Recharts uses default imports. Plug a small Rollup-compatible plugin (enforce: 'pre') in front of the resolver: any 'es-toolkit/compat/<name>' request becomes a virtual module that imports the named symbol from the right .mjs file and re-exports it as both default and named. The plugin is registered as a top-level plugin (for the prod build) and via the new Vite 8 optimizeDeps.rolldownOptions.plugins (for the dev pre-bundler), so both pipelines pick it up consistently. |
||
|
|
75b0a21987 |
chore(frontend): silence swagger-ui-react peer-dep warnings on React 19
swagger-ui-react@5.32.6 bundles three deps whose declared peer ranges predate React 19: react-copy-to-clipboard@5.1.0 (peer 15-18) react-debounce-input@3.3.0 (peer 15-18, unmaintained) react-inspector@6.0.2 (peer 16-18) For the first two, the actual code is React-19 compatible - only the metadata is stale. Resolve via npm overrides: - react-copy-to-clipboard bumped to ^5.1.1 (peer is open-ended >=15.3.0 in that release). - react-inspector bumped to ^9.0.0 (^8 was a broken publish per its own deprecation notice). - react-debounce-input is wedged on 3.3.0 with no maintained successor on npm. Use the nested-override syntax to satisfy its react peer: "react-debounce-input": { "react": "^19.0.0" } That tells npm to use our React 19 for the package's peer dependency, which silences the warning without changing the package version. |
||
|
|
6bbc9f6769 |
feat(frontend): drive form validation from Zod schemas
NodeFormModal — full conversion to AntD Form.useForm with antdRule on every required field. Inline field errors replace the single 'fillRequired' toast. testConnection now runs validateFields(['address','port']) before sending. ClientFormModal and ClientBulkAddModal — minimal conversion: keep the existing useState-driven controlled-component pattern, but replace the hand-rolled `if (!form.x)` checks with schema.safeParse(form). The schema is the single source of truth for required-ness and types; ClientCreateFormSchema layers on the create-only `inboundIds.min(1)` rule. New schemas (in src/schemas/): NodeFormSchema (node.ts) ClientFormSchema / ClientCreateFormSchema (client.ts) ClientBulkAddFormSchema (client.ts) Other 16+ form modals stay on the current pattern — the antdRule adapter ships from the first Zod pass for opportunistic migration as forms are touched. |
||
|
|
2cd2085b75 |
fix(vite): treat /panel/xray as SPA page, not API root
The dev-server bypass classified /panel/xray as an API path because
the PANEL_API_PREFIXES matcher did `stripped === prefix.replace(/\/$/, '')`,
which made the bare path collide with the SPA route of the same name
(see web/controller/xui.go: g.GET("/xray", a.panelSPA)).
On reload, /panel/xray got proxied to the Go backend instead of being
served by Vite. The backend returned the embedded built index.html
with hashed asset names that the dev server doesn't have, so every
asset 404'd.
Prefix-only match for trailing-slash entries fixes it: panel/xray/...
still routes to the API, but panel/xray itself reaches the SPA branch.
|
||
|
|
c16fb93899 |
fix(frontend): allow null slices in client/summary schemas
Go's encoding/json emits nil []T as null, not []. The initial ClientPageResponseSchema and ClientHydrateSchema rejected null inboundIds / summary.online / summary.depleted / etc., causing [zod] warnings on every empty list. Add nullableStringArray / nullableNumberArray helpers that accept null and transform to [] so consuming code keeps seeing arrays. Mark ClientRecord.traffic and .reverse nullable too (reverse is explicitly null in MarshalJSON when storage is empty). |
||
|
|
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>. |
||
|
|
6846fac1cc |
feat(frontend): add Zod runtime validation at API boundary
Introduces Zod 4 schemas for response validation on the three highest-traffic endpoints (server/status, nodes/list, setting/all) and a Zod->AntD form rule adapter, replacing the duplicated per-file ApiMsg<T> interfaces. Validation runs safeParse with console.warn + raw-payload fallback so backend drift never breaks the UI for users. Login form switches to schema-driven rules as the proof-of-life for the adapter. Class-based models stay untouched; remaining query/mutation hooks and form modals will migrate in follow-ups. |
||
|
|
20edaee8ed |
refactor(frontend): port api-docs/endpoints to TypeScript
endpoints.js was the only remaining JS file under src/. It's a pure data
file describing every panel API surface for the in-panel Swagger docs;
scripts/build-openapi.mjs reads it at build time to emit
public/openapi.json.
Convert it to endpoints.ts with explicit interfaces:
HttpMethod, ParamLocation, ParamType,
EndpointParam, Endpoint, SubscriptionHeader, Section
Type-checking surfaced shapes the .js had silently accepted:
- 'in' values beyond plain 'body' — 'body (form)', 'body (json)',
'body (multipart)' for non-JSON request bodies
- 'type' arrays — 'integer[]', 'object[]'
- Subscription section's subHeader documenting response headers
All four are now part of the union types so the existing data type-checks.
Dead exports removed:
- safeInlineHtml — unused since the docs page switched to Swagger UI
- methodColors — unused
Build pipeline:
- scripts/build-openapi.mjs imports endpoints.ts directly
- gen:api runs via Node 22's native --experimental-strip-types; no
tsx/ts-node dependency added
- --disable-warning=ExperimentalWarning silences just the strip-types
notice while keeping deprecation warnings intact
|
||
|
|
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.
|
||
|
|
19e88c4610 |
fix: address open bug reports (#4539, #4538, #4535, #4531, #4515) (#4545)
* fix: hash-storage panic on SIGHUP and seeder dup-key on cold restart (#4539) Two bugs that combine into an unrecoverable crash loop after a user enables the Telegram bot in settings on a fresh install. 1. CheckHashStorageJob.Run panics with a nil pointer dereference. The cron job is scheduled whenever settings say the bot is enabled, but the package-level hash storage is only initialized inside Tgbot.Start, which StartPanelOnly intentionally skips (startTgBot=false). Toggling the bot on via the panel triggers SIGHUP, the storage stays nil, and the cron fires 2 minutes later and panics, exiting 2. 2. seedClientsFromInboundJSON is not idempotent. The fresh-install early-return path recorded only UserPasswordHash + ApiTokensTable, never ClientsTable. After the admin adds clients via the panel (which writes to the clients table through SyncInbound), the next start runs the seeder for the first time, finds matching emails already in the table, and fails with SQLSTATE 23505 on idx_clients_email, turning the panic above into an unrecoverable crash loop on PostgreSQL. Fixes: - web/job/check_hash_storage.go: nil-check the storage before calling RemoveExpiredHashes. - database/db.go: in the fresh-install early-return path, also record ClientsTable so the seeder never re-runs against panel-added data. - database/db.go: hydrate seedClientsFromInboundJSON's byEmail cache from existing rows so it merges instead of inserting when a row with the same email already lives in the clients table. Regression tests cover both paths. Closes #4539 * fix(clients): preserve protocol-specific credentials across multi-inbound syncs (#4538) fillProtocolDefaults only populates the credential relevant to the inbound's protocol (c.ID for VLESS, c.Auth for Hysteria, c.Password for Trojan/Shadowsocks). Each inbound's settings.clients JSON therefore carries the same client with only one of those fields set. SyncInbound's update path was unconditionally copying every credential column from incoming to the existing clients row, so the second sync (e.g. Hysteria after VLESS) would write UUID="" over a valid VLESS UUID and Auth="" the other way around. The next GetXrayConfig then emitted VLESS client entries with no "id" field, and xray-core crashed on startup with "common/uuid: invalid UUID:". Guard UUID/Password/Auth/Flow/Security/Reverse against empty overwrites so each protocol's sync only writes the credentials it actually owns. Other fields (LimitIP, TotalGB, Comment, etc.) keep the existing copy-everything behavior so admins can still clear them through the panel. Regression test in client_sync_multiprotocol_test.go. Closes #4538 * fix(expiry): show delayed-start countdown in subscribe and client info (#4535) A client with "start after first use" expiry stores the duration as a negative number of milliseconds (e.g. -86400000 = 1 day after first connect). The clients page row already renders this correctly as "Delayed start: 1d", but two other surfaces treated negative values as zero and rendered them as unlimited: - Subscription header: the index==0 / index>0 branches in subService, subClashService and subJsonService only carried ExpiryTime forward when > 0, so traffic.ExpiryTime stayed at zero and the header sent expire=0. Every imported client appeared to have no expiry, and the built-in subscribe page rendered the "unlimited" tag. - ClientInfoModal: both the expiryLabel helper and the rendering check treated <= 0 as the "no expiry" branch, so the modal showed an infinity tag instead of "Delayed start: Nd". Add subscriptionExpiryFromClient to map negative durations onto a "now + |value|" timestamp so subscription clients see an actual expiry they can count down from. Update ClientInfoModal's helper and render to match the clients-page convention. Regression test in subService_test.go covers the helper. Refs #4535 * feat(clash): emit xhttp and httpupgrade transports in subscription (#4531) applyTransport's switch only covered tcp/ws/grpc; xhttp and httpupgrade inbounds fell through to the default branch and returned false. buildProxy then returned a nil map and the inbound was dropped from the Clash subscription. When the subscription only contained xhttp/httpupgrade inbounds, the proxies list ended up empty and the client saw a 404 (or an "Error!" body on older builds), then refused to parse. Add a case for each, mapping the inbound's stream settings onto the Mihomo-format opts blocks: xhttp -> xhttp-opts: { path, host, mode } httpupgrade -> http-upgrade-opts: { path, headers: { Host } } Host falls back to the headers map when the dedicated `host` field is empty, matching the existing ws behavior. Closes #4531 * fix(online): refresh online-clients list even when no WS frontend is connected (#4515) XrayTrafficJob and NodeTrafficSyncJob both gated the entire post-traffic-write block behind websocket.HasClients() to skip expensive broadcasts when no browser is open. The block included the RefreshOnlineClientsFromMap call that keeps the in-memory p.onlineClients list current. Several non-WS consumers read that same list: - Telegram bot (tgbot.go calls p.GetOnlineClients in 3 places) - REST GET /panel/api/onlines (returned to API callers) - Internal alerts that check whether a client is online When no browser was watching the dashboard, the list went stale and stayed empty, so the bot reported "nobody online" and the onlines API returned [] even when xray had active sessions. Move RefreshOnlineClientsFromMap above the HasClients guard so the in-memory list is always fresh. Only the actual BroadcastTraffic / BroadcastClientStats / BroadcastOutbounds calls (and the GetAllClientTraffics / GetInboundsTrafficSummary work that feeds them) remain gated by HasClients. Closes #4515 * fix: address copilot review on #4545 Two issues raised by the Copilot review: 1) subscriptionExpiryFromClient called time.Now() per invocation. Two clients with the same delayed-start duration normalized to timestamps a few milliseconds apart, so the aggregator's "if normalized != traffic.ExpiryTime" check tripped and the subscription header expire= dropped back to 0 — the exact bug the helper was meant to fix, just one client later. Take nowMs as a parameter; each of GetSubs / GetClash / GetConfig captures one timestamp per request and reuses it. 2) Guarding Flow against empty incoming values in SyncInbound prevented a user from ever clearing a VLESS flow via the panel. FlowOverride on client_inbounds is the per-inbound mechanism that already preserves flow correctly across protocols, so the guard on the shared clients.flow column is the wrong place. Drop the Flow guard, keep the rest (UUID/Password/Auth/Security/ Reverse — none of which have a per-inbound override column). Adds a regression test that asserts clearing flow on the owning inbound makes ListForInbound return flow="". The existing cross-protocol test is rewritten to assert on the user-visible behavior (ListForInbound flow) instead of the shared clients.flow column. |
||
|
|
b196f481a8 |
chore(github): overhaul issue and PR templates
Bug, feature, and question templates now collect the triage signal the maintainers usually have to ask for (install method, OS, area, reverse proxy, logs, version). config.yml disables blank issues and points to Wiki / existing issues / latest release from the picker. PR template adds Summary/Why/Type/Areas/Testing/Breaking-changes sections and a fuller checklist (build, tests, lint, typecheck, docs). Renamed pull_request_template.yml -> .md to match GitHub's conventional extension; the old .yml was being read as markdown anyway. |
||
|
|
1f90d2a6ee |
feat(inbound): Advanced XHTTP and external TLS proxy settings (#4491)
* ✨ Introduce extended XHTTP and external proxy settings * ✨ Add custom SNI for proxy * ✨ Add previous changes into React version of app * fix(sub): isolate per-proxy tlsSettings during external-proxy iteration cloneMap (Clash) is shallow and `newStream := stream` (JSON) is an alias, so tlsSettings was shared across iterations. The new applyExternalProxyTLSToStream mutates it, leaking one proxy's serverName/fingerprint/alpn into the next (only overwritten when the next proxy explicitly sets the same field). Add cloneStreamForExternalProxy: shallow clones the top-level stream plus deep clones tlsSettings and tlsSettings.settings. Regression test locks in that proxy B does not inherit proxy A's fingerprint/alpn when B leaves them unset. |
||
|
|
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.
|
||
|
|
867a145979 |
feat(clients): add inbound filter + mobile page-size control
Filter bar gets an Inbound select next to Protocol — the dropdown is narrowed to inbounds matching the chosen protocol (or shows everything when no protocol is picked), with remark search inside the dropdown. Choosing a protocol clears any inbound selection that no longer fits. Server side, ClientPageParams gains an Inbound int and ListPaged runs a clientMatchesInbound check after the protocol filter. The selection persists in clientsFilterState localStorage alongside the existing search/filter/protocol entries. Mobile clients view also grows the AntD Pagination control that was previously only on the desktop table, so page size / page navigation are reachable from phones.v3.1.0 |
||
|
|
6185db586a |
fix(clients): drop tombstone gate that blocked re-import after delete
ClientService.Delete tombstones a just-deleted email for 90s to keep a late node snapshot from resurrecting it. The same check was also gating the create branch of SyncInbound — which silently dropped clients on any legitimate re-add (delete inbound + re-import within 90s left the clients table empty even though settings.clients carried the rows). The snapshot-side caller in setRemoteTraffic already filters tombstoned emails before handing the list to SyncInbound, so removing the duplicate check inside SyncInbound preserves the protection where it's needed and unblocks user-initiated re-imports. While here, mirror the addInbound shape in importInbound (NodeID=0→nil normalisation, early return on error, broadcastInboundsUpdate) and fan out a notifyClientsChanged from add/del/update/import so an open Clients page picks up settings.clients reconciliation without a manual refresh. |
||
|
|
4c71669815 |
fix(clients): match by email when client identifier is stale
DBs migrated from older versions where the same email lived in multiple inbounds with different UUIDs/passwords/auths end up with one merged ClientRecord but each inbound's settings.clients JSON still carries its original protocol-specific identifier. Editing such a client through /panel/api/clients/update/:email failed with "empty client ID" because UpdateInboundClient couldn't locate the entry by the ClientRecord's identifier. When the primary lookup misses, fall back to resolving the ClientRecord by the supplied identifier and matching the inbound entry by email. The update then proceeds and the inbound JSON converges to the merged identifier. |
||
|
|
c6123f9628 |
fix(frontend): resolve lazy chunk URLs against runtime base path (#4505)
* fix(frontend): reload page on Vite chunk preload error after upgrade
After a panel upgrade the embedded dist/ ships with new hashed chunk
filenames, so SPA tabs loaded before the upgrade hold references to
chunks that no longer exist on the server and lazy modals 404. Hook
`vite:preloadError` and force one full reload (guarded by a session
flag) so the browser picks up the new index.html.
* Revert "fix(frontend): reload page on Vite chunk preload error after upgrade"
This reverts commit
|