mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-09 14:45:02 +03:00
b6cb818a51d50a3479af5effd98c13e066da2b21
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1bdb517b88 |
fix(a11y): silence live stats and make Select tags removable for screen readers
Ant Design's Spin wraps its content in aria-live="polite", so every WebSocket tick on the dashboard, inbounds, clients and nodes pages was announced, and only the changed number was read, with no label. Pass aria-live="off" on every Spin that wraps content; aria-busy still signals loading. Each vital tile is now a group named by its metric, so "47.2 %" is read as CPU. rc-select renders each tag's remove "x" as an aria-hidden span with no tabIndex, so screen-reader users could only Backspace the last value away, never remove one SNI or inbound from the middle. selectTagRender swaps in a real button named "Remove <value>" and moves focus to the next tag after removal. It is applied to every tags/multiple Select. maxTagCount="responsive" is dropped because it hid selected inbounds behind "+N", where no screen reader could reach them. Two oxlint rules (antd-a11y/spin-content-not-live and antd-a11y/select-tag-remove) keep new code from regressing either fix. |
||
|
|
e2184777a0 |
fix(frontend): make the panel usable with screen readers (#6907)
* fix(frontend): make the panel usable with screen readers * fix(frontend): keep mobile tab names and the inbound expiry hint Review of #6907 found two gaps in the screen-reader pass: - On phone-width screens catTabLabel renders only the icon and names the tab by putting aria-label on it, but callers now pass that icon with aria-hidden for the desktop label, so every Settings, Xray and Host category tab lost its name under TalkBack. catTabLabel now clears aria-hidden on the icon it uses as the name. - DateTimePicker forwarded id and aria-label but not aria-describedby, so the inbound expiry hint ("leave blank to never expire") was never attached. It now forwards it on both the Gregorian and Jalali input. |
||
|
|
b467d4c676 |
feat(reality): warn when target cert chain is too small for ML-DSA-65 (#6470)
* feat(reality): warn when target cert chain is too small for ML-DSA-65 Expose peer cert-chain DER size from the REALITY scanner and surface a UI warning when ML-DSA-65 is enabled but the chain is under xray-core's 3500-byte minimum, so silent fallback failures are easier to catch. Fixes #5973 * fix(reality): gate scanner ML-DSA tag and sync docs OpenAPI Only warn on short cert chains in the target scanner when ML-DSA-65 is enabled. Copy frontend/public/openapi.json to docs/public/openapi.json and fix oxfmt wrapping in the new test. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
2ec6c73613 |
feat(xray): update xray-core to v26.9.8 and adapt panel
Bump xtls/xray-core to 37ceb8b4b6 (v26.9.8) and the three binary pins (DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted symbols; the impact is entirely on the JSON config surface. Outbound "proxySettings" is now refused by the config loader (moved to streamSettings.sockopt.dialerProxy) and a freedom outbound rejects sockopt.addressPortStrategy. Either key in a stored template would keep the core from starting after the upgrade, so a new OutboundRemovedKeysFix seeder rewrites xrayTemplateConfig once: proxySettings.tag becomes sockopt.dialerProxy (an existing dialerProxy wins) and addressPortStrategy is dropped from freedom outbounds. Template saves and outbound subscriptions already run through the vendored loader, so the new refusals surface there with the core's own message. REALITY no longer applies a built-in minClientVer (26.3.27) when the field is empty. The form placeholder and the min/max hints in all 13 locales now say that empty means no minimum. New upstream keys the Zod schemas would otherwise strip, with form support where a sibling field already had it: - blackhole response type "custom" with base64 customResponseData - realm finalmask ipMode (dual/v4/v6) and portMapping (UPnP / NAT-PMP) - quicParams brutalDisableLossCompensation, disableChromeParrot, disableGSO, disableStatelessReset - hysteria masquerade proxy xForwarded - wireguard outbound remoteDNS - routing rule localOS freedom.domainStrategy is only deprecated upstream (auto-migrated to sockopt.domainStrategy with a warning) and is left untouched. |
||
|
|
92fb94d856 |
Move to TypeScript 7 and the oxc toolchain (oxlint + oxfmt) (#6262)
* chore(frontend,docs): move to TypeScript 7 and replace ESLint with oxlint
TypeScript 7 is the native Go port and ships no programmatic compiler
API, so typescript-eslint cannot run at all: it peer-pins
typescript >=4.8.4 <6.1.0 (canary too) and hard-crashes with
"typescript-eslint does not support TS 7.0". Upstream support is
tracked in typescript-eslint#10940 and targets TS >=7.1.
Rather than wait, or carry Microsoft's side-by-side alias (which keeps
a second TS 6 install alive purely to feed the linter), both projects
move to oxlint, which never depended on the TypeScript API.
Typecheck drops from ~9.7s to ~2.2s and 167 packages leave frontend/.
oxlint has no no-restricted-syntax, so the #6121/#6127 cleared-
InputNumber guard is reimplemented as a JS plugin in
frontend/tools/oxlint/. It was verified to still fire in
pages/settings/** and pages/xray/** and to stay exempt in *Modal.tsx.
The type-aware @deprecated sweep survives too, as
`npm run lint:deprecated`: oxlint's type-aware mode runs on
oxlint-tsgolint, which drives the TS 7 typescript-go checker, so the
TS 7 move is what makes it possible.
Behaviour is preserved rather than tightened. jsx-a11y/prefer-tag-over-role
is off in both configs because it was never part of the recommended sets
ESLint actually ran, and oxlint honours the existing eslint-disable
comments, so no source churn was needed.
Two real fixes fell out of the stricter linting:
- outbound-link-parser.test.ts used `out?.streamSettings` behind an `as`
cast, which hid the optional chain from ESLint and would throw on a
null parse; the rest of the file already used `out!`.
- InputAddon's conditional role/tabIndex/onKeyDown is genuinely
accessible but oxlint cannot evaluate it, so it gets a scoped disable.
* chore(docs): replace Prettier with oxfmt
oxfmt is the oxc project's Prettier-compatible formatter, so this pairs
with the oxlint move and drops the last JS-based tool from the docs
toolchain.
The swap is behaviour-preserving. Running Prettier and oxfmt over the
same files, with the existing .prettierrc.json settings migrated via
`oxfmt --migrate=prettier`, produces byte-identical output on every
file. (Comparing them outside the project directory is misleading:
Prettier silently falls back to its defaults when it cannot find its
config, which looks like a mismatch but is not one.)
The 18 files reformatted here were already failing `pnpm format:check`
before this change — Prettier wanted the exact same edits. The check is
not part of docs-ci.yml, which is why the drift went unnoticed.
.prettierignore becomes ignorePatterns in .oxfmtrc.json, keeping the
deliberate MDX exclusion: reflowing MDX prose merges headings into
paragraphs and collapses lists inside Steps/Callout components. Both
that and the generated fumadocs-openapi reference output were verified
untouched.
oxfmt is pinned to 0.63.0 rather than latest. pnpm 11's built-in
minimumReleaseAge policy rejects same-day releases, and 0.64.0 would
have made pnpm silently append 20 waiver lines to pnpm-workspace.yaml.
* style(frontend): adopt oxfmt and format src
frontend/ has never had a formatter, so this reformats 344 of 497 files
in src/. The change is purely whitespace, quoting and line wrapping —
no logic is touched. It is kept in its own commit so it does not bury
the TypeScript 7 / oxlint migration or the git blame for the code
itself.
Settings match docs/ and the code as it was already written: single
quotes, semicolons, trailing commas, 2-space indent, 100 columns. That
was measured rather than assumed — src/ was already uniformly
single-quoted and 2-space indented, with p90 line length at 75.
Formatting is scoped to src/ (mirroring `oxlint src`) and
.oxfmtrc.json ignores src/generated. Both matter: `make gen-check`
compares src/generated and public/openapi.json, and
`make msw-worker-check` byte-compares public/mockServiceWorker.js
against the installed MSW runtime, so reformatting any of them breaks
the gate.
Reflowing also moves `eslint-disable-next-line` comments off the line
they guard, which broke two suppressions that had been silently
correct before:
- clone-inbound-modal.test.tsx: the object literal became multi-line,
leaving `} as any;` four lines below its no-explicit-any disable.
- ClientsPage.tsx: the useMemo dependency array moved onto its own
line, out from under its exhaustive-deps disable.
Both comments were relocated onto the line they actually guard, and
verified to still suppress by removing them and watching the errors
return.
* ci: enforce formatting in CI and make verify
Adding oxfmt in the previous two commits gave both projects a formatter
but nothing that checks it, which is how docs/ had already drifted to 18
unformatted files: docs-ci.yml runs typecheck, lint, test and build, but
never format:check, so Prettier's complaints were only ever visible to
whoever ran it by hand.
Wire `format:check` into the frontend job in ci.yml and the docs job in
docs-ci.yml, and add a `format-check` target to `make verify` so the
local gate keeps mirroring CI as the Makefile header promises.
Verified the step actually bites rather than passing vacuously: adding
a badly formatted line to a source file in each project makes both
`make format-check` and `pnpm format:check` fail, and reverting it makes
them pass again.
No workflow referenced ESLint or Prettier by name — they all invoke the
package scripts — so the tooling swap needed no other CI changes.
* ci: trigger CI on Makefile changes
The path filters listed **.go, go.mod, go.sum, frontend/**, .nvmrc and
ci.yml itself, but not the Makefile — so a change to the canonical task
runner that ci.yml is meant to mirror could land without any job
running. The previous commit, which edits both, only triggers because
it happens to touch ci.yml too.
* fix(frontend): replace deprecated Ant Design 6 APIs in the geo components
`npm run lint:deprecated` reported five uses of props Ant Design 6 has
deprecated. All five are gone, and the matching runtime warnings no
longer appear in the test output.
Tag `bordered={false}` becomes `variant="filled"` and Space `direction`
becomes `orientation`; both are the one-to-one replacements named in
antd's own deprecation messages, and `direction`/`orientation` share the
same Orientation type.
Input `addonAfter` is the one that is not a rename. It becomes a
`Space.Compact block` wrapping the Input and the browse Button, which is
antd's documented migration. `block` keeps the field filling its form
row as the addon did. Note this is a deliberate visual change: the
button used to be a borderless `type="text"` icon sitting inside the
addon's grey box, and is now a regular button whose border joins the
input. The tooltip, aria-label, ref, id and onBlur wiring are unchanged,
so the react-hook-form binding in RuleFormModal and the existing tests
still address it the same way.
Only these five were deprecated. The other `bordered` props in the tree
sit on QRCode, Table, Descriptions and Alert, where the prop is not
deprecated, and these were the only two Space `direction` uses in the
codebase.
* fix(frontend): restore lint rules lost in the oxlint migration, and test the guard
Addresses the review on #6262.
The frontend config re-enabled only no-explicit-any and no-unused-vars
and left the rest of tseslint's recommended set to oxlint's correctness
category. It does not cover all of it. Confirmed by linting one probe
file against both configs: docs/ (which enumerates the rules) reports
all nine, frontend/ reported four. So ban-ts-comment,
no-empty-object-type, no-namespace, no-require-imports and
no-unsafe-function-type had silently stopped being enforced — a `//
@ts-ignore` or a `namespace` block would have landed unflagged. The ten
rules are now mirrored from docs/.oxlintrc.json, and src/ still passes.
The #6121/#6127 guard was 57 lines of hand-written AST walking with no
test. It now has one: fixtures for the three banned shapes plus an
onNumber()-wrapped control, asserting the rule fires three times and
that .oxlintrc.json still wires it to the right paths. Verified it fails
for the right reason by making walk() enumerate nothing, which is the
silent-death mode the review described — the traversal depends on
Object.keys() seeing AST children as own enumerable properties.
The fixtures deliberately violate the rule, so their oxlint config is
named guard.oxlintrc.json rather than .oxlintrc.json: oxlint discovers
nested configs by directory, which would otherwise turn the fixtures
into three lint errors. The test passes it explicitly with -c.
Also from the review:
- lint and format now cover tools/ as well as src/, so the one piece of
hand-written lint logic in the repo is no longer the least covered
file in it.
- lint-staged runs oxfmt before oxlint --fix. Formatting became a hard
CI gate in this PR while the hook only ran the linter, so a commit
could pass the hook and fail CI on formatting alone.
- .oxfmtrc.json ignores public/, so the artefacts that make gen-check
and make msw-worker-check byte-compare stay safe even if oxfmt is
invoked without a path argument.
- The MDX and generated-reference rationales that .prettierignore
carried are back as comments in docs/.oxfmtrc.json — oxlint and oxfmt
both accept JSONC, so relocating them was unnecessary.
Not applied: the review also suggested restoring ../internal/web/dist to
the ignore lists. Both tools reject `..` patterns outright ("patterns
are resolved within the config file's directory"), and being outside
frontend/ it is unreachable anyway.
|
||
|
|
708a69acde |
fix(reality): make the REALITY target check usable on a private network (#6242)
* fix(reality): make the REALITY target check usable on a private network The probe dials through netsafe.SSRFGuardedDialContext, so a fronting service reachable only inside the deployment (a Docker service name, a LAN address) always failed with "blocked private/internal address": the inbound itself works, because the guard sits in the probe path only, so the panel reported a red verdict on a healthy configuration. Instead of a panel-wide setting that lifts the guard for good, the guard is now lifted per probe and only after the operator confirms the local-network warning in a modal; the verdict keeps privateTarget set, so a passing local check stays a warning rather than a green success. The probe also sent the target host as SNI. Clients dial the target but send a name from serverNames, so a fronting proxy answered with its default certificate — a Traefik front reached as "traefik" reported "certificate is valid for <hash>.traefik.default, not traefik" on a deployment whose clients get a valid chain. The panel now sends the first configured serverName as SNI and the certificate is verified against it; empty serverNames keeps the old fallback. The reported target stays the dialled address, so a passing check no longer rewrites the target field with the SNI host. The result panel reports what was actually seen: the SNI used, the certificate subject/issuer and its expiry stay visible when the chain is untrusted (with "Not trusted" appended) instead of being replaced by that verdict alone. Certificate names are copied into the SNI field only when the chain verified — the names on a proxy's default certificate would otherwise become the SNI of the next check. The bulk/CIDR scanner keeps the guard unconditionally: honouring the opt-in there would turn it into an internal network scanner. * fix(reality): recover from a stale SNI and report a refused address reliably Review follow-up on the REALITY target check. The probe sends the stored serverNames as SNI, and the panel only wrote names back when the whole chain verified, so switching Target while the SNI field still held the previous target's names failed every rescan: the new target's real names came back from the probe but were discarded with the verdict. The certificate is now checked in two steps — chain first, then the name — and a trusted chain presented for other names is enough for the panel to offer those names, so the next scan passes. Picking a row in the bulk scanner replaces the names outright, since keeping the previous target's SNI leaves a REALITY config that cannot work. SSRFGuardedDialContext kept the refusal only in lastErr, so on a dual-stack name a refused private address followed by a failing public one lost the sentinel and the panel silently skipped the confirmation. The refusal is now tracked separately and reported alongside the last dial error. Honouring the opt-in is logged with the target and the resolved address, since it bypasses the SSRF guard on an authenticated endpoint. The read-only SNI row in the result is labelled "SNI used" so it no longer collides with the SNI field below it, and the comment blocks are back within the 2-line limit. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ca6955d88b |
feat(ui): validate the REALITY client version range at save time (#6126)
* feat(ui): validate the REALITY client version range at save time The impossible range from PR #6125 — a max below the effective minimum — could still be saved; the tooltip only helps a user who hovers it. Add save-time validation mirroring xray-core's parser (up to three dot-separated parts, each 0-255) on both fields, plus a cross-field check that a non-empty max is not below a non-empty min. Errors are field-level i18n keys following the REALITY target precedent, so the modal stays open and points at the offending field instead of storing a config that rejects every client. A malformed min is reported by its own field and skipped by the max comparison, so the user sees one precise error per field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): reject untrimmed client versions and revalidate max on min edits From review: the validators trimmed but the save path ships the value verbatim, and xray-core's part parser accepts no surrounding whitespace — so a green form could still save a config the core refuses to load. Reject any value that differs from its trimmed form. Also revalidate the max field after a min edit when max already shows an error, so correcting the min clears the stale cross-field message without waiting for the next submit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a2774bf212 |
fix(ui): explain the REALITY client version gate and drop the impossible placeholder (#6125)
* fix(ui): explain the REALITY client version gate and drop the impossible placeholder An empty Min Client Ver looks unrestricted, but Xray-core silently falls back to a built-in minimum (currently 26.3.27) that rejects third-party cores such as Mihomo and sing-box with a bare REALITY verification failure, and nothing in the panel points at the field. Add tooltips to both version fields explaining the fallback and its TLS-fingerprint-freshness rationale. The Max Client Ver placeholder (25.9.11) sat below the built-in minimum, so filling in both placeholders produced a range that rejects every client. Remove it; empty genuinely means no upper limit for that field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reality): warn that an empty min client version rejects old cores Common pitfalls covered bad targets, SNI mismatches, leaked keys and wrong flow, but not the client version gate that currently bites Mihomo and sing-box users. Add it to all four doc languages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): word the version hints against the effective minimum Address the automated review: the Max Client Ver hint said only 'not lower than Min Client Ver', which re-establishes the empty-means-unset mental model when the effective floor is the core's built-in minimum. Both hints now name the effective minimum and tie the quoted 26.3.27 to the core build the panel runs, since operators can install any Xray-core version. Also from review: full-width quotes and a missing verb in the zh doc bullet, the idiomatic Arabic opening, and a format-only x.y.z placeholder on Max Client Ver so the field still conveys its shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
814cda3fb4 |
feat(xray): update xray-core to v26.7.11 and adapt panel
Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins (DockerInit.sh, release.yml x2) in lockstep. Adapt the panel to the upstream changes: - Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from the core. A migration rewrites stored none/plain SS methods to a supported cipher and none/zero VMess security to "auto" (on both the clients column and inbound settings JSON); the SS build-time heal does the same so a row injected after boot cannot brick startup. The removed values are dropped from every frontend option list, schema and adapter, and coerced to "auto" at the Go link/sub/Clash emit sites and both link importers. Fix the CipherType_NONE sentinel that no longer compiles. - Unencrypted vless/trojan outbounds to a public address are now refused by the core. Validate outbounds through the vendored config loader when saving the xray template and when storing/merging outbound subscriptions, so one such outbound cannot keep the core from starting. - New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub link allowlist, the frontend enum and the FinalMask form (hostname, usernames, required password), and document it. - streamSettings gained a "method" alias for "network"; canonicalize it to "network" at inbound save time and in the form adapters/schema so a method-keyed config keeps its transport. - New root "env" config key is passed through xray.Config, compared in Equals, and forces a restart in the hot diff. - REALITY now defaults minClientVer to 26.3.27; update the form placeholder. |
||
|
|
61e12e4c29 |
Frontend dev tooling (Husky, lint-staged, MSW, Storybook) + full React Hook Form migration (#5859)
* chore(frontend): add husky + lint-staged pre-commit gate Wire a local pre-commit gate that runs eslint --fix on staged frontend TypeScript via lint-staged. Because the only package.json lives in frontend/ while the git root is one level up, the prepare script installs husky hooks at frontend/.husky from the repo root (cd .. && husky frontend/.husky), and the pre-commit hook cd's into frontend/ before invoking lint-staged so node_modules resolves. * test(frontend): add MSW request mocking Add Mock Service Worker so tests can exercise the real http-init.ts request pipeline (CSRF acquisition, 403 refetch-and-retry, body parsing) instead of only stubbing HttpUtil. A node setupServer is started for the vitest unit project with onUnhandledRequest bypass so the existing HttpUtil spies and 55 component tests are untouched; the browser worker is copied to public/ for Storybook and dev use. * chore(frontend): add Storybook + component stories Set up Storybook 10 on the React-Vite builder (compatible with the pinned Vite 8.1.3 and React 19). The preview decorator mirrors the vitest component harness: an Ant Design ConfigProvider with a light/dark toolbar toggle and an en-US i18next instance. main.ts neutralizes the app vite config bits that do not belong in a component workshop (the three-entry rollup input, renderBuiltUrl, and the shared dist outDir) so build-storybook can never clobber internal/web/dist. Seeds stories across the presentational library (viz, ui, clients, feedback). build-storybook is a local tool and is not wired into the CI gate. * feat(frontend): add React Hook Form primitives Introduce the shared RHF layer that AntD inputs bind through, ahead of migrating the forms off Ant Design's Form store: - FormField wraps a Controller in an Ant Design Form.Item shell, reconciling the value/onChange shapes of Input, Switch, InputNumber, Select and friends via normalizeAntdOnChange, with input/output transforms and Zod-issue-key error messages resolved through t(). - useZodForm wires zodResolver (Zod 4) with the AntD-matching modes (validate on submit, then live) and shouldUnregister false so hidden and unmounted-tab fields keep their values. - rhfZodValidate covers the rare per-field rule sites. Covered by a FormField test exercising normalization, transforms, and resolver error surfacing. * refactor(frontend): migrate Pattern-B leaf forms to React Hook Form Move the controlled-useState leaf forms onto RHF via the FormField primitive, keeping Ant Design components and each form's exact submit behaviour (same safeParse, same toast on the first Zod issue, same payload building): - clients: ClientBulkAdjustModal, BulkAddToGroupModal, ClientBulkAddModal - xray: RuleFormModal, BalancerFormModal, WarpModal, NordModal Multi-control widgets that don't fit a single input (inbound dual select, subId regen, expiry branches, the balancer tag warning) stay as explicit Controller/setValue. Derived visibility now reads live values through useWatch. FormField gains a required prop so migrated fields keep their required-asterisk affordance. Settings tabs are intentionally excluded: they are control-panel components that live-patch a parent AllSetting via SettingListItem, not Ant Design Form submit-forms. * refactor(frontend): migrate LoginPage to React Hook Form Replace the Ant Design Form store + antdRule per-field validation with useForm + FormField. The AntD Form stays as the layout/submit wrapper, now driving methods.handleSubmit(onSubmit) via onFinish. Username and password validate through rhfZodValidate(LoginFormSchema.shape.*); the two-factor field keeps its conditional required rule (only registered when 2FA is enabled). Submit posts the same values to /login. * refactor(frontend): migrate ClientFormModal to React Hook Form Move the client add/edit form off controlled useState onto RHF while preserving exact submit behaviour (same ClientFormSchema / ClientCreateFormSchema safeParse, same toast, same payload + attach/ detach diff + external-links build). expiryDate is stored as an epoch number (never a Dayjs) to survive RHF's value cloning, converted at the DateTimePicker boundary. externalLinks uses useFieldArray with stable ids. inboundIds and the derived show*/ss2022 visibility read live via useWatch. Space.Compact button-group widgets stay manual Controllers so the joined borders keep working. * refactor(frontend): migrate Node and DNS modals to React Hook Form Both are self-contained Pattern-A forms (no shared fragments). Replace Form.useForm with useForm + FormProvider, Form.useWatch with useWatch, setFieldValue with setValue, and partial validateFields([...]) with methods.trigger([...]). Per-field antdRule becomes rhfZodValidate rules; the Node scheme->tlsVerify cascade moves to FormField onAfterChange; the DNS domains/expectIPs/unexpectIPs string arrays are driven by useWatch + setValue. Submit runs through handleSubmit on the modal OK button, preserving each form's exact validation, payload build, and save/onConfirm behaviour. * refactor(frontend): migrate HostFormModal to React Hook Form The host external-proxy editor's outer form moves to useForm + FormProvider. Security/tab visibility reads via useWatch; the three json-form editors (HostMuxForm/HostSockoptForm/HostFinalMaskForm) are bound as value/onChange black boxes through a Controller (their own internal forms are unchanged). remark/inboundId keep their validation via rhfZodValidate; submit runs through handleSubmit and builds the same payload (isDisabled = !enable) and save call. * refactor(frontend): migrate OutboundFormModal + fragments to React Hook Form Move the outbound form cluster off Ant Design's Form store onto RHF. The parent uses useForm + FormProvider with a watch() subscription for the protocol reseed cascade and setValue-based network/security/xmux cascades; the JSON<->Basic bridge and the formValuesToWirePayload submit are preserved exactly. Every outbound transport/protocol/security fragment now binds through FormField/useWatch via context. The shared config editors stay untouched and are bound through small value/onChange adapters (src/lib/xray/forms/fields: FinalMaskField, SniffingField, SockoptCustomField) via Controller; HeaderMapEditor binds directly. The host json-form wrappers that reuse the outbound MuxForm/ SockoptForm (HostMuxForm, HostSockoptForm, OutboundSubtreeJsonForm) move to a local RHF provider to match. Outbound render/link tests pass unchanged. * refactor(frontend): migrate InboundFormModal + fragments to React Hook Form Move the inbound add/edit form (the largest form in the panel) and its transport/protocol/security fragments off Ant Design's Form store onto RHF, mirroring the outbound migration. The parent uses useForm + FormProvider with a watch() subscription for the protocol reseed cascade (type==='change' guard so programmatic resets don't reseed) and setValue-based network/security cascades; useSecurityActions drives the TLS/Reality keypair + scan through setValue. Hidden pass-through Form.Items are dropped (their values ride in the reset object and survive via shouldUnregister:false), so getValues() still returns the settings.clients subtree untouched. accounts / certificates / tun lists use useFieldArray; the shared FinalMask/Sniffing/Sockopt editors bind through the value/onChange adapters. Submit keeps the manual InboundFormSchema.safeParse + formatInboundValidation toast + formValuesToWirePayload exactly. The golden link/full fixtures pass byte-for-byte, confirming identical wire output. inbound-form-blocks test harness rewritten from a Form.useForm harness to an RHF provider. * refactor(frontend): retire antdRule; document the RHF form pattern All forms now build on React Hook Form, so the AntD-Form Zod adapter antdRule (src/utils/zodForm.ts) has no remaining callers — remove it. Update frontend/CLAUDE.md: forms use useZodForm + FormField from components/form/rhf with zodResolver/rhfZodValidate validation; AntD <Form> is layout-only; the shared FinalMask/Sniffing/Sockopt editors stay AntD islands wrapped as value/onChange adapters bound via a Controller. * chore(frontend): cover esbuild in the allowScripts allowlist esbuild (pulled in transitively by Vite/Vitest/Storybook) ships a postinstall that npm's allow-scripts flags as uncovered on every install. Its platform binary is delivered through the @esbuild/<platform> optionalDependencies, so the postinstall isn't needed here; deny it like the other entries to silence the warning. * fix(frontend): restore label layout in Sniffing/FinalMask field adapters The value/onChange adapters that wrap the shared SniffingFields and FinalMaskForm editors put them in their own isolated AntD Form, but that Form was missing the label layout the fields used to inherit from the inbound/outbound parent form. Their labels rendered full-width instead of the compact right-aligned column, so the Sniffing tab and the TCP Masks / QUIC Params sections looked broken. Give both adapter forms the same colon=false, labelCol/wrapperCol span 8/14, labelWrap layout. * ci: add least-privilege permissions to Docs CI workflow The docs-ci workflow had no explicit permissions block, so it inherited the repository default for GITHUB_TOKEN. The build job only checks out and builds the docs, so restrict it to contents: read, resolving the CodeQL actions/missing-workflow-permissions alert. |
||
|
|
c8ef1b1f68 |
feat(reality): derive a stable per-client spiderX for shared links
The inbound's spiderX now acts as a per-client seed: exports emit sha256(seed|subKey) truncated to a 15-hex "/path", so a client's spx no longer changes on every subscription fetch (#5718) while different clients stop sharing one fingerprintable value. The form gains a regenerate button that rotates every client's path at once. The frontend link builders derive through the same function (lib/xray/spider-x.ts, @noble/hashes) keyed on subId-then-email like the Go subKey, so panel QR/copy links and subscription output agree — cross-language vector tests lock both sides byte-for-byte. streamData now tolerates malformed stored stream settings (unparseable JSON, null tls/reality settings) instead of panicking the subscription request. |
||
|
|
71aca2018a |
feat(a11y): screen-reader & keyboard accessibility across the panel (#5486) (#5652)
* feat(a11y): label list, toolbar & dashboard actions for screen readers Phase 1 of #5486 (Android TalkBack support). Icon-only controls across the management surfaces previously announced only their untranslated icon name (e.g. "edit", "ellipsis") or nothing at all. - Add aria-label to icon-only row-action and toolbar buttons across inbounds, clients, groups, hosts, nodes and xray (outbounds/routing/dns/balancers) lists, plus the dashboard cards. - Make clickable bare icons and AntD Card actions keyboard-operable via role/tabIndex + Enter/Space (new activateOnKey helper); convert mobile dropdown triggers to buttons so they open from the keyboard. - Fix the sidebar hamburger's mislabeled aria-label (was the dashboard label) and translate previously-hardcoded outbound menu labels. New i18n keys in all 13 locales: sort, menu.openMenu, pages.xray.outbound.moveToTop. * feat(a11y): label modal, QR and copy/download controls for screen readers Phase 2 of #5486. Modal and overlay controls relied on tooltips (not a reliable accessible name) or were bare clickable icons with no keyboard or screen-reader support. - Add aria-label to copy/QR/download/info icon buttons in the inbound and client info modals, sub-links modal, QR panel, backup/log modals, and to the bare search/select inputs of the attach/detach client modals. - Make click-to-copy QR codes and the IP-log refresh/clear, geofile reload and log refresh icons keyboard-operable (role/tabIndex + Enter/Space) with translated labels. - Label the 2FA code input; drop the QrPanel download-image string fallback now that the key exists. New i18n key in all 13 locales: downloadImage. * feat(a11y): label form fields and shared form components for screen readers Phase 3 of #5486. Form controls and shared form widgets were largely unlabelled, and several remove controls were not keyboard-operable. - SettingListItem now ties its title to the control via aria-labelledby, giving accessible names to the ~90 settings-tab inputs at once. - InputAddon gains button semantics (role/tabIndex/Enter+Space) and an ariaLabel prop when used as an interactive remove control. - Sparkline charts expose a role="img" summary of their latest values. - Add aria-label to add/remove/regenerate icon buttons and bare inputs/selects across inbound, client and xray (dns/routing/balancer/ outbound) forms; make clickable remove icons keyboard-operable; mark decorative help/target icons aria-hidden; label the JSON editor, date-time clear button, header-map remove, notification select-all and remark token chips. New i18n keys in all 13 locales: regenerate, jsonEditor, pages.xray.balancer.{costMatch,costValue,costRegexp}. * chore(a11y): add eslint-plugin-jsx-a11y harness and fix flagged interactions Phase 4 of #5486. Adds eslint-plugin-jsx-a11y (recommended ruleset, scoped to .tsx) so screen-reader/keyboard regressions fail lint. - Make the mobile node-card header a proper keyboard disclosure (role=button, aria-expanded, Enter/Space activation that ignores clicks on the nested action buttons) and drop the now-redundant stop-propagation click handlers the linter flagged on card-action wrappers in the node, client and inbound mobile cards. - Disable jsx-a11y/no-autofocus: the autofocus on the login field and modal primary inputs is intentional focus management that helps screen-reader and keyboard users land on the right control. make lint passes with the a11y ruleset enforced. * feat(a11y): cover remaining deferred spots (settings tabs, sockopt, API docs) Completes the panel sweep for #5486 by labelling the spots previously left out of phases 1-4: - NotifyTimeField (Telegram notifications): the mode, interval, unit and custom-cron inputs now carry aria-labels. - The Sockopt toggle in transport options. - Settings category tabs in icons-only (mobile) mode now expose the tab name as the icon's aria-label instead of the raw icon name. - The Swagger API-docs view is wrapped in a labelled region landmark. New i18n keys in all 13 locales: pages.settings.notifyTime.{interval,unit}. * feat(a11y): label shared xray form components and remark field Code review surfaced frontend/src/lib/xray/forms/ — shared form components used by the host and inbound JSON forms — which the initial audit missed. - FinalMaskForm (TCP/UDP final-mask editor): label the icon-only add and regenerate buttons and make all six remove icons keyboard-operable (role/tabIndex/Enter+Space); adds useTranslation to its sub-components. - CustomSockoptList: the remove icon is now keyboard-operable. - SniffingFields: aria-label on the otherwise label-less destOverride select. - RemarkTemplateField: aria-label on the remark-variable picker button. New i18n key in all 13 locales: pages.inbounds.sniffingDestOverride. * feat(a11y): label client info modal and WireGuard config block After rebasing onto the WireGuard client-config feature, re-apply the ClientInfoModal copy/QR/IP-log aria-labels (the modal was restructured upstream, so the original labels did not carry over) and label the new ConfigBlock component's copy/download/QR actions. ConfigBlock's action wrapper keeps its stop-propagation handler (a non-interactive guard for the Collapse header) under a scoped jsx-a11y exception. * fix(frontend): let npm install jsx-a11y under ESLint 10 eslint-plugin-jsx-a11y@6.10.2 declares a peer range that stops at ESLint 9, but the panel is on ESLint 10, so `npm ci` aborts with ERESOLVE even though the plugin runs fine on ESLint 10 with flat config. Add an npm override so jsx-a11y accepts the project's ESLint version. This keeps normal peer resolution (recharts' react-is peer still auto-installs) — no global legacy-peer-deps and no manual react-is pin needed. * fix(a11y): size mobile row triggers and move node expand role to chevron Address automated review on #5652: - add size="small" to the inbound/client/node mobile-card "more" dropdown triggers so they match the adjacent small Switch and the established desktop RowActions pattern. - move the node card-head disclosure semantics (role/tabIndex/aria-expanded/ keyboard) onto the chevron affordance so the expand control is no longer a role="button" wrapping the Switch, info button and dropdown. Mouse click-anywhere-to-expand is preserved on the header div. |
||
|
|
6964d84742 |
feat(reality): add live REALITY target scanner with IP/CIDR discovery
Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate. - Single-domain validate auto-fills target and serverNames from the cert SAN - Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe - New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries - Add scanner strings to all 13 locales - Replace deprecated AntD Alert message prop with title across the panel |
||
|
|
7c8889466b |
feat(tls,reality): port xray TLS/REALITY fields, cert-hash helpers, fallback UX
TLS: add verifyPeerCertByName (vcn) to inbound settings + emit in both share-link generators (frontend + Go sub) and outbound parser; the allowInsecure replacement xray removed after 2026-06-01. Add server-side curvePreferences, masterKeyLog, echSockopt (passthrough + form) at tlsSettings top-level so they survive the panel-only settings strip. REALITY: add limitFallbackUpload/Download (afterBytes/bytesPerSec/burstBytesPerSec) with per-field tooltips, plus masterKeyLog. Verified field names/semantics against pinned xray v1.260327.1 (bytesPerSec=0 disables). Hosts: fix verify_peer_cert_by_name column bool->string (xray expects comma-separated names) with an idempotent, history-gate-free migration (SQLite typeof blank; Postgres ALTER once); emit vcn for hosts/external proxies. Server: add getCertHash (local cert DER SHA-256) and getRemoteCertHash (xray tls ping) endpoints + api-docs; wire pinned-cert field buttons. Drop the meaningless random-hash button. Xray UI: metrics endpoint (listen/tag) config in Basics; import/export for routing rules and outbounds. Fallbacks card: compact empty state, header-aligned actions, responsive labeled grid rows. i18n: add all new keys to every locale; drop unused generateRandomPin. |
||
|
|
4915d6b18d |
refactor(frontend): move form-item hints from extra to tooltip
Switch reality target, node options, and WARP auto-update-IP hints from inline extra text to label tooltips for a cleaner form layout. |
||
|
|
6ed6f57b5c |
fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target (#4988)
* fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target Strip mode-specific XHTTP fields for stream-one, reset harmful sockopt defaults to 0, split server/client Reality fields on save, validate target host:port in the inbound form, and expose Happy Eyeballs for the direct freedom outbound. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(panel): keep REALITY public key on the wire, guard freedom noises The REALITY server/client wire split deleted realitySettings.settings on save, but the panel stores the REALITY public key there and every share-link / subscription generator reads it back from that path (frontend inbound-link.ts, Go subService/subJsonService/subClashService). Stripping it produced empty pbk= links, breaking client connectivity after save+reload. Revert the reality normalization (drop normalizeRealityForWire and the key sets), restore the inbound REALITY form fields (uTLS, spiderX, publicKey, mldsa65Verify) while keeping the new validated target field, and restore the mldsa65Verify clear handler. Also guard freedomToWire against undefined noises/finalRules (same defensive treatment as the existing fragment guard, issue #4686) which the new freedom-outbound test surfaced as a crash. Tests now assert the public key is preserved. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
d1882c7f29 |
refactor(frontend): reorganize source tree & break down oversized modals/tabs (#4698)
* refactor(frontend): reorganize components & pages into feature folders
No behavior change; pure file relocation + import path updates.
* refactor(frontend): move shared protocol enums to schemas/protocols/shared
Decouple Outbound from Inbound schemas: SSMethodSchema and VmessSecuritySchema (shared between inbound & outbound) now live in a neutral schemas/protocols/shared/ module. Outbound no longer reaches into schemas/protocols/inbound/*. Pure relocation + import rewiring; schema values identical, snapshots & golden tests unchanged.
* refactor(frontend): break InboundList into helpers/types/RowActions/columns hook/stats modal
InboundList.tsx 781 -> 203 lines. Extracted pure helpers (network labels, sort fns, isInboundMultiUser), shared types, the row-actions menu/cell, the table columns hook, and the mobile stats modal into the list/ folder. Code moved verbatim; no behavior change. typecheck/lint/test/build green, 337 tests pass.
* refactor(frontend): extract InboundInfoModal helpers, types & buildInboundInfo
InboundInfoModal.tsx 1081 -> 836 lines. Moved the pure data helpers (network host/path readers, link-protocol check, copy/download/statsColor/IP formatting) plus all shared types and the buildInboundInfo data builder into info/helpers.ts and info/types.ts. The state-coupled render body is left intact (no React render tests to guard a deeper split). Code moved verbatim; no behavior change. All gates green, 337 tests pass.
* test(frontend): add React Testing Library + jsdom render-test harness
- vitest projects: node unit tests stay lean; new jsdom 'components' project runs *.test.tsx
- component setup: matchMedia/ResizeObserver/localStorage polyfills, react-i18next init, persian-calendar-suite stub (only used under jalali locale)
- smoke + field-label structure snapshots for Inbound & Outbound form modals
- establishes the regression net required before decomposing the oversized form modals
- 341 tests pass (337 unit + 4 component); typecheck/lint/build green
* test(frontend): per-protocol field-structure coverage for both form modals
- drive the protocol Select in jsdom and snapshot rendered Form.Item labels for every protocol
- 10 outbound + 10 inbound protocol states captured as the regression net for protocol-core extraction
- add robust select-driving helpers (test-utils) + post-test body cleanup (setup.components)
- 341 tests pass; typecheck/lint green
* refactor(frontend): extract OutboundFormModal constants & stream helpers
OutboundFormModal.tsx 2238 -> 2080. Moved the pure option arrays/sets and the stream-slice helpers (newStreamSlice, hysteriaStreamSlice, isMuxAllowed, buildAddModeValues) into outbound-form-constants.ts and outbound-form-helpers.ts. Per-protocol render snapshots unchanged -> verified no behavior change. typecheck/lint/build green.
* refactor(frontend): extract InboundFormModal advanced JSON editors
InboundFormModal.tsx 3129 -> 2863. Moved AdvancedSliceEditor and AdvancedAllEditor (the in-modal JSON slice/all editors) into advanced-editors.tsx along with their adapter-helper imports. Per-protocol render snapshots unchanged -> verified no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal loopback/blackhole/dns field blocks
Moved the outbound-only protocol field blocks (loopback, blackhole, dns) out of the modal render body into outbound-only-fields.tsx. First render-body extraction behind the per-protocol snapshot net: loopback/blackhole/dns snapshots unchanged -> verified no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal freedom field block
OutboundFormModal.tsx 2063 -> 1753. Moved the freedom protocol field block (domainStrategy, fragment, noises, finalRules) into outbound-freedom-fields.tsx. Verbatim relocation; freedom per-protocol snapshot unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal wireguard field block
OutboundFormModal.tsx 1753 -> 1622. Moved the wireguard protocol field block (address, keypair gen, domainStrategy, peers + allowedIPs) into outbound-wireguard-fields.tsx; dropped now-unused icon/InputAddon/WireguardDomainStrategy imports. Verbatim relocation; wireguard snapshot unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal core protocol fields
OutboundFormModal.tsx 1622 -> 1538. Moved the shared protocol core field blocks (vmess/vless ID, vmess security, vless encryption/reverseTag, trojan/ss password, ss method/uot, socks/http user/pass) into outbound-core-fields.tsx; dropped now-unused schema/option imports. Per-protocol snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): fold OutboundFormModal server address/port block into core fields
OutboundFormModal.tsx 1538 -> 1516. Moved the shared connect-target (address/port) block into OutboundCoreProtocolFields at the same render position; dropped the now-unused SERVER_PROTOCOLS import. Snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split outbound-only protocol forms into per-protocol files
Replace the grouped outbound-only-fields.tsx + outbound-freedom-fields.tsx with one file per protocol under outbounds/protocols/: freedom.tsx, blackhole.tsx, dns.tsx, loopback.tsx (+ barrel). Matches the prompt's 1-file-per-protocol structure. Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split outbound protocol forms into per-protocol files
Replace the grouped outbound-core-fields / outbound-wireguard-fields with one file per protocol under outbounds/protocols/: vmess, vless, trojan, shadowsocks, http, socks, wireguard, freedom, blackhole, dns, loopback (+ shared server-target). Matches the prompt's 1-file-per-protocol structure (per-modal). Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split outbound transport forms into per-transport files
Extract the tcp(raw)/kcp/ws/grpc/httpupgrade transport blocks into outbounds/transport/ per-file components (RawForm, KcpForm, WsForm, GrpcForm, HttpUpgradeForm). xhttp + hysteria transport remain inline for a follow-up. Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal xhttp transport form
Move the xhttp transport block into transport/xhttp.tsx (takes form + onXmuxToggle prop); drop now-unused HeaderMapEditor and MODE_OPTIONS imports from the modal. OutboundFormModal.tsx down to ~1001 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal tls/reality security forms
Move the TLS and Reality field blocks into outbounds/security/{tls,reality}.tsx; the none/TLS/Reality Radio.Group selector stays in the modal. Drop now-unused ALPN_OPTIONS/UTLS_OPTIONS imports. OutboundFormModal.tsx down to ~918 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split inbound-only protocol forms (tun, tunnel) into per-file
Extract the tun and tunnel protocol blocks from InboundFormModal into inbounds/form/protocols/{tun,tunnel}.tsx (presentational, declarative). First inbound-side per-protocol split. Verbatim relocation; inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split inbound wireguard & shadowsocks protocol forms
Extract the wireguard and shadowsocks protocol blocks from InboundFormModal into inbounds/form/protocols/{wireguard,shadowsocks}.tsx (presentational; form + regen handlers / isSSWith2022 passed as props). Drop now-unused Divider + SSMethodSchema imports. Verbatim relocation; inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split inbound vless/http/mixed/hysteria protocol forms
Extract the remaining inbound protocol blocks into inbounds/form/protocols/: vless (auth handlers/state as props), http + mixed (shared accounts-list), hysteria. Drop now-unused HysteriaMasqueradeForm/Typography/Text imports from the modal. InboundFormModal.tsx 2841 -> 2478. Inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): move HysteriaMasqueradeForm to lib/xray/forms/transport
The hysteria masquerade form edits streamSettings.hysteriaSettings.masquerade (a transport/stream concept) and is rendered identically by both modals, so it belongs next to FinalMaskForm in lib/xray/forms/transport/ rather than protocols/shared/. Moved the file, updated the transport barrel + both consumers (inbound hysteria protocol form, outbound modal), and removed the now-empty protocols/shared/ folder. Pure relocation; snapshots unchanged, typecheck/lint/build green.
* refactor(frontend): extract inbound transport forms into transport/ folder
Move the six inbound stream-transport blocks (tcp/raw, ws, grpc, xhttp,
httpupgrade, kcp) out of InboundFormModal into presentational components
under inbounds/form/transport/. XhttpForm takes the form instance and
re-derives its mode/obfs/placement watches internally; the rest are
declarative. InboundFormModal drops from 2566 to 2105 lines. No behavior
change — per-protocol field-label snapshots unchanged.
* refactor(frontend): extract inbound security forms into security/ folder
Move the inbound TLS and Reality stream-security blocks out of
InboundFormModal into presentational components under
inbounds/form/security/. The Radio.Group security selector stays in the
modal; TlsForm and RealityForm receive their cert/key/ECH generation
handlers and the saving flag as props. InboundFormModal drops from 2105
to 1708 lines.
Add inbound-form-blocks.test.tsx: render-snapshot coverage for each
extracted transport (raw/ws/grpc/kcp/httpupgrade/xhttp) and security
(tls/reality) component in isolation inside a minimal Form. The full
modal cannot exercise the stream/security tabs in jsdom because they are
gated behind Form.useWatch values that do not propagate in the test
harness, so component-level snapshots are the regression net for these
blocks. No behavior change.
* refactor(frontend): extract outbound sockopt/mux/hysteria transport blocks
Move the last three oversized inline stream blocks out of
OutboundFormModal into presentational components under
xray/outbounds/transport/: SockoptForm (~260 lines, the worst offender),
MuxForm, and HysteriaForm. Each takes the form instance; MuxForm also
takes protocol/network and keeps its isMuxAllowed gate. OutboundFormModal
drops from 962 to 621 lines and no inline section now exceeds the
250-line guideline. Existing outbound-form-modal snapshots already cover
sockopt/mux and stay byte-identical, confirming no behavior change.
* refactor(frontend): extract inbound sockopt + external-proxy blocks
Move the inbound Sockopt (~250 lines) and External Proxy stream blocks
out of InboundFormModal into presentational components under
inbounds/form/transport/, mirroring the outbound extraction. Each takes
its toggle handler (toggleSockopt / toggleExternalProxy) as a prop and
keeps its render-prop getFieldValue gate. InboundFormModal drops from
1708 to 1332 lines.
Extend inbound-form-blocks.test.tsx with isolated render-snapshot
coverage for both (SockoptForm seeded enabled + happyEyeballs;
ExternalProxyForm seeded with one TLS entry). No behavior change.
* refactor(frontend): break down RoutingTab into sections
Extract RoutingTab's presentational pieces into the routing/ folder:
helpers.ts (arrJoin/csv/chipPreview/ruleCriteriaChips), types.ts
(RuleRow), CriterionRow.tsx, RuleCardList.tsx (mobile card view), and
useRoutingColumns.tsx (desktop table columns hook). RoutingTab stays the
orchestrator holding rule state, mutate, tag-option memos and the
pointer-drag reorder logic, and drops from 550 to 291 lines. No behavior
change.
* refactor(frontend): extract BasicsTab constants and rule helpers
Move BasicsTab's geo option arrays + freedom/ipv4 outbound presets into
basics/constants.ts and the routing-rule get/set/sync helpers into
basics/helpers.ts. BasicsTab drops from 550 to 447 lines and keeps its
Collapse-of-settings panels (which stay coupled to mutate + derived
state, so splitting them into components would only add prop-drilling).
No behavior change.
* refactor(frontend): break down DnsTab columns/helpers/types
Extract DnsTab's pure pieces into the dns/ folder: helpers.ts
(STRATEGIES/DEFAULT_FAKEDNS + addr/domains/expectedIPs accessors),
types.ts (DnsConfig/HostRow/FakednsRow), and useDnsColumns.tsx
(useDnsServerColumns + useFakednsColumns table-column hooks taking their
row handlers as params). DnsTab stays the orchestrator for dns state,
mutate, hosts sync and the Collapse panels, and drops from 539 to 424
lines. No behavior change.
* refactor(frontend): break down OutboundsTab into sections
Extract OutboundsTab's pieces: outbounds-tab-types.ts (OutboundRow),
outbounds-tab-helpers.ts (address/untestable/security/breakdown +
traffic/testing/result accessors), useOutboundColumns.tsx (desktop table
columns hook) and OutboundCardList.tsx (mobile card view). OutboundsTab
stays the orchestrator for outbound state, mutate, reorder and the
toolbar, and drops from 516 to 238 lines. No behavior change.
This completes plan section 2.4.5 — all four oversized Xray tabs
(Basics/Routing/Dns/Outbounds) are now broken into sections + hooks.
* refactor(frontend): fold HysteriaMasqueradeForm into the hysteria forms
Inline the masquerade fields directly into both hysteria transport forms
(inbounds/form/protocols/hysteria + xray/outbounds/transport/hysteria)
and delete the shared lib/xray/forms/transport/HysteriaMasqueradeForm so
each hysteria form is self-contained. The masquerade JSX is unchanged;
form is typed as the untyped FormInstance (as the shared component was)
so the masquerade name paths still resolve. No behavior change.
* refactor(frontend): slim InboundFormModal by extracting hooks + sections
Pull the modal's non-layout logic into focused files at the form root:
- useSecurityActions.ts: TLS/Reality key + cert generation handlers and
onSecurityChange (consumed by the security tab)
- useInboundFallbacks.ts: fallback row state + load/save/derive/add/
update/remove/move handlers + eligible-child options
- FallbacksCard.tsx: the fallbacks card UI (presentational)
- SniffingTab.tsx: the sniffing tab UI (presentational)
Also drop the stale "Pattern A rewrite / sibling file" header comment and
the imports the extractions made unused. InboundFormModal goes from 1332
to 868 lines with no behavior change (351 tests green, snapshots
unchanged).
|