txlyre

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 3c5e9fa774 fix(sub): preserve userinfo encoding in trojan/shadowsocks/hysteria links The link builders ran the assembled share link through url.Parse + parsedURL.String(), which decodes the userinfo and re-emits it via Go's lenient encoder — sub-delim chars (=, +, ;) are left literal even when the caller had pre-encoded them via encodeUserinfo. Result: copy URL from the panel UI worked (FE never round-trips), but the same inbound in the subscription body became "trojan://abc%2Fdef=ghi+@..." and was rejected by Trojan/Hysteria clients. Replace url.Parse + .String() with a direct string-builder that appends ?query and #fragment without touching the userinfo, and apply it to genHysteriaLink's inline copies too. Also switch the shadowsocks userinfo from base64.StdEncoding (with =/+/ /padding) to base64.RawURLEncoding to match the frontend's Base64.encode(s, true).
  • 31d7ed5103 refactor(outbound): probe via xray burstObservatory instead of SOCKS round-trip Replace the HTTP-mode outbound test that spun up a SOCKS inbound and ran an httptrace'd request from the Go client with a probe-only xray config: burstObservatory probes the target outbound directly and the result is read from xray's /debug/vars metrics endpoint. The probe lives inside xray, so the measured delay and failure reasons reflect what xray itself sees over the real proxy chain. Drops the DNS/Connect/TLS/TTFB breakdown (and statusCode) since the observatory snapshot only exposes total delay; the frontend popover is updated accordingly.
  • 3f787ae169 feat: complete Zod migration of frontend + bulk client batching (#4599) * 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. * 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>. * 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). * 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. * 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. * chore(frontend): silence swagger-ui-react peer-dep warnings on React 19 [email protected] bundles three deps whose declared peer ranges predate React 19: [email protected] (peer 15-18) [email protected] (peer 15-18, unmaintained) [email protected] (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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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). * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * test(frontend): refresh inbound-full snapshot with vmess-tcp-tls fixture * 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. * 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. * 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. * 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. * 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. * 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. * 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/. * 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. * 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. * 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>(). * 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. * 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. * 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) * 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. * 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. * feat(frontend): sniffing tab on InboundFormModal.new.tsx (Pattern A) Second section of the sibling-file rewrite. Wires the six sniffing sub-fields to nested form paths ['sniffing', 'enabled'], ['sniffing', 'destOverride'], etc. Uses Form.useWatch on the enabled flag to drive conditional rendering of the dependent fields — the same gate the legacy modal expressed via `ib.sniffing.enabled &&`. Checkbox.Group renders one Checkbox per SNIFFING_OPTION entry. The two exclusion lists use Select mode="tags" so the user can paste comma- separated IP/CIDR or domain rules. No transient form state, no class methods — every field maps directly to a wire-shape path in InboundFormValues. Protocol tab is next. * feat(frontend): protocol tab VLESS auth on InboundFormModal.new.tsx Adds the protocol tab to the sibling-file rewrite — currently only the VLESS section, which lays out decryption/encryption inputs and the three buttons that drive them: Get New x25519, Get New mlkem768, Clear. getNewVlessEnc + clearVlessEnc are ported from the legacy modal as pure setFieldValue paths into ['settings', 'decryption'] / ['settings', 'encryption'] — no class methods, no inboundRef. The matchesVlessAuth helper mirrors the legacy fuzzy label-matching so the backend response shape stays the only source of truth. selectedVlessAuth derives the displayed auth label from the encryption string via Form.useWatch — same heuristic as the legacy modal (.length > 300 → mlkem768, otherwise x25519). Tab spread is conditional: the protocol tab only appears when protocol === 'vless' right now. As more protocol sections land (shadowsocks, http/mixed, tunnel, tun, wireguard) the condition will widen to cover each one. * feat(frontend): protocol tab Shadowsocks section (Pattern A) Adds the Shadowsocks sub-form: method picker (from SSMethodSchema's seven schema-aligned options), conditional password input gated on isSS2022, network picker (tcp/udp/tcp,udp), ivCheck toggle. Method change cascades through the Select's onChange — regenerating the inbound-level password via RandomUtil.randomShadowsocksPassword. The shadowsockses[] multi-user list reset is deferred until the clients-management section lands. Uses isSS2022 from lib/xray/protocol-capabilities to gate the password field exactly the way the legacy modal did — keeps the form behavior identical without referencing the legacy class. SSMethodSchema.options drives the Select rather than the legacy SSMethods const (which the inbound modal pulled from models/inbound.ts). This commits to the schema-aligned 7-entry list for inbound; the outbound divergence (9 entries with legacy aliases) is still pending in OutboundFormModal — defer the UX decision to that rewrite. * feat(frontend): protocol tab HTTP and Mixed sections (Pattern A) Adds the HTTP and Mixed sub-forms. Both share an accounts list — first Form.List usage in the rewrite. Each row binds via [field.name, 'user'] / [field.name, 'pass'] under the parent ['settings', 'accounts'] path, so the wire shape stays exactly what HttpInboundSettingsSchema and MixedInboundSettingsSchema validate. HTTP-only: allowTransparent Switch. Mixed-only: auth Select (noauth/password), udp Switch, conditional ip Input gated on the udp value via Form.useWatch. Tab visibility widens to include http + mixed alongside vless + shadowsocks. The string cast on the includes-check keeps the frozen Protocols const's narrow union from rejecting the broader protocol string at the call site. * feat(frontend): protocol tab Tunnel section (Pattern A) Adds the Tunnel sub-form: rewriteAddress + rewritePort, allowedNetwork picker (tcp/udp/tcp,udp), Form.List-driven portMap with name/value pairs, and the followRedirect Switch. portMap is the second Form.List in the rewrite — same shape as the HTTP/Mixed accounts list but with name/value rather than user/pass. The wire shape stays `settings.portMap: { name, value }[]` exactly. Tab visibility widens to Tunnel. * feat(frontend): protocol tab TUN section (Pattern A) Adds the TUN sub-form: interface name, MTU, four primitive-array Form.Lists (gateway, dns, autoSystemRoutingTable), userLevel, autoOutboundsInterface. Primitive Form.Lists bind each row's Input directly to `field.name` (no inner key) — distinct from the object-row Form.Lists that bind to `[field.name, 'fieldKey']`. The Form.useWatch('protocol') return type comes from the schema's protocol enum which excludes 'tun' (TUN is in the legacy Protocols const for data parity but never accepted by the wire validator). Cast to string at the source so per-section comparisons against Protocols.TUN typecheck. Why: legacy DB rows with protocol === 'tun' still need to render; widening here keeps reads from rejecting them. Tab visibility widens to TUN. * feat(frontend): protocol tab Wireguard section (Pattern A) Adds the Wireguard sub-form: server secretKey input with regen icon, derived disabled public-key display, mtu, noKernelTun toggle, and a Form.List of peers — each peer having its own privateKey (regen icon), publicKey, preSharedKey, allowedIPs (nested Form.List for the string array), keepAlive. pubKey is purely derived (computed via Wireguard.generateKeypair from the watched secretKey) and is NOT stored in the form value — the schema omits it from the wire shape on purpose. The disabled display shows the live derivation without polluting form state. regenInboundWg generates a fresh keypair and writes only the secretKey path; pubKey re-derives automatically. regenWgPeerKeypair writes both privateKey and publicKey at the peer's path index. The preSharedKey wire-shape name is used instead of the legacy class's internal psk — matches WireguardInboundPeerSchema. Tab visibility widens to Wireguard. * feat(frontend): stream tab skeleton with TCP + KCP (Pattern A) Opens the stream tab on the sibling-file rewrite. Tab visibility is driven by canEnableStream from lib/xray/protocol-capabilities — same gate the legacy modal used, now schema-aware. Transmission picker (network select) is hidden for HYSTERIA since that protocol's network is implicit. onNetworkChange clears any stale per-network settings keys (tcpSettings/kcpSettings/...) and seeds an empty object for the new branch so AntD Form.Items don't read from undefined nested paths. TCP section: acceptProxyProtocol Switch (literal-true-optional on the wire — the form stores true/false but Zod's strip behavior keeps false-as-omission round-trips clean) plus an HTTP-camouflage toggle that flips header.type between 'none' and 'http'. The full HTTP camouflage request/response sub-form lands in a follow-up commit. KCP section: six numeric knobs (mtu, tti, upCap, downCap, cwndMultiplier, maxSendingWindow). WS / gRPC / HTTPUpgrade / XHTTP / external-proxy / sockopt / hysteria stream / FinalMaskForm hookup all still pending. * feat(frontend): stream tab WS + gRPC + HTTPUpgrade sections (Pattern A) Adds the three medium-complexity network branches to the stream tab. Plain Form.Item paths into the corresponding *Settings keys — no Form.List wrappers since these schemas don't have arrays at the top level. WS: acceptProxyProtocol, host, path, heartbeatPeriod gRPC: serviceName, authority, multiMode HTTPUpgrade: acceptProxyProtocol, host, path Header editing is deferred to a later commit — WsHeaderMap is a Record<string,string> on the wire, V2HeaderMap a Record<string,string[]>, and the form needs an array-of-{name,value} UI that converts on edit. Worth building once and reusing across WS, HTTPUpgrade, XHTTP, TCP request/response, and Hysteria masquerade headers. XHTTP + external-proxy + sockopt + hysteria stream + finalmask hookup still pending. * feat(frontend): stream tab XHTTP section (Pattern A) XHTTP is the heaviest network branch — 19 fields rendered conditionally on mode, xPaddingObfsMode, and the three *Placement selectors. Each gates its dependent field set via Form.useWatch. Field structure mirrors the legacy XHTTPStreamSettings form 1:1: - mode picker (auto / packet-up / stream-up / stream-one) - packet-up adds scMaxBufferedPosts + scMaxEachPostBytes; stream-up adds scStreamUpServerSecs - serverMaxHeaderBytes, xPaddingBytes, uplinkHTTPMethod (with the packet-up gate on the GET option) - xPaddingObfsMode unlocks xPadding{Key,Header,Placement,Method} - sessionPlacement / seqPlacement each unlock their respective Key field when set to anything other than 'path' - packet-up mode additionally unlocks uplinkDataPlacement, and that in turn unlocks uplinkDataKey when the placement is not 'body' - noSSEHeader Switch at the tail XHTTP headers editor still pending (same WsHeaderMap as WS — will be unified in the header-editor extraction commit). * feat(frontend): stream tab external-proxy + sockopt sections (Pattern A) External Proxy: Switch driven by externalProxy array length. Toggling on seeds one row with the window hostname + the inbound's current port; toggling off clears the array. Each row is a Form.List item with forceTls/dest/port/remark inline, and a nested SNI/Fingerprint/ALPN row that conditionally renders on forceTls === 'tls' via a shouldUpdate-closure that watches the per-row forceTls path. Sockopt: Switch driven by whether the sockopt object exists in form state. Toggling on calls SockoptStreamSettingsSchema.parse({}) so every default the schema declares (mark=0, tproxy='off', domainStrategy='UseIP', tcpcongestion='bbr', etc.) flows into the form; toggling off sets to undefined. Renders the seventeen sockopt fields directly bound to ['streamSettings', 'sockopt', X] paths. Option lists pull from the primitives const dictionaries (UTLS_FINGERPRINT, ALPN_OPTION, DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION) rather than the schema's .options to keep one source of truth for UI label strings. * feat(frontend): security tab base + TLS section (Pattern A) Adds the security tab to the sibling-file rewrite. Visibility is paired with the stream tab — both gated on canEnableStream. The security selector is itself disabled when canEnableTls is false, and the reality option only appears when canEnableReality is true, mirroring the legacy modal's Radio.Group guards. onSecurityChange clears the previous branch's *Settings key and seeds the new branch from the schema's parsed defaults (the same trick the sockopt toggle uses). The security selector itself is rendered via a shouldUpdate closure so the on-change handler can write the cleaned streamSettings shape atomically without racing AntD's per-field sync. TLS section: serverName (the wire field — the legacy class calls it sni internally), cipherSuites (with the 13 named suites from TLS_CIPHER_OPTION), min/max version pair, uTLS fingerprint, ALPN multi-select, plus the three policy Switches. TLS certificates list, ECH controls, the full Reality sub-form, and the four API-call buttons (genRealityKeypair / genMldsa65 / getNewEchCert / randomizers) land in a follow-up commit. * feat(frontend): security tab Reality + ECH + mldsa65 controls (Pattern A) Adds the Reality sub-form and the four API-call buttons that drive the server-generated material: - genRealityKeypair calls /panel/api/server/getNewX25519Cert and writes the result into ['streamSettings', 'realitySettings', 'privateKey'] and the nested settings.publicKey path. - genMldsa65 calls /panel/api/server/getNewmldsa65 for the post-quantum seed/verify pair. - getNewEchCert calls /panel/api/server/getNewEchCert with the current serverName and writes echServerKeys + settings.echConfigList. - randomizeRealityTarget seeds target + serverNames from the random reality-targets pool. - randomizeShortIds calls RandomUtil.randomShortIds (comma-joined string) and splits into the schema's string[] form. Reality fields are bound directly to schema paths — show/xver/target, maxTimediff, min/max ClientVer, the settings.{publicKey, fingerprint, spiderX, mldsa65Verify} nested subtree, plus the array fields (serverNames, shortIds) rendered as Select mode="tags" since both ship as string[] on the wire. TLS certificates list (Form.List with the useFile DU) still pending — that's a chunky sub-form on its own. * feat(frontend): security tab TLS certificates list (Pattern A) Closes out the security tab: a Form.List of certificates that toggles between TlsCertFileSchema (certificateFile + keyFile string paths) and TlsCertInlineSchema (certificate + key as string arrays per the wire shape) via a per-row useFile boolean. useFile is a transient form-only field — not part of TlsCertSchema. Zod's default-strip behavior drops it during InboundFormSchema parse on submit, leaving only the matching wire branch's keys populated. Whichever side the user wasn't on stays empty, so Zod's union picks the populated branch. For inline certs the TextAreas use normalize + getValueProps to convert between the wire-side string[] and the multi-line text the user types. Each line becomes one array element, matching the legacy class's `cert.split('\n')` toJson convention. Per-row buildChain is conditionally rendered when usage === 'issue' — a shouldUpdate-closure watches the specific path so the toggle re-renders inline without listening to unrelated form changes. Security tab is now functionally complete. Advanced JSON tab, Fallbacks card, and the atomic swap in InboundsPage are next. * feat(frontend): advanced JSON tab on InboundFormModal.new.tsx (Pattern A) Adds the advanced JSON tab. Each sub-tab (settings / streamSettings / sniffing) renders an AdvancedSliceEditor — a small CodeMirror-backed JsonEditor that holds a local text buffer and forwards parsed JSON to form state on every valid edit. Invalid JSON sits silently in the local buffer; once the user finishes balancing braces / quoting, the next valid parse pushes through to the form. No stamping ref, no apply-on-tab-switch ceremony — the form is the single source of truth. The buffer seeds once from form state on mount. The Modal's destroyOnHidden means each open is a fresh editor instance, so external form mutations during a single open session can't desync the editor either. The streamSettings sub-tab is omitted when streamEnabled is false (matching the legacy modal's behavior for protocols like Http / Mixed that have no stream layer). * feat(frontend): fallbacks card on InboundFormModal.new.tsx (Pattern A) Adds the fallbacks card rendered inside the protocol tab whenever the current values describe a fallback host — VLESS or Trojan on tcp with tls or reality security. The protocol tab visibility widens to include Trojan in that exact case (it has no other protocol sub-form). Fallbacks live in a useState alongside the form rather than inside form values, mirroring the legacy modal: fallbacks save via a distinct endpoint (/panel/api/inbounds/{id}/fallbacks) after the main inbound POST, not as part of the inbound payload. loadFallbacks runs on open for edit-mode VLESS/Trojan; saveFallbacks runs after a successful POST inside the submit handler. Each row: child picker (filtered down to other inbounds), then four inline edits for SNI / ALPN / path / xver. Add adds an empty row; delete pulls the row from state. Quick-Add-All, the rederive-from-child helper, and the per-row up/down movers are deferred — the basic add/edit/remove cycle is what the modal actually needs to function. * feat(frontend): atomic swap InboundFormModal to Pattern A Deletes the 2261-line class-mutation modal and renames the 1900-line sibling rewrite into its place. InboundsPage.tsx already imports the file by path so no consumer change is needed — the swap is one file delete plus one file rename. Build, lint, and 280 tests stay green. What the new modal covers end-to-end: - Basic (enable / remark / nodeId / protocol / listen / port / totalGB / trafficReset / expireDate) - Sniffing (enabled / destOverride / metadataOnly / routeOnly / ipsExcluded / domainsExcluded) - Protocol per DU branch: VLESS (decryption/encryption + buttons), Shadowsocks (method/password/network/ivCheck), HTTP + Mixed (accounts list + per-protocol toggles), Tunnel (rewrite + portMap + followRedirect), TUN (interface/mtu + four primitive lists + userLevel/autoInterface), Wireguard (secretKey + derived pubKey + peers list with nested allowedIPs) - Stream per network: TCP base, KCP, WS, gRPC, HTTPUpgrade, XHTTP (the 22-field one), plus external-proxy and sockopt extras - Security: TLS (SNI/cipher/version/uTLS/ALPN/policy switches + certificates list with file/inline toggle + ECH controls), Reality (every field + the four API-call buttons), none - Advanced JSON (settings / streamSettings / sniffing live editors that round-trip into form state on every valid parse) - Fallbacks (load on open for VLESS/Trojan TLS-or-Reality TCP hosts; save through the secondary endpoint after the main POST succeeds) Known regressions vs the legacy modal, all reachable via Advanced JSON until backfilled in follow-up commits: - Hysteria stream sub-form (masquerade / udpIdleTimeout / version) — schema gap; the existing inbound DU has no hysteria stream branch - FinalMaskForm hookup — the component is still class-shape coupled - HeaderMapEditor — TCP request/response headers, WS / HTTPUpgrade / XHTTP headers, Hysteria masquerade headers all need a shared editor - TCP HTTP camouflage request/response body (version, method, path list, headers, status, reason) — only the on/off toggle is wired - Fallbacks polish — up/down move, quick-add-all, rederive-from-child, the per-row advanced-toggle / proxy-tag chips No reference to @/models/inbound's Inbound class anywhere in the new modal — only @/models/dbinbound (out of scope) and @/models/reality-targets (out of scope). The protocol-capabilities predicates and the rawInboundToFormValues + formValuesToWirePayload adapters carry every behavior the class used to provide. * fix(frontend): finish InboundFormModal rename after atomic swap The atomic-swap commit landed the new file but the exported function was still named InboundFormModalNew. Rename to match the file. * feat(frontend): outbound form schema + wire adapter foundation Lay the groundwork for OutboundFormModal's Pattern A rewrite: - schemas/forms/outbound-form.ts: discriminated-union form values across all 12 outbound protocols, with flat per-protocol settings shapes that match the legacy class fields (vmess vnext / trojan-ss-socks-http servers / wireguard csv address-reserved all flattened). - lib/xray/outbound-form-adapter.ts: rawOutboundToFormValues converts wire-shape outbound JSON to typed form values; formValuesToWirePayload re-nests on submit. Replaces the Outbound.fromJson/toJson dependency the modal currently has on the legacy class hierarchy. - test/outbound-form-adapter.test.ts: 15 round-trip cases covering each protocol's wire quirks (vmess vnext flatten, vless reverse-wrap, wireguard csv↔array, blackhole response wrap, DNS rule normalization, mux gating). * 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. * 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. * 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. * 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). * 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. * 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. * 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. * 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) * test(frontend): convert legacy-class parity tests to snapshot baselines With the inbound/outbound modal rewrites complete, the cross-check against the legacy Inbound class has served its purpose. The new pure-function / Zod-schema paths are the source of truth for production code; the parity assertions were the migration safety net. Convert the three parity test files to snapshot-based regression tests: - headers.test.ts: toHeaders + toV2Headers run against snapshots captured at the close of the migration (when both new and legacy were verified byte-equal). - protocol-capabilities.test.ts: 140 cases (10 fixtures × 14 stream shapes) snapshot the predicate-result tuple. Was: parity vs legacy Inbound.canEnableX() class methods. - inbound-link.test.ts: per-protocol genXxxLink + genInboundLinks orchestrator output is snapshotted. Was: byte-equality vs legacy Inbound.genXxxLink() methods. Also delete shadow.test.ts — its purpose was a dual-parse drift detector (Inbound.Settings.fromJson vs InboundSettingsSchema.parse). inbound-full.test.ts already snapshots the Zod parse output, which covers the same ground without the legacy dependency. models/inbound.ts and models/outbound.ts stay in the tree for now — DBInbound still consumes Inbound via its toInbound() method, and DBInbound migration is out of scope per the migration spec ('Do NOT migrate Status, DBInbound, or AllSetting...'). No production page imports from @/models/inbound or @/models/outbound directly anymore. * chore(frontend): enforce no-explicit-any: error + add typecheck/test to CI Step 7 of the Zod migration: lock the migration's gains in place via lint + CI enforcement. - eslint.config.js: `@typescript-eslint/no-explicit-any` set to error. Verified locally — zero violations in src/, with the only file-level disables being src/models/inbound.ts and src/models/outbound.ts (kept for DBInbound's toInbound() consumer; their migration is out of spec scope). - .github/workflows/ci.yml: add Typecheck and Test steps to the frontend job, between Lint and Build. PRs now have to pass tsc --noEmit and the full vitest suite (285 tests + 172 snapshots) before build runs. Migration scoreboard (vs the spec): Step 1 primitives + barrels done Step 2 protocol leaf + DUs done Step 3 pure-fn extraction done Step 4 form modals -> Pattern A done (Inbound + Outbound) Step 5 delete models/ files DEFERRED (DBInbound still uses Inbound; spec marks DBInbound migration out of scope) Step 6 tighten .loose() / unknown DEFERRED (invasive, separate PR) Step 7 lint + CI enforcement done (this commit) Production code paths now have no direct dependency on the legacy Inbound or Outbound classes. * 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. * 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. * 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. * 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). * feat(frontend): fallbacks polish — move up/down + Add all button Two small UX wins on the InboundFormModal Fallbacks card: - Per-row Move up / Move down buttons (ArrowUp/Down icons) that swap adjacent indices. Order survives reloads via sortOrder (rebuilt from index on save). First row's Up button + last row's Down button are disabled. - 'Add all' button next to 'Add fallback' that one-shot inserts a fresh row for every eligible inbound (every option in fallbackChildOptions) not already wired up. Disabled when every eligible inbound is already covered. Convenient for operators running catch-all routing across every host on the panel. * 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. * feat(frontend): inbound TCP HTTP camouflage response fields + request headers Complete the TCP HTTP camouflage UI on the inbound side. Already there from the previous symmetric host/path commit: - Request host (string[] via comma-string) - Request path (string[] via comma-string) This commit adds: - Request headers (V2 map: name -> string[]) via HeaderMapEditor. - Response version (defaults to '1.1' when camouflage toggles on). - Response status (defaults to '200'). - Response reason (defaults to 'OK'). - Response headers (V2 map) via HeaderMapEditor. The HTTP camouflage Switch seeds both request and response sub-objects on toggle-on so xray-core sees a valid TcpHeader.http shape from the first save. Without the response seed, partial fills would emit a schema-incomplete response block that xray-core might reject. * 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. * feat(frontend): inbound Hysteria stream sub-form (auth + udpIdleTimeout + masquerade) Restore the inbound side of Hysteria stream configuration that was previously hidden — the legacy modal exposed these knobs but the Pattern A rewrite gated them out. schemas/protocols/stream/hysteria.ts: - HysteriaMasqueradeSchema covers the inbound-only masquerade wire shape: type ('proxy'|'file'|'string'), dir, url, rewriteHost, insecure, content, headers, statusCode. The three masquerade types cover the spectrum: reverse-proxy upstream, serve static files, or return a fixed string body. - HysteriaStreamSettingsSchema gains 3 inbound-side optional fields: protocol, udpIdleTimeout, masquerade. Outbound side is untouched (the legacy class accepted both wire shapes via the same struct). InboundFormModal.tsx: - New hysteria stream sub-form section in streamTab, gated by protocol === HYSTERIA. Fields: version (disabled, locked to 2), auth, udpIdleTimeout, masquerade Switch + nested type-Select with three conditional sub-blocks (proxy URL+rewriteHost+insecure, file dir, string statusCode+body+headers). - onValuesChange cascade: switching TO hysteria seeds streamSettings with the hysteria branch (forcing network='hysteria' + TLS); switching AWAY from hysteria snaps back to TCP so the standard network selector has a valid starting point. masquerade headers use the HeaderMapEditor v1 component. * 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. * 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. * feat(frontend): round-trip XHTTP advanced fields in outbound link parser Pick up xPaddingBytes, scMaxEachPostBytes, scMinPostsIntervalMs, uplinkChunkSize, and noGRPCHeader from both vmess:// JSON and the URL query-param parsers (vless/trojan). The advanced xmux/padding-obfs/ reality-shortId knobs still wait on a follow-up; this slice unblocks the common case where a phone-issued xhttp link carries non-default padding or post sizes. * feat(frontend): round-trip XHTTP padding-obfs + remaining advanced knobs Extract the XHTTP key-mapping into typed string/number/bool key arrays applied by both the URL query-param branch and the vmess JSON branch. The parser now covers xPaddingObfsMode + xPaddingKey/Header/Placement/ Method, sessionKey/seqKey/uplinkData{Placement,Key}, noSSEHeader, scMaxBufferedPosts, scStreamUpServerSecs, serverMaxHeaderBytes, and uplinkHTTPMethod alongside the previous five XHTTP fields. Two new round-trip tests cover the padding-obfs surface on both link forms. * 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. * docs(frontend): record FinalMaskForm rewrite + hookup in status doc Mainline migration goal — replace class-based xray models with Zod schemas as the single source of truth + drive all forms through AntD `Form.useForm` + `antdRule(schema.shape.X)` — is complete. Remaining items are incremental polish. * fix(frontend): Phase 2 Inbound form reactivity bugs (B1-B9, consolidated) A run of resets dropped the per-bug commits 1401d833 / 5b1ae450 / 5bce0dc5 / 4007eec7. Re-landing all fixes against the same files in one commit to avoid another rebase-style drop. B1 — Transmission Select / External Proxy + Sockopt switches didn't react after click. AntD 6.4.3 Form.useWatch on nested paths doesn't re-fire reliably after `setFieldValue('streamSettings', cleaned)` on the parent. Bound Transmission via `name={['streamSettings', 'network']}` and wrapped the two switches in `<Form.Item shouldUpdate>` blocks that read state via getFieldValue. B2 — Security regressed from `Radio.Group buttonStyle="solid"` to a Select dropdown, and disable state didn't refresh because tlsAllowed/ realityAllowed were derived at the top of the component. Restored Radio.Button group and moved canEnableTls/canEnableReality evaluation inside the shouldUpdate render prop. B3 — Advanced tab "All" sub-tab was missing. Added it as the first item with a new AdvancedAllEditor that round-trips top-level fields + the three nested slices on edit. B4 — Advanced tab title/subtitle and per-section help text were gone. Wrapped the Tabs in the existing `.advanced-shell` / `.advanced-panel` structure and restored the `.advanced-editor-meta` help under each sub-tab using existing i18n keys. B5 — TLS / Reality sub-forms didn't render when selecting tls or reality on the Security tab. The `{security === 'tls' && ...}` and `{security === 'reality' && ...}` conditionals used a stale top-level useWatch value. Wrapped both in <Form.Item shouldUpdate> blocks that read `security` via getFieldValue. B6 — Advanced JSON editors stale after Stream/Sniffing changes. The editors seeded text via lazy useState and AntD Tabs renders all panes upfront, so the Advanced tab was already mounted with stale data. Both AdvancedSliceEditor and AdvancedAllEditor now subscribe via Form.useWatch and re-sync the text buffer when the watched JSON differs from a lastEmitRef (the serialization at the moment of our own last accepted write). User typing doesn't trigger re-sync because setFieldValue updates lastEmitRef too. (A prior attempt added `destroyOnHidden` to the outer Tabs but broke conditional tab items when the unmounted Form.Item for `protocol` lost its value — abandoned in favor of useWatch reactivity.) B7 — HeaderMapEditor + button did nothing. addRow() appended a blank {name:'', value:''} row, but commit() filtered it via rowsToMap before reaching the form, so AntD saw no change and didn't re-render. The editor now keeps a local rows state so blank rows survive during editing; only filled rows are emitted to onChange. B9 — Sniffing destOverride defaults (HTTP/TLS/QUIC/FAKEDNS) were not pre-checked on a fresh Add Inbound. buildAddModeValues() seeded sniffing: {} which left destOverride undefined. Now seeds with SniffingSchema.parse({}) so the Zod defaults populate. * fix(frontend): FinalMaskForm TCP Mask sub-forms + Advanced JSON wrap (B10/B11) B10 — FinalMaskForm TCP Mask: after adding a mask and picking a Type (Fragment/Header Custom/Sudoku), the type-specific sub-forms didn't render. TcpMaskItem read `type` via Form.useWatch on a path inside Form.List, which doesn't re-fire reliably in AntD 6.4.3 — same root cause as the earlier B1/B2/B5 reactivity issues. Replaced with a <Form.Item shouldUpdate> wrapper that reads `type` via getFieldValue inside the render prop. B11 — Advanced sub-tabs (settings / streamSettings / sniffing) showed just the inner value (e.g. `{clients:[],decryption:"none",...}`), but the legacy modal wrapped each slice with its key envelope (e.g. `{settings:{...}}`) so the JSON matches the wire shape's slice and round-trips cleanly from copy-pasted inbound configs. Added a `wrapKey` prop to AdvancedSliceEditor that wraps/unwraps the value on render/write; the three sub-tabs now pass settings / streamSettings / sniffing as their wrapKey. * fix(frontend): import InboundFormModal.css so layout classes apply (B12) The file InboundFormModal.css existed but was never imported, so every class in it had no effect — including: - .vless-auth-state — the "Selected: <auth>" caption next to the X25519/ ML-KEM/Clear button row stayed inline next to Clear instead of display:block beneath the row - .advanced-shell / .advanced-panel — the Advanced tab's header / panel framing was missing - .advanced-editor-meta — the per-section help text under each Advanced sub-tab had no spacing - .wg-peer — wireguard peer rows had no top margin Add a side-effect import of the CSS file at the top of the modal. No other change needed; the legacy modal must have either imported it or had a global import that the new modal didn't inherit. * fix(frontend): FinalMaskForm relative paths + network-switch defaults (B13/B14) B13 — FinalMaskForm used absolute paths like ['streamSettings', 'finalmask', 'tcp', 0, 'type'] for Form.Item names inside Form.List render props. AntD's Form.List prefixes Form.Item names with the list's own name, so the actual storage path became ['streamSettings', 'finalmask', 'tcp', 'streamSettings', 'finalmask', 'tcp', 0, 'type'] — total nonsense. Symptoms: Type Select didn't show the 'fragment' default after add(), and the sub-form for the picked type never rendered (Fragment/Sudoku/HeaderCustom). Rewrote FinalMaskForm to use RELATIVE names inside every Form.List context (TCP/UDP outer list + nested clients/servers/noise inner lists). Added a `listPath` prop on the items so the shouldUpdate guard and the side-effect setFieldValue calls (resetting `settings` when type changes) can still address the absolute path; the displayed Form.Items use the relative form (`[fieldName, 'type']`). Replaced top-level Form.useWatch on nested paths with <Form.Item shouldUpdate> blocks reading via getFieldValue, same pattern as the earlier B5 fix — Form.useWatch on paths inside Form.List doesn't re-fire reliably in AntD 6.4.3. B14 — Switching network (KCP, WS, gRPC, XHTTP, ...) seeded the new XSettings blob as `{}` so every field showed as empty. The legacy `newStreamSlice` populated mtu=1350, tti=20, etc. Restored those defaults in onNetworkChange and seeded the initial tcpSettings.header in buildAddModeValues so even the default TCP state shows the HTTP-camouflage Switch in the correct off state instead of an undefined header object. * fix(frontend): inbound TCP HTTP camouflage drops request fields + KCP UI field rename (B15/B16) B15 — Inbound TCP HTTP camouflage exposed Host / Path / Method / Version / request-headers inputs. Per Xray docs (https://xtls.github.io/config/transports/raw.html#httpheaderobject), the `request` object is honored only by outbound proxies; the inbound listener reads `response`. Those inputs were writing dead data the server ignored. Removed them from the inbound modal; only Response {version, status, reason, headers} remain. The toggle still seeds an empty request object so the wire shape stays valid against the schema. B16 — KCP Uplink / Downlink inputs bound to non-existent form fields `upCap` / `downCap`, while the schema (and wire) use `uplinkCapacity` / `downlinkCapacity`. Renamed the Form.Items to the schema names so defaults populate and saves persist. Also corrected newStreamSlice('kcp') to seed the four KCP defaults (uplinkCapacity / downlinkCapacity / cwndMultiplier / maxSendingWindow) — the missing two were why "CWND Multiplier" and "Max Sending Window" still showed empty after switching to KCP. * fix(frontend): seed full Zod-schema defaults for stream slices + QUIC params (B17) XHTTP showed blank Selects for Session Placement / Sequence Placement / Padding Method / Uplink HTTP Method (and several other knobs). Those fields have a literal "" (empty string) value in the schema, which the Select renders as "Default (path)" / "Default (repeat-x)" / etc. The form field was `undefined`, not `""`, so the Select showed blank instead of the labelled default option. newStreamSlice in InboundFormModal hand-rolled per-network seed objects with only a handful of fields. Replaced with {Tcp,Kcp,Ws,Grpc,HttpUpgrade,XHttp}StreamSettingsSchema.parse({}) so every default declared in the schema populates the form on network switch. Same change in buildAddModeValues for the initial TCP state. QUIC Params (FinalMaskForm) had the same shape on a smaller scale — defaultQuicParams() only seeded congestion + debug + udpHop. The schema's other fields are .optional() (no Zod default) so a schema parse won't help. Hard-coded the xray-core / hysteria recommended values (maxIdleTimeout 30, keepAlivePeriod 10, brutalUp/Down 0, maxIncomingStreams 1024, four window sizes) so the InputNumber controls render with usable starting values instead of blank. * fix(frontend): forceRender all tabs so fields register at modal open (B18) AntD Tabs with the `items` API lazy-mounts inactive tab panes by default. The Form.Items inside an unvisited tab never register, so: - Form.useWatch on a parent path (e.g. 'sniffing') returns a partial view containing only registered children. Until the user clicked the Sniffing tab, Advanced > Sniffing JSON showed `{sniffing: {}}` instead of the full default object set by setFieldsValue. - After visiting the Sniffing tab once, the `sniffing.enabled` Form.Item registered, so useWatch suddenly returned `{enabled: false}` — still partial, because the rest of the sniffing children only register when their Form.Items mount in conditional sub-sections. Setting `forceRender: true` on every tab item forces all tab panes to mount at modal open. Every Form.Item registers immediately; the watch result reflects the full form value seeded by buildAddModeValues. This also likely resolves the earlier "Invalid discriminator value" error on submit, which surfaced when streamSettings had an unregistered security field whose Form.Item hadn't mounted yet. * 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") * 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. * fix(frontend): inboundFromDb fills Zod defaults for stream + settings Smoke-testing the new inboundFromDb helper surfaced two regressions that the strict lib/xray link generators expose when fed raw DB streamSettings without per-network sub-keys. 1. genVlessLink / genTrojanLink crash on `stream.tcpSettings.header` when streamSettings lacks `tcpSettings` (true for slim list rows and for handcrafted minimal-JSON inbounds). The legacy Inbound.fromJson chain populated TcpStreamSettings via its own constructor; the new helper now does the same by parsing the raw <network>Settings sub-object through the matching Zod schema and merging schema defaults onto whatever the DB stored. 2. genVlessLink writes `encryption=undefined` into the share URL when settings lacks the `encryption: 'none'` literal that vless wire JSON normally carries. Fixed by running raw settings through InboundSettingsSchema.safeParse() to populate per-protocol defaults (encryption, decryption, fallbacks, etc.) the same way the legacy class fromJson chain did. Same pattern applied to security branch (tls/realitySettings). Tests: src/test/inbound-from-db.test.ts covers - JSON-string / object / empty settings coercion - genInboundLinks vless (TCP/none, with encryption=none) - genWireguardConfigs + genWireguardLinks peer fanout - genAllLinks trojan with TLS sub-defaults applied - protocol-capability helpers with raw shapes - getInboundClients across vless/SS-single/non-client protocols 296/296 pass. * fix(frontend): QUIC udpHop.interval is a range string, not a number (B19) User report: "streamSettings.finalmask.quicParams.udpHop.interval: Invalid input: expected string, received number". Three-part fix: - FinalMaskForm: Hop Interval input changed from InputNumber to Input with "e.g. 5-10" placeholder. xray-core spec says interval is a range string like '5-10' (seconds between min-max hops), not a single number. - FinalMaskForm: defaultQuicParams() seeds interval: '5-10' instead of the broken `interval: 5`. - QuicUdpHopSchema: preprocess coerces number → string for legacy DB rows that were written by the now-fixed buggy UI. Stops the load-time validation crash on existing inbounds. Tests still 296/296. * fix(frontend): outbound link parser handles extra/fm/x_padding_bytes (B20) User-reported vless share link with full xhttp + reality + finalmask config failed to round-trip on outbound import. The inbound link generator emits three payloads the outbound parser was ignoring: 1. `extra=<json>` — bundles advanced xhttp knobs (xPaddingBytes, scMaxEachPostBytes, scMinPostsIntervalMs, padding-obfs keys, etc.). applyXhttpStringFromParams now JSON.parses this and merges the fields into xhttpSettings via the same JSON-branch logic used by vmess. 2. `x_padding_bytes=<range>` — snake_case alias the inbound emits alongside the camelCase form. Now applied before camelCase so explicit `xPaddingBytes` URL params still win. 3. `fm=<json>` — full finalmask object including quicParams.udpHop and tcp/udp mask arrays. New applyFinalMaskParam attaches the decoded object to streamSettings.finalmask. Wired into both parseVlessLink and parseTrojanLink. Tests: - Real B20 link parses with xhttp + reality + finalmask all populated - Precedence: camelCase URL > extra JSON > snake_case alias > default - Malformed extra JSON falls through without crashing the parser 300/300 pass. * 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. * 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. * refactor(frontend): extract fillStreamDefaults to shared helper Move the network/security schema-default filler out of inbound-from-db.ts into stream-defaults.ts so other consumers can reuse it without dragging in the DBInbound-specific code path. * fix(frontend): derive QUIC/UDP-hop switch state from data presence (B22) The QUIC Params and UDP Hop toggles previously persisted as separate boolean flags (enableQuicParams / hasUdpHop) which weren't part of the xray wire format and weren't restored when a config was pasted into the modal. Use data presence as the single source of truth: the switch is on iff the corresponding sub-object exists. Switching off clears it back to undefined. * 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. * 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. * 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. * test(frontend): golden fixtures for DNS, Balancer, Rule schemas Adds JSON fixtures under golden/fixtures/{dns,dns-server,balancer,rule} plus three vitest files that parse them through the new schemas and snapshot the result. dns/: minimal (servers as strings) + full (every top-level field plus hosts with geosite/domain/full prefixes and 5 mixed string/object servers covering fakedns, localhost, https://, tcp://, quic+local://). dns-server/: full (every DnsServerObject field) + legacy-expectips (asserts the z.preprocess that migrates the legacy `expectIPs` key into the canonical `expectedIPs`). balancer/: random-minimal (default strategy by omission), roundrobin, leastping, leastload-full (covers all StrategySettings fields and both regexp=true|false costs). rule/: minimal, full (exercises every RuleObject field including localPort, localIP, process aliases like `self/`, all four protocol enum values, ip negation `!geoip:`, attrs with regexp value, and the WebhookObject with deduplication+headers), balancer-routed (uses balancerTag instead of outboundTag), port-number (port as a number to prove the union(number,string) accepts both). * fix(frontend): serialize bulk client delete + drop deprecated Alert.message useClients.removeMany was firing all DELETEs in parallel via Promise.all. The 3x-ui backend mutates a single config JSON per request (read / modify / write), so 20 concurrent deletes raced on the same file: every request reported success, but only the last writer's copy stuck — about half the selected clients reappeared after the toast. Replace the parallel fan-out with a sequential for-of loop so each delete sees the committed state of the previous one. The trade-off is total latency (20 * ~250ms = ~5s) which is the correct behavior until the backend grows a proper /bulkDel endpoint. Also rename the Alert `message` prop to `title` in ClientBulkAdjustModal to clear the AntD v6 deprecation warning. * feat(clients): server-side bulk create/delete with per-inbound batching Replace the panel-side fan-out (Promise.all of single /add and /del calls) that raced on the shared inbound config and capped throughput at roughly one round-trip per client. New endpoints batch the work on the server: - POST /panel/api/clients/bulkDel { emails, keepTraffic } - POST /panel/api/clients/bulkCreate [ {client, inboundIds}, ... ] BulkDelete groups emails by inbound and performs a single read-modify-write per inbound (one JSON parse, one marshal, one Save) instead of N. Per-row DB cleanups (ClientInbound, ClientTraffic, InboundClientIps, ClientRecord) are batched with WHERE...IN queries. Per-email failures are reported via Skipped[] and processing continues. BulkCreate iterates payloads sequentially through the same Create path single-add uses, so heterogeneous batches (different inboundIds, plans) remain valid in one round-trip. Frontend bulkDelete/bulkCreate hooks parse the new response shape ({ deleted|created, skipped[] }) and the bulk-add modal now posts a single request instead of fanning out emails. * perf(clients): batch BulkAdjust per inbound, skip no-op xray calls on local Same per-inbound batching strategy as BulkDelete. The previous code called Update once per email, which itself looped through each inbound the client belonged to — reparsing the same settings JSON, calling RemoveUser+AddUser on xray, and running SyncInbound for every single email. For 200 emails in one inbound that's 200 JSON read/write cycles and 400 xray runtime calls. The new BulkAdjust groups emails by inbound and per inbound: - locks once, reads settings JSON once - mutates expiryTime/totalGB in place for every target client - writes the inbound and runs SyncInbound once ClientTraffic rows are updated with a single per-email query at the end (values differ per client so they can't be folded into one statement). For local-node inbounds the xray runtime calls are skipped entirely. The AddUser payload only contains email/id/security/flow/auth/password/ cipher — none of which change in an adjust — so RemoveUser+AddUser was a no-op that briefly flapped active users. Limit enforcement is driven by the panel's traffic loop reading ClientTraffic, not by xray-core. For remote-node inbounds rt.UpdateUser is preserved so the remote panel receives the new totals/expiry. Skip+report semantics match BulkDelete: any per-email error leaves that email's record/traffic untouched and is returned in Skipped[]. * refactor(backend): retire hysteria2 as a top-level protocol Hysteria v2 is not a separate xray protocol — it is plain "hysteria" with streamSettings.version = 2. The frontend already dropped hysteria2 from the protocol enum in 5a90f7e3; the backend was still carrying the literal as a compat alias. Removed: - model.Hysteria2 constant - model.IsHysteria helper (only callers were buildProxy + genHysteriaLink) - TestIsHysteria - "hysteria2" from the Inbound.Protocol validate oneof enum - All `case model.Hysteria, model.Hysteria2:` and `case "hysteria", "hysteria2":` branches across client.go, inbound.go, outbound.go, xray.go, port_conflict.go, xray/api.go, subService.go, subJsonService.go, subClashService.go - Stale #4081 comments Kept (correctly — these are client-side URI/config schemes that are independent of the xray protocol type): - hysteria2:// share-link URI in subService.genHysteriaLink - "hysteria2" Clash proxy type in subClashService.buildHysteriaProxy - Comments referring to Hysteria v2 as a transport version Note: this change does not include a DB migration. Existing rows with protocol = 'hysteria2' will fall through to the default switch arms after upgrade. A separate `UPDATE inbounds SET protocol = 'hysteria' WHERE protocol = 'hysteria2'` is required for installs that still hold legacy data. * refactor(frontend): retire all AntD + Zod deprecations Swept the codebase for @deprecated APIs using a one-off type-aware ESLint config (eslint.deprecated.config.js) and fixed every hit: - 78 instances of `<Select.Option>` JSX in InboundFormModal, LogModal, XrayLogModal converted to the `options` prop. - Zod's `z.ZodTypeAny` (deprecated for `z.ZodType` in zod v4) replaced in _envelope.ts, zodForm.ts, zodValidate.ts, and inbound-form-adapter.ts. - Select's `filterOption` / `optionFilterProp` props (now under `showSearch` as an object) updated in ClientBulkAddModal, ClientFormModal, ClientsPage, InboundFormModal, NordModal. - `Input.Group compact` swapped for `Space.Compact` in FinalMaskForm. - Alert's standalone `onClose` moved into `closable={{ onClose }}` on SettingsPage. - `document.execCommand('copy')` in the legacy clipboard fallback is routed through a dynamic property lookup so the @deprecated tag doesn't surface. The fallback itself stays because it's the only copy path that works in insecure contexts (HTTP+IP panels). The dropped ClientFormModal.css was already unimported. eslint.deprecated.config.js loads the type-aware ruleset and turns everything off except `@typescript-eslint/no-deprecated`, so future scans are a single command: npx eslint --config eslint.deprecated.config.js src Not wired into `npm run lint` because typed linting roughly triples the run time. Verified clean: typecheck, lint, and the deprecated scan all 0 warnings. * feat(clients): show comment under email in the Client column The clients table's Client cell already stacks email + subId; add the admin comment as a third muted line so notes like "VIP" or "friend of X" are visible in the list view without opening the info modal. Renders only when set, so rows without a comment look unchanged. * docs(frontend): refresh README + simplify deprecated-scan config README rewrite reflects the post-Zod-migration state: - 3 Vite entries (index/login/subpage), not "one per panel route" - New folders: schemas/, lib/xray/, generated/, test/, layouts/ - Scripts table covers test/gen:api/gen:zod alongside the existing dev/build/lint/typecheck - New sections on the Zod schema tree, the three validation layers, the unified Form.useForm + antdRule pattern, and the golden fixture testing setup - "Adding a new page" updated to reflect that most additions are just react-router entries in routes.tsx, not new Vite bundles - Explicit note that `@deprecated` in the prose is a JSDoc tag, not a shell command — comes with the exact one-line npx invocation eslint.deprecated.config.js trimmed: dropping the recommendedTypeChecked spread + the ~28 rule overrides that came with it. The config now wires the @typescript-eslint and react-hooks plugins manually and enables exactly one rule (`@typescript-eslint/no-deprecated`). 45 lines → 30, same output: zero false-positives, zero noise, zero deprecations on the current tree. * chore(frontend): bump deps + refresh lockfile `npm update` within the existing semver ranges, plus a Vite bump the user explicitly accepted: - vite 8.0.13 → 8.0.14 (exact pin kept) - dayjs 1.11.20 → 1.11.21 - i18next 26.2.0 → 26.3.0 - typescript-eslint 8.59.4 → 8.60.0 - @rc-component/table + a handful of other transitive antd deps resolved to newer patch versions in the lockfile The earlier 8.0.13 pin was carried over from an esbuild dep-optimizer regression that broke vue-i18n in Vite 8.0.14 dev mode. This codebase uses react-i18next, doesn't hit the same chunking edge case, and `npm run dev` was smoked clean on 8.0.14 before accepting the bump. * feat(clients): compact link + inbound rows in the info modal and table ClientInfoModal — Copy URL section reskinned: - Each link is a single row: [PROTOCOL] [remark] [copy] [QR] instead of a card with the raw 200-char URL printed inline - Remark is parsed per-protocol — VMess pulls it from the base64-JSON `ps` field, the rest from the `#fragment` - The row title strips the client email suffix so the same string isn't repeated three times in the modal; the QR popover still uses the full remark (it's the QR's own name for the download file) - QR button opens an inline Popover with the existing QrPanel, size 220, destroyed on close - Subscription section uses the same row layout (SUB / JSON tags, clickable subId, copy + QR actions) - New per-protocol Tag colors so the protocol is identifiable at a glance ClientInfoModal — Attached inbounds + ClientsPage table column: - Chip format changed from `${remark} (${proto}:${port})` to just `${proto}:${port}` — when an admin attaches 5 inbounds to one client the remark was repeated 5 times and wrapped onto two lines - Only the first inbound chip is shown; the rest collapse into a `+N` chip that opens a Popover with the full list (remark included). INBOUND_CHIP_LIMIT = 1 - Per-protocol Tag colors - Tooltip on each chip shows the full `${remark} (${proto}:${port})` - Table column pinned to width: 170 so the row doesn't reserve the old 300px of whitespace next to the compact chip Comment row in the info table is always shown now (renders `-` when unset) so the layout doesn't jump per-client. VmessSecuritySchema gets a preprocess pass that maps legacy `security: ""` (persisted on pre-enum-lock VMess inbounds) back to `'auto'`. z.enum's `.default()` only fires on a missing field, not on an empty string — without this, old rows fail validation with "expected one of aes-128-gcm|chacha20-poly1305| auto|none|zero". `z.infer` is taken from the raw enum so the inferred type stays the union, not `unknown`. i18n adds a `more` key (en-US + fa-IR) used by the overflow chip label. * fix(xray): heal shadowsocks per-client method across all start paths xray-core's multi-user shadowsocks insists the per-client `method` matches the inbound's top-level cipher exactly for legacy ciphers, and is empty for 2022-blake3-*. The previous code (xray.go) copied `Client.Security` into the per-client `method` blindly, so a multi-protocol client created with the VMess default `"auto"` poisoned the SS config with `method: "auto"` → "unsupported cipher method: auto". Fix in two parts: - GetXrayConfig no longer projects `Client.Security` into the SS entry; the inbound's top-level method is now the single source of truth. - HealShadowsocksClientMethods moves to `database/model` and is invoked from `Inbound.GenXrayInboundConfig`, so the runtime add/update path (runtime.AddInbound) is normalised in addition to the full-restart path. For legacy ciphers heal now overwrites mismatched per-client methods rather than preserving them, so stale DB rows are also healed. * feat(sub): compact subscription rows with per-link email + PQ QR hide Mirror the ClientInfoModal redesign on the public SubPage so the subscription viewer reads as a tight `[PROTO] [remark] [copy] [QR]` row per link instead of raw URL cards. - subService.GetSubs now returns the per-link email list alongside the links, threaded through subController and BuildPageData into the `emails` field on subData (env.d.ts updated). Public links.go is updated to ignore the new return. - SubPage strips the client email from each row title using the matched per-link email (same trimEmail behaviour as the modal), and hides the QR button for post-quantum links (`pqv=`, `mlkem768`, `mldsa65`) since the encoded URL won't fit in a single QR. * feat(clients): hide QR for post-quantum links in client info modal Post-quantum keys (mldsa65 / ML-KEM-768) blow the encoded URL past what a single QR can hold. Detect them by the markers VLESS share links actually carry — `pqv=<base64>` for mldsa65Verify and `encryption=mlkem768x25519plus.*` for ML-KEM-768 — and drop the QR button for those rows. Copy still works. * fix(schemas): widen VLESS decryption/encryption to accept PQ values The post-quantum auth blocks (ML-KEM-768, X25519) populate `settings.decryption` / `settings.encryption` with values like `mlkem768x25519plus.<base64>` and `xchacha20-poly1305.aead.x25519`, but the schema pinned both fields to z.literal('none') so saving an inbound after picking "ML-KEM-768 auth" failed with `Invalid input: expected "none"`. Relax both fields (inbound + outbound + outbound form) to z.string().min(1) keeping the 'none' default. xray-core does its own validation server-side so a string check at the form boundary is enough. * feat(sub): clash row + reorganise SubPage around Subscription info ClientInfoModal: - Add a Clash / Mihomo row to the subscription section, gated on subClashEnable + subClashURI from /panel/setting/defaultSettings. Defaults payload schema is widened to carry subClashURI/subClashEnable. SubPage: - Drop the rectangular QR-codes header that used to sit at the very top of the card. The subscription info table now leads, followed by Divider("Copy URL") + per-protocol link rows (already converted to the compact ClientInfoModal pattern), then a new Divider("Subscription") + compact rows for the SUB / JSON / CLASH URLs with copy + QR-popover actions. The apps dropdown row remains the footer. CSS clean-up: removed the now-unused .qr-row/.qr-col/.qr-box/.qr-code rules; kept .qr-tag and trimmed the info-table top gap. Added a .sub-link-anchor underline-on-hover style for the new URL rows. * fix(sub): multi-inbound traffic + trojan/hysteria userinfo + utf-8 vmess remark Three bugs surfaced by the new SubPage and the recent client-record refactor: - xray.ClientTraffic.Email is globally unique, so a multi-inbound client has exactly one traffic row attached to whichever inbound claimed it. Iterating inbound.ClientStats per inbound dedup-locked the first lookup to zero for clients that lived under any other inbound, so the SubPage info table read 0 B for all the multi- inbound subs. Replaced appendUniqueTraffic with a single AggregateTrafficByEmails(emails) helper that runs one WHERE email IN (?) over xray.ClientTraffic and folds the rows. GetSubs / SubClashService.GetClash / SubJsonService.GetJson all share it. - Trojan and Hysteria share-links embedded the raw password/auth into the userinfo (scheme://<value>@host) without percent-encoding, so passwords containing `/` or `=` (e.g., base64-with-padding) broke popular trojan clients with parse errors. Added encodeUserinfo() that wraps url.QueryEscape and rewrites the `+` (space) back to `%20` for parity with encodeURIComponent on the frontend; applied to trojan.password and hysteria.auth. Same fix on the frontend's genTrojanLink. - VMess link remarks ride inside a base64-encoded JSON payload, but the SubPage / ClientInfoModal parser used JSON.parse(atob(body)), which treats the binary string as Latin-1 and shreds any multi-byte UTF-8 sequence. Most visible on the emoji decorations (genRemark appends 📊/⏳), so a remark like `test-1.00GB📊` rendered as `test-1.00GBð…`. Routed through Uint8Array + TextDecoder('utf-8') so multi-byte codepoints survive. * feat(settings): drop email leg from default remark model Change the default remarkModel from "-ieo" to "-io" so a freshly installed panel composes share-link remarks from the inbound name + optional extra only, leaving out the client email. Existing panels keep whatever value they have saved — only fresh installs and fallback paths (parse failure, missing setting) pick up the new default. Touched everywhere the literal "-ieo" lived: the canonical default map, the two sub-package fallback constants, the four frontend defaults (model class, link generator, two inbound modals, useInbounds hook). Two snapshot tests regenerated and one obsolete "contains email" assertion in inbound-from-db.test.ts removed. To migrate an existing panel that wants the new behaviour, edit Settings → Remark Model and remove the email leg. * feat(sub): usage summary card + remark-email on QR popover labels SubPage now opens with a clear quota panel directly under the info table: large `used / total` numbers, gradient progress bar (green ≤ 75%, orange to 90%, red above), `remained` and `%` on the foot, plus a Tag chip for unlimited subscriptions and a coloured chip for days left until expiry (blue >3d, orange ≤3d, red on expiry). Driven entirely off existing subData fields — no backend changes. While the row title in the link list stays email-stripped (default remark model omits email now), the QR popover label folds it back in so the rendered QR card identifies the client unambiguously. Tag content becomes `<rowTitle>-<email>` in both SubPage and ClientInfoModal — the encoded link itself is unchanged. SubPage section order is now: info table → usage summary → SUB / JSON / CLASH endpoints → per-protocol Copy URL rows → apps row, so the most-glanceable status sits above the fold.
  • Просмотр сравнение для этих 3 коммитов »

4 часов назад

txlyre синхронизированные и удаленные ссылки feat/frontend-zod-validation на txlyre/3x-ui из зеркала

4 часов назад

txlyre синхронизированные коммиты с feat/frontend-zod-validation на txlyre/3x-ui из зеркала

  • 6c279d48fd feat(sub): clash row + reorganise SubPage around Subscription info ClientInfoModal: - Add a Clash / Mihomo row to the subscription section, gated on subClashEnable + subClashURI from /panel/setting/defaultSettings. Defaults payload schema is widened to carry subClashURI/subClashEnable. SubPage: - Drop the rectangular QR-codes header that used to sit at the very top of the card. The subscription info table now leads, followed by Divider("Copy URL") + per-protocol link rows (already converted to the compact ClientInfoModal pattern), then a new Divider("Subscription") + compact rows for the SUB / JSON / CLASH URLs with copy + QR-popover actions. The apps dropdown row remains the footer. CSS clean-up: removed the now-unused .qr-row/.qr-col/.qr-box/.qr-code rules; kept .qr-tag and trimmed the info-table top gap. Added a .sub-link-anchor underline-on-hover style for the new URL rows.
  • 87eaa79e5d fix(schemas): widen VLESS decryption/encryption to accept PQ values The post-quantum auth blocks (ML-KEM-768, X25519) populate `settings.decryption` / `settings.encryption` with values like `mlkem768x25519plus.<base64>` and `xchacha20-poly1305.aead.x25519`, but the schema pinned both fields to z.literal('none') so saving an inbound after picking "ML-KEM-768 auth" failed with `Invalid input: expected "none"`. Relax both fields (inbound + outbound + outbound form) to z.string().min(1) keeping the 'none' default. xray-core does its own validation server-side so a string check at the form boundary is enough.
  • 1752702f74 feat(clients): hide QR for post-quantum links in client info modal Post-quantum keys (mldsa65 / ML-KEM-768) blow the encoded URL past what a single QR can hold. Detect them by the markers VLESS share links actually carry — `pqv=<base64>` for mldsa65Verify and `encryption=mlkem768x25519plus.*` for ML-KEM-768 — and drop the QR button for those rows. Copy still works.
  • e7ac1fadaa feat(sub): compact subscription rows with per-link email + PQ QR hide Mirror the ClientInfoModal redesign on the public SubPage so the subscription viewer reads as a tight `[PROTO] [remark] [copy] [QR]` row per link instead of raw URL cards. - subService.GetSubs now returns the per-link email list alongside the links, threaded through subController and BuildPageData into the `emails` field on subData (env.d.ts updated). Public links.go is updated to ignore the new return. - SubPage strips the client email from each row title using the matched per-link email (same trimEmail behaviour as the modal), and hides the QR button for post-quantum links (`pqv=`, `mlkem768`, `mldsa65`) since the encoded URL won't fit in a single QR.
  • ad8d58c2b6 fix(xray): heal shadowsocks per-client method across all start paths xray-core's multi-user shadowsocks insists the per-client `method` matches the inbound's top-level cipher exactly for legacy ciphers, and is empty for 2022-blake3-*. The previous code (xray.go) copied `Client.Security` into the per-client `method` blindly, so a multi-protocol client created with the VMess default `"auto"` poisoned the SS config with `method: "auto"` → "unsupported cipher method: auto". Fix in two parts: - GetXrayConfig no longer projects `Client.Security` into the SS entry; the inbound's top-level method is now the single source of truth. - HealShadowsocksClientMethods moves to `database/model` and is invoked from `Inbound.GenXrayInboundConfig`, so the runtime add/update path (runtime.AddInbound) is normalised in addition to the full-restart path. For legacy ciphers heal now overwrites mismatched per-client methods rather than preserving them, so stale DB rows are also healed.
  • Просмотр сравнение для этих 34 коммитов »

12 часов назад

txlyre синхронизированные коммиты с feat/frontend-zod-validation на txlyre/3x-ui из зеркала

  • bfdaf7a8f8 docs(frontend): record FinalMaskForm rewrite + hookup in status doc Mainline migration goal — replace class-based xray models with Zod schemas as the single source of truth + drive all forms through AntD `Form.useForm` + `antdRule(schema.shape.X)` — is complete. Remaining items are incremental polish.
  • 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.
  • 34590dc327 feat(frontend): round-trip XHTTP padding-obfs + remaining advanced knobs Extract the XHTTP key-mapping into typed string/number/bool key arrays applied by both the URL query-param branch and the vmess JSON branch. The parser now covers xPaddingObfsMode + xPaddingKey/Header/Placement/ Method, sessionKey/seqKey/uplinkData{Placement,Key}, noSSEHeader, scMaxBufferedPosts, scStreamUpServerSecs, serverMaxHeaderBytes, and uplinkHTTPMethod alongside the previous five XHTTP fields. Two new round-trip tests cover the padding-obfs surface on both link forms.
  • 2f1a146f45 feat(frontend): round-trip XHTTP advanced fields in outbound link parser Pick up xPaddingBytes, scMaxEachPostBytes, scMinPostsIntervalMs, uplinkChunkSize, and noGRPCHeader from both vmess:// JSON and the URL query-param parsers (vless/trojan). The advanced xmux/padding-obfs/ reality-shortId knobs still wait on a follow-up; this slice unblocks the common case where a phone-issued xhttp link carries non-default padding or post sizes.
  • 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.
  • Просмотр сравнение для этих 71 коммитов »

20 часов назад

txlyre синхронизированные коммиты с feat/frontend-zod-validation на txlyre/3x-ui из зеркала

  • 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.
  • Просмотр сравнение для этих 3 коммитов »

1 день назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 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.
  • Просмотр сравнение для этих 2 коммитов »

1 день назад

txlyre синхронизированные коммиты с feat/frontend-zod-validation на txlyre/3x-ui из зеркала

  • 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 [email protected] bundles three deps whose declared peer ranges predate React 19: [email protected] (peer 15-18) [email protected] (peer 15-18, unmaintained) [email protected] (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.
  • Просмотр сравнение для этих 10 коммитов »

1 день назад

txlyre синхронизированные новые ссылки feat/frontend-zod-validation к txlyre/3x-ui из зеркала

1 день назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 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) * :sparkles: Introduce extended XHTTP and external proxy settings * :sparkles: Add custom SNI for proxy * :sparkles: 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.
  • Просмотр сравнение для этих 4 коммитов »

2 дней назад

txlyre синхронизированные коммиты с v3.1.0 на txlyre/3x-ui из зеркала

3 дней назад

txlyre синхронизированные новые ссылки v3.1.0 к txlyre/3x-ui из зеркала

3 дней назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 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.
  • 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 bf0754d21e44e8645930b87728e5355248c3c081. * fix(frontend): resolve lazy chunk URLs against runtime base path Vite's default chunk-preload helper prepends a hardcoded `/` to asset filenames, so dynamic chunk preloads always 404 when the panel is served under a non-root webBasePath (e.g. /CxuVUNgm5mRLmjPhp3/). Use experimental.renderBuiltUrl to embed window.X_UI_BASE_PATH (injected by dist.go) as the runtime prefix, so __vite__mapDeps emits URLs like `<basePath>assets/<file>` regardless of where the dist is mounted.
  • 2ed85aadda v3.1.0
  • Просмотр сравнение для этих 8 коммитов »

3 дней назад

txlyre синхронизированные и удаленные ссылки bash на txlyre/3x-ui из зеркала

3 дней назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • c5b71041d3 Reduce list-page payloads with slim/paged endpoints (#4500) * perf(inbounds): slim list payload + lazy hydrate for row actions Adds GET /panel/api/inbounds/list/slim that returns the same list shape but strips every per-client field besides email/enable/comment from settings.clients[] and skips UUID/SubId enrichment on ClientStats. The inbounds page only reads those three to compute its client counters and badges, so the slim variant trims tens of bytes per client (uuid, password, flow, security, totalGB, expiryTime, limitIp, tgId, ...). On a panel with thousands of clients this is the dominant load-time cost. Detail flows (edit / info / qr / export / clone) call /get/:id through a new hydrateInbound helper before opening — the slim list view never needs the secrets it doesn't render. * perf(clients): server-side pagination + slim row payload Adds GET /panel/api/clients/list/paged that filters, sorts, and paginates on the server, returns a slim row shape (drops uuid/password/auth/flow/ security/reverse/tgId per client), and includes a stable summary (total, active, online[], depleted[], expiring[], deactive[]) computed across the full DB row set so the dashboard cards don't change as the user paginates or filters. Page size capped at 200. useClients now exposes { clients (current page), total, filtered, query, setQuery, summary, hydrate }. ClientsPage feeds its filter/sort/page state into setQuery via a single effect, debounces search by 300ms, and hydrates the full client record via /get/:email before opening edit/info/ qr modals. Local filter/sort logic and the all-clients summary memo are gone. On a 2000-client panel this turns the initial response from ~MB to ~25 row slice (~10s of KB) and removes the all-client parse cost from every refresh. * perf(settings): use /inbounds/options for LDAP tag picker The General settings tab only needs each inbound's tag/protocol/port to fill a dropdown but was calling /panel/api/inbounds/list which ships the full settings JSON with every embedded client. Switched it to /options and added Tag to the projection. On a panel with thousands of clients this drops the General-tab load payload from megabytes to a tiny per-inbound row each. * perf(clients): de-duplicate options + paged list fetches Two issues caused each clients-page load to fire its requests twice: 1. setQuery in the hook took whatever object the consumer passed and stored it as-is. The consumer (ClientsPage) constructs a new object literal in an effect, so even when nothing actually changed the ref was new — the hook's useEffect saw a new query and re-fetched. Wrapped setQuery with a shallow value compare so identical params are a no-op. 2. The picker /inbounds/options fetch was bundled into refresh() with a length==0 guard, but the two back-to-back refreshes both saw an empty inbounds array (the first hadn't resolved yet) so both fired the request. Moved the options fetch into its own one-shot effect. * perf(inbounds): share nodes list with form modal instead of refetching InboundsPage and InboundFormModal both called useNodes() — each instance maintains its own state and fires its own /panel/api/nodes/list fetch on mount. Since the modal is always rendered (open or not), every page load hit the endpoint twice. Threaded nodes from the page through an availableNodes prop on the form modal so they share one fetch. * docs(api): register /clients/list/paged endpoint TestAPIRoutesDocumented was failing because the new paginated clients endpoint added in this branch wasn't listed in endpoints.js.
  • 9c60ed7ea8 Bulk extend client expiry / traffic + clients page polish (#4499) * chore(sub): drop unused getFallbackMaster projectThroughFallbackMaster fully supersedes it for both panel-tracked and legacy unix-socket fallbacks. * feat(clients): bulk extend expiry / traffic for selected clients Adds POST /panel/api/clients/bulkAdjust which shifts ExpiryTime by addDays and TotalGB by addBytes for every email in one request. The endpoint is wired into the clients page through a new ClientBulkAdjustModal that opens from the existing multi-select toolbar. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field so bulk extend never accidentally converts an unlimited client to a limited one. Negative values are allowed for refunds / corrections. Translations added for all 13 locales. * fix(db): silence GORM record-not-found spam in debug mode getSetting handles ErrRecordNotFound via database.IsNotFound and falls back to defaults, but GORM's Default logger still logs each miss as an error. With periodic jobs reading unset keys (xrayTemplateConfig, externalTrafficInformEnable) the panel log flooded thousands of times. Switch to a logger.New with IgnoreRecordNotFoundError=true so legitimate slow-query and SQL traces still surface in debug mode. * fix(clients): include inboundsById in columns memo deps Without it, the table's first paint captured an empty inboundsById and rendered each attached inbound as #<id>. Once a sort/filter forced the memo to rebuild it self-corrected, hence the visible flicker on reload. * fix(clients): handle delayed-start expiry in bulk adjust Negative ExpiryTime encodes a delay duration (magnitude = ms until the trial begins on first use). Adding positive addDays was simply arithmetically added, so e.g. a -7d delay + 30d turned into +23d since epoch (1970), making the client instantly expired. Branch on sign now: positive ExpiryTime extends additively, negative extends by subtracting so the value stays negative (more delay). Cross-sign reductions are skipped with an explicit reason instead of silently corrupting the field. * fix(clients): step traffic input by 1 GB instead of 0.1 The +/- buttons on the Total Sent/Received field nudged in 0.1 GB increments which is too granular for typical use. Set step=1 so each press moves a whole GB; users can still type decimal values directly. * fix(inbounds): step Total Flow input by 1 GB instead of 0.1 Matches the same nudge fix applied to the client form's Total Sent/Received field.
  • 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
  • Просмотр сравнение для этих 3 коммитов »

3 дней назад

txlyre синхронизированные коммиты с bash на txlyre/3x-ui из зеркала

  • a2c2c5f41d Merge branch 'main' into bash
  • 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
  • 237b7c898d Bump frontend deps: vue and vite Update frontend dependencies to pull in recent patch fixes and compatibility updates. package.json bumps vue from ^3.5.13 to ^3.5.34 and vite from ^8.0.11 to 8.0.13. package-lock.json updated accordingly (including postcss 8.5.14 → 8.5.15 and nanoid ^3.3.11 → ^3.3.12).
  • 7368359924 fix(xray): resolve relative log paths under panel log folder Rewrite relative `log.access`/`log.error` values in the Xray config to absolute paths under config.GetLogFolder() so Xray writes log files alongside the panel's logs regardless of the panel's working directory. Absolute paths, empty/"none" values, and nested relative paths are left untouched.
  • f2f5d584b3 fix(frontend): stack form fields on mobile in client/inbound/node modals Replace fixed :span values with responsive :xs="24" :md="N" so form rows collapse to a single column on narrow viewports instead of squeezing.
  • Просмотр сравнение для этих 5 коммитов »

3 дней назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 237b7c898d Bump frontend deps: vue and vite Update frontend dependencies to pull in recent patch fixes and compatibility updates. package.json bumps vue from ^3.5.13 to ^3.5.34 and vite from ^8.0.11 to 8.0.13. package-lock.json updated accordingly (including postcss 8.5.14 → 8.5.15 and nanoid ^3.3.11 → ^3.3.12).
  • 7368359924 fix(xray): resolve relative log paths under panel log folder Rewrite relative `log.access`/`log.error` values in the Xray config to absolute paths under config.GetLogFolder() so Xray writes log files alongside the panel's logs regardless of the panel's working directory. Absolute paths, empty/"none" values, and nested relative paths are left untouched.
  • f2f5d584b3 fix(frontend): stack form fields on mobile in client/inbound/node modals Replace fixed :span values with responsive :xs="24" :md="N" so form rows collapse to a single column on narrow viewports instead of squeezing.
  • Просмотр сравнение для этих 3 коммитов »

5 дней назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 3d1d75d65a Revert "build(deps-dev): bump vite from 8.0.13 to 8.0.14 in /frontend (#4487)" this version of vite have issue
  • 6e2816d035 fix(frontend): override browser default background color on autofilled login inputs (#4478)
  • 7fc7c14ac1 build(deps-dev): bump vite from 8.0.13 to 8.0.14 in /frontend (#4487) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.13 to 8.0.14. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.14/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.14 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Просмотр сравнение для этих 3 коммитов »

5 дней назад

txlyre синхронизированные коммиты с bash на txlyre/3x-ui из зеркала

  • 3452267302 Merge branch 'main' into bash
  • 3d1d75d65a Revert "build(deps-dev): bump vite from 8.0.13 to 8.0.14 in /frontend (#4487)" this version of vite have issue
  • b5cb069a07 Merge branch 'main' into bash
  • 6e2816d035 fix(frontend): override browser default background color on autofilled login inputs (#4478)
  • 7fc7c14ac1 build(deps-dev): bump vite from 8.0.13 to 8.0.14 in /frontend (#4487) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.13 to 8.0.14. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.14/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.14 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Просмотр сравнение для этих 14 коммитов »

5 дней назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 5f318f3b16 Add SockOpt.Mark and SockOpt.Interface parameters for Outbound stream (#4480)

6 дней назад

txlyre синхронизированные коммиты с main на txlyre/3x-ui из зеркала

  • 9f80cfedab fix(sub): use standard sub://BASE64#REMARK scheme for Shadowrocket
  • 1b436bb3e0 fix(clients): honor global pageSize and widen size-changer dropdown Read pageSize from defaultSettings and apply it to the clients table so the panel-wide pagination preference is respected. Widen the AntD size-changer trigger and its teleported popup so '100 / page' no longer truncates.
  • 5b5ac3f04b fix(migrate): include hysteria, hysteria2, shadowsocks in client sync The MigrationRequirements protocol filter only covered vmess/vless/trojan, so orphaned clients in hysteria/hysteria2/shadowsocks inbounds were never synced into the relational clients table on startup.
  • Просмотр сравнение для этих 3 коммитов »

1 неделя назад