Explorar el Código

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).
Sanaei hace 20 horas
padre
commit
d1882c7f29
Se han modificado 100 ficheros con 5422 adiciones y 6455 borrados
  1. 662 0
      frontend/package-lock.json
  2. 3 0
      frontend/package.json
  3. 0 0
      frontend/src/components/feedback/PromptModal.tsx
  4. 0 0
      frontend/src/components/feedback/TextModal.tsx
  5. 2 0
      frontend/src/components/feedback/index.ts
  6. 0 0
      frontend/src/components/form/DateTimePicker.css
  7. 0 0
      frontend/src/components/form/DateTimePicker.tsx
  8. 1 1
      frontend/src/components/form/HeaderMapEditor.tsx
  9. 0 0
      frontend/src/components/form/JsonEditor.css
  10. 0 0
      frontend/src/components/form/JsonEditor.tsx
  11. 3 0
      frontend/src/components/form/index.ts
  12. 0 0
      frontend/src/components/ui/InfinityIcon.tsx
  13. 0 0
      frontend/src/components/ui/InputAddon.css
  14. 0 0
      frontend/src/components/ui/InputAddon.tsx
  15. 0 0
      frontend/src/components/ui/SettingListItem.css
  16. 0 0
      frontend/src/components/ui/SettingListItem.tsx
  17. 3 0
      frontend/src/components/ui/index.ts
  18. 0 0
      frontend/src/components/utility/LazyMount.tsx
  19. 1 0
      frontend/src/components/utility/index.ts
  20. 0 0
      frontend/src/components/viz/Sparkline.css
  21. 0 0
      frontend/src/components/viz/Sparkline.tsx
  22. 1 0
      frontend/src/components/viz/index.ts
  23. 0 0
      frontend/src/layouts/AppSidebar.css
  24. 0 0
      frontend/src/layouts/AppSidebar.tsx
  25. 0 0
      frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx
  26. 1 0
      frontend/src/lib/xray/forms/transport/index.ts
  27. 1 1
      frontend/src/lib/xray/inbound-link.ts
  28. 1 1
      frontend/src/pages/api-docs/ApiDocsPage.tsx
  29. 1 1
      frontend/src/pages/clients/ClientBulkAddModal.tsx
  30. 1 1
      frontend/src/pages/clients/ClientFormModal.tsx
  31. 1 1
      frontend/src/pages/clients/ClientInfoModal.tsx
  32. 1 1
      frontend/src/pages/clients/ClientQrModal.tsx
  33. 2 2
      frontend/src/pages/clients/ClientsPage.tsx
  34. 2 2
      frontend/src/pages/groups/GroupsPage.tsx
  35. 0 3129
      frontend/src/pages/inbounds/InboundFormModal.tsx
  36. 0 781
      frontend/src/pages/inbounds/InboundList.tsx
  37. 11 11
      frontend/src/pages/inbounds/InboundsPage.tsx
  38. 0 0
      frontend/src/pages/inbounds/clients/AddClientsToGroupModal.tsx
  39. 1 1
      frontend/src/pages/inbounds/clients/AttachClientsModal.tsx
  40. 0 0
      frontend/src/pages/inbounds/clients/DetachClientsModal.tsx
  41. 3 0
      frontend/src/pages/inbounds/clients/index.ts
  42. 123 0
      frontend/src/pages/inbounds/form/FallbacksCard.tsx
  43. 0 0
      frontend/src/pages/inbounds/form/InboundFormModal.css
  44. 868 0
      frontend/src/pages/inbounds/form/InboundFormModal.tsx
  45. 67 0
      frontend/src/pages/inbounds/form/SniffingTab.tsx
  46. 184 0
      frontend/src/pages/inbounds/form/advanced-editors.tsx
  47. 1 0
      frontend/src/pages/inbounds/form/index.ts
  48. 47 0
      frontend/src/pages/inbounds/form/protocols/accounts-list.tsx
  49. 20 0
      frontend/src/pages/inbounds/form/protocols/http.tsx
  50. 16 8
      frontend/src/pages/inbounds/form/protocols/hysteria.tsx
  51. 8 0
      frontend/src/pages/inbounds/form/protocols/index.ts
  52. 33 0
      frontend/src/pages/inbounds/form/protocols/mixed.tsx
  53. 67 0
      frontend/src/pages/inbounds/form/protocols/shadowsocks.tsx
  54. 93 0
      frontend/src/pages/inbounds/form/protocols/tun.tsx
  55. 37 0
      frontend/src/pages/inbounds/form/protocols/tunnel.tsx
  56. 60 0
      frontend/src/pages/inbounds/form/protocols/vless.tsx
  57. 120 0
      frontend/src/pages/inbounds/form/protocols/wireguard.tsx
  58. 2 0
      frontend/src/pages/inbounds/form/security/index.ts
  59. 143 0
      frontend/src/pages/inbounds/form/security/reality.tsx
  60. 309 0
      frontend/src/pages/inbounds/form/security/tls.tsx
  61. 136 0
      frontend/src/pages/inbounds/form/transport/external-proxy.tsx
  62. 29 0
      frontend/src/pages/inbounds/form/transport/grpc.tsx
  63. 37 0
      frontend/src/pages/inbounds/form/transport/httpupgrade.tsx
  64. 8 0
      frontend/src/pages/inbounds/form/transport/index.ts
  65. 34 0
      frontend/src/pages/inbounds/form/transport/kcp.tsx
  66. 164 0
      frontend/src/pages/inbounds/form/transport/raw.tsx
  67. 270 0
      frontend/src/pages/inbounds/form/transport/sockopt.tsx
  68. 37 0
      frontend/src/pages/inbounds/form/transport/ws.tsx
  69. 218 0
      frontend/src/pages/inbounds/form/transport/xhttp.tsx
  70. 187 0
      frontend/src/pages/inbounds/form/useInboundFallbacks.ts
  71. 205 0
      frontend/src/pages/inbounds/form/useSecurityActions.ts
  72. 0 0
      frontend/src/pages/inbounds/info/InboundInfoModal.css
  73. 12 262
      frontend/src/pages/inbounds/info/InboundInfoModal.tsx
  74. 170 0
      frontend/src/pages/inbounds/info/helpers.ts
  75. 1 0
      frontend/src/pages/inbounds/info/index.ts
  76. 87 0
      frontend/src/pages/inbounds/info/types.ts
  77. 0 0
      frontend/src/pages/inbounds/list/InboundList.css
  78. 203 0
      frontend/src/pages/inbounds/list/InboundList.tsx
  79. 141 0
      frontend/src/pages/inbounds/list/InboundStatsModal.tsx
  80. 81 0
      frontend/src/pages/inbounds/list/RowActions.tsx
  81. 106 0
      frontend/src/pages/inbounds/list/helpers.ts
  82. 2 0
      frontend/src/pages/inbounds/list/index.ts
  83. 88 0
      frontend/src/pages/inbounds/list/types.ts
  84. 290 0
      frontend/src/pages/inbounds/list/useInboundColumns.tsx
  85. 1 1
      frontend/src/pages/inbounds/qr/QrCodeModal.tsx
  86. 0 0
      frontend/src/pages/inbounds/qr/QrPanel.css
  87. 0 0
      frontend/src/pages/inbounds/qr/QrPanel.tsx
  88. 2 0
      frontend/src/pages/inbounds/qr/index.ts
  89. 3 3
      frontend/src/pages/index/IndexPage.tsx
  90. 1 1
      frontend/src/pages/index/SystemHistoryModal.tsx
  91. 1 1
      frontend/src/pages/index/XrayMetricsModal.tsx
  92. 1 1
      frontend/src/pages/nodes/NodeHistoryPanel.tsx
  93. 1 1
      frontend/src/pages/nodes/NodesPage.tsx
  94. 1 1
      frontend/src/pages/settings/GeneralTab.tsx
  95. 1 1
      frontend/src/pages/settings/SecurityTab.tsx
  96. 1 1
      frontend/src/pages/settings/SettingsPage.tsx
  97. 1 1
      frontend/src/pages/settings/SubscriptionFormatsTab.tsx
  98. 1 1
      frontend/src/pages/settings/SubscriptionGeneralTab.tsx
  99. 1 1
      frontend/src/pages/settings/TelegramTab.tsx
  100. 0 2238
      frontend/src/pages/xray/OutboundFormModal.tsx

+ 662 - 0
frontend/package-lock.json

@@ -31,6 +31,8 @@
       },
       },
       "devDependencies": {
       "devDependencies": {
         "@eslint/js": "^10.0.1",
         "@eslint/js": "^10.0.1",
+        "@testing-library/dom": "^10.4.1",
+        "@testing-library/react": "^16.3.2",
         "@types/react": "^19.2.15",
         "@types/react": "^19.2.15",
         "@types/react-dom": "^19.2.3",
         "@types/react-dom": "^19.2.3",
         "@types/swagger-ui-react": "^5.18.0",
         "@types/swagger-ui-react": "^5.18.0",
@@ -38,6 +40,7 @@
         "eslint": "^10.4.0",
         "eslint": "^10.4.0",
         "eslint-plugin-react-hooks": "^7.1.1",
         "eslint-plugin-react-hooks": "^7.1.1",
         "globals": "^17.6.0",
         "globals": "^17.6.0",
+        "jsdom": "^29.1.1",
         "typescript": "^6.0.3",
         "typescript": "^6.0.3",
         "typescript-eslint": "^8.60.0",
         "typescript-eslint": "^8.60.0",
         "vite": "8.0.14",
         "vite": "8.0.14",
@@ -141,6 +144,57 @@
         "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
         "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       }
       }
     },
     },
+    "node_modules/@asamuzakjp/css-color": {
+      "version": "5.1.11",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+      "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/generational-cache": "^1.0.1",
+        "@csstools/css-calc": "^3.2.0",
+        "@csstools/css-color-parser": "^4.1.0",
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@asamuzakjp/dom-selector": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+      "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/generational-cache": "^1.0.1",
+        "@asamuzakjp/nwsapi": "^2.3.9",
+        "bidi-js": "^1.0.3",
+        "css-tree": "^3.2.1",
+        "is-potential-custom-element-name": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@asamuzakjp/generational-cache": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+      "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@asamuzakjp/nwsapi": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+      "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@babel/code-frame": {
     "node_modules/@babel/code-frame": {
       "version": "7.29.7",
       "version": "7.29.7",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -402,6 +456,19 @@
         "node": ">=6.9.0"
         "node": ">=6.9.0"
       }
       }
     },
     },
+    "node_modules/@bramus/specificity": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+      "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "css-tree": "^3.0.0"
+      },
+      "bin": {
+        "specificity": "bin/cli.js"
+      }
+    },
     "node_modules/@codemirror/autocomplete": {
     "node_modules/@codemirror/autocomplete": {
       "version": "6.20.2",
       "version": "6.20.2",
       "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz",
       "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz",
@@ -505,6 +572,146 @@
         "w3c-keyname": "^2.2.4"
         "w3c-keyname": "^2.2.4"
       }
       }
     },
     },
+    "node_modules/@csstools/color-helpers": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+      "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/@csstools/css-calc": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+      "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-color-parser": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
+      "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/color-helpers": "^6.0.2",
+        "@csstools/css-calc": "^3.2.1"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-parser-algorithms": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+      "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-syntax-patches-for-csstree": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz",
+      "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "peerDependencies": {
+        "css-tree": "^3.2.1"
+      },
+      "peerDependenciesMeta": {
+        "css-tree": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@csstools/css-tokenizer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+      "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
     "node_modules/@emnapi/core": {
     "node_modules/@emnapi/core": {
       "version": "1.10.0",
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
       "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -679,6 +886,24 @@
         "node": "^20.19.0 || ^22.13.0 || >=24"
         "node": "^20.19.0 || ^22.13.0 || >=24"
       }
       }
     },
     },
+    "node_modules/@exodus/bytes": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+      "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@noble/hashes": "^1.8.0 || ^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@noble/hashes": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@humanfs/core": {
     "node_modules/@humanfs/core": {
       "version": "0.19.2",
       "version": "0.19.2",
       "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
       "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -2629,6 +2854,54 @@
         "react": "^18 || ^19"
         "react": "^18 || ^19"
       }
       }
     },
     },
+    "node_modules/@testing-library/dom": {
+      "version": "10.4.1",
+      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+      "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.10.4",
+        "@babel/runtime": "^7.12.5",
+        "@types/aria-query": "^5.0.1",
+        "aria-query": "5.3.0",
+        "dom-accessibility-api": "^0.5.9",
+        "lz-string": "^1.5.0",
+        "picocolors": "1.1.1",
+        "pretty-format": "^27.0.2"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@testing-library/react": {
+      "version": "16.3.2",
+      "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+      "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@testing-library/dom": "^10.0.0",
+        "@types/react": "^18.0.0 || ^19.0.0",
+        "@types/react-dom": "^18.0.0 || ^19.0.0",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@tybys/wasm-util": {
     "node_modules/@tybys/wasm-util": {
       "version": "0.10.2",
       "version": "0.10.2",
       "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
       "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -2640,6 +2913,13 @@
         "tslib": "^2.4.0"
         "tslib": "^2.4.0"
       }
       }
     },
     },
+    "node_modules/@types/aria-query": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+      "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/chai": {
     "node_modules/@types/chai": {
       "version": "5.2.3",
       "version": "5.2.3",
       "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
       "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -3249,6 +3529,29 @@
         "url": "https://github.com/sponsors/epoberezkin"
         "url": "https://github.com/sponsors/epoberezkin"
       }
       }
     },
     },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+      "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
     "node_modules/antd": {
     "node_modules/antd": {
       "version": "6.4.3",
       "version": "6.4.3",
       "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.3.tgz",
       "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.3.tgz",
@@ -3324,6 +3627,16 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "license": "Python-2.0"
       "license": "Python-2.0"
     },
     },
+    "node_modules/aria-query": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+      "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "dequal": "^2.0.3"
+      }
+    },
     "node_modules/assertion-error": {
     "node_modules/assertion-error": {
       "version": "2.0.1",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -3418,6 +3731,16 @@
         "node": ">=6.0.0"
         "node": ">=6.0.0"
       }
       }
     },
     },
+    "node_modules/bidi-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+      "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "require-from-string": "^2.0.2"
+      }
+    },
     "node_modules/brace-expansion": {
     "node_modules/brace-expansion": {
       "version": "5.0.6",
       "version": "5.0.6",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
@@ -3715,6 +4038,20 @@
         "node": ">= 8"
         "node": ">= 8"
       }
       }
     },
     },
+    "node_modules/css-tree": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+      "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mdn-data": "2.27.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+      }
+    },
     "node_modules/css.escape": {
     "node_modules/css.escape": {
       "version": "1.5.1",
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
       "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -3848,6 +4185,20 @@
         "node": ">=12"
         "node": ">=12"
       }
       }
     },
     },
+    "node_modules/data-urls": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+      "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^16.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
     "node_modules/dayjs": {
     "node_modules/dayjs": {
       "version": "1.11.21",
       "version": "1.11.21",
       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
@@ -3871,6 +4222,13 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/decimal.js": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+      "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/decimal.js-light": {
     "node_modules/decimal.js-light": {
       "version": "2.5.1",
       "version": "2.5.1",
       "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
       "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@@ -3941,6 +4299,16 @@
         "node": ">=0.4.0"
         "node": ">=0.4.0"
       }
       }
     },
     },
+    "node_modules/dequal": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/detect-libc": {
     "node_modules/detect-libc": {
       "version": "2.1.2",
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3951,6 +4319,13 @@
         "node": ">=8"
         "node": ">=8"
       }
       }
     },
     },
+    "node_modules/dom-accessibility-api": {
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+      "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/dompurify": {
     "node_modules/dompurify": {
       "version": "3.4.7",
       "version": "3.4.7",
       "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
       "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
@@ -3990,6 +4365,19 @@
       "dev": true,
       "dev": true,
       "license": "ISC"
       "license": "ISC"
     },
     },
+    "node_modules/entities": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+      "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
     "node_modules/es-define-property": {
     "node_modules/es-define-property": {
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -4663,6 +5051,19 @@
       "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
       "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
       "license": "CC0-1.0"
       "license": "CC0-1.0"
     },
     },
+    "node_modules/html-encoding-sniffer": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+      "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.6.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
     "node_modules/html-parse-stringify": {
     "node_modules/html-parse-stringify": {
       "version": "3.0.1",
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
       "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -4881,6 +5282,13 @@
       "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
       "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/is-typed-array": {
     "node_modules/is-typed-array": {
       "version": "1.1.15",
       "version": "1.1.15",
       "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
       "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
@@ -4933,6 +5341,57 @@
         "js-yaml": "bin/js-yaml.js"
         "js-yaml": "bin/js-yaml.js"
       }
       }
     },
     },
+    "node_modules/jsdom": {
+      "version": "29.1.1",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+      "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/css-color": "^5.1.11",
+        "@asamuzakjp/dom-selector": "^7.1.1",
+        "@bramus/specificity": "^2.4.2",
+        "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+        "@exodus/bytes": "^1.15.0",
+        "css-tree": "^3.2.1",
+        "data-urls": "^7.0.0",
+        "decimal.js": "^10.6.0",
+        "html-encoding-sniffer": "^6.0.0",
+        "is-potential-custom-element-name": "^1.0.1",
+        "lru-cache": "^11.3.5",
+        "parse5": "^8.0.1",
+        "saxes": "^6.0.0",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^6.0.1",
+        "undici": "^7.25.0",
+        "w3c-xmlserializer": "^5.0.0",
+        "webidl-conversions": "^8.0.1",
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^16.0.1",
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "canvas": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jsdom/node_modules/lru-cache": {
+      "version": "11.5.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+      "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
     "node_modules/jsesc": {
     "node_modules/jsesc": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -5350,6 +5809,16 @@
         "yallist": "^3.0.2"
         "yallist": "^3.0.2"
       }
       }
     },
     },
+    "node_modules/lz-string": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+      "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "lz-string": "bin/bin.js"
+      }
+    },
     "node_modules/magic-string": {
     "node_modules/magic-string": {
       "version": "0.30.21",
       "version": "0.30.21",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5369,6 +5838,13 @@
         "node": ">= 0.4"
         "node": ">= 0.4"
       }
       }
     },
     },
+    "node_modules/mdn-data": {
+      "version": "2.27.1",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+      "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
     "node_modules/mime-db": {
     "node_modules/mime-db": {
       "version": "1.52.0",
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -5639,6 +6115,19 @@
       "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
       "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/parse5": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+      "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^8.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
     "node_modules/path-exists": {
     "node_modules/path-exists": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5743,6 +6232,28 @@
         "node": ">= 0.8.0"
         "node": ">= 0.8.0"
       }
       }
     },
     },
+    "node_modules/pretty-format": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+      "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1",
+        "ansi-styles": "^5.0.0",
+        "react-is": "^17.0.1"
+      },
+      "engines": {
+        "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+      }
+    },
+    "node_modules/pretty-format/node_modules/react-is": {
+      "version": "17.0.2",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+      "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/prismjs": {
     "node_modules/prismjs": {
       "version": "1.30.0",
       "version": "1.30.0",
       "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
       "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -6165,6 +6676,16 @@
         "node": ">=0.10"
         "node": ">=0.10"
       }
       }
     },
     },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/requires-port": {
     "node_modules/requires-port": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -6240,6 +6761,19 @@
       ],
       ],
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/saxes": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "xmlchars": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=v12.22.7"
+      }
+    },
     "node_modules/scheduler": {
     "node_modules/scheduler": {
       "version": "0.27.0",
       "version": "0.27.0",
       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -6568,6 +7102,13 @@
         "react-dom": ">=16.8.0 <20"
         "react-dom": ">=16.8.0 <20"
       }
       }
     },
     },
+    "node_modules/symbol-tree": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/throttle-debounce": {
     "node_modules/throttle-debounce": {
       "version": "5.0.2",
       "version": "5.0.2",
       "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
       "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
@@ -6627,6 +7168,26 @@
         "node": ">=14.0.0"
         "node": ">=14.0.0"
       }
       }
     },
     },
+    "node_modules/tldts": {
+      "version": "7.4.2",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz",
+      "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tldts-core": "^7.4.2"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "7.4.2",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz",
+      "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/to-buffer": {
     "node_modules/to-buffer": {
       "version": "1.2.2",
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
       "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -6647,6 +7208,32 @@
       "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
       "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/tough-cookie": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+      "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tldts": "^7.0.5"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+      "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
     "node_modules/tree-sitter": {
     "node_modules/tree-sitter": {
       "version": "0.21.1",
       "version": "0.21.1",
       "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
       "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
@@ -6796,6 +7383,16 @@
         "typescript": ">=4.8.4 <6.1.0"
         "typescript": ">=4.8.4 <6.1.0"
       }
       }
     },
     },
+    "node_modules/undici": {
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz",
+      "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
     "node_modules/unraw": {
     "node_modules/unraw": {
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz",
@@ -7067,6 +7664,19 @@
       "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
       "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/w3c-xmlserializer": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/web-tree-sitter": {
     "node_modules/web-tree-sitter": {
       "version": "0.24.5",
       "version": "0.24.5",
       "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.5.tgz",
       "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.5.tgz",
@@ -7074,6 +7684,41 @@
       "license": "MIT",
       "license": "MIT",
       "optional": true
       "optional": true
     },
     },
+    "node_modules/webidl-conversions": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+      "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+      "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+      "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.11.0",
+        "tr46": "^6.0.0",
+        "webidl-conversions": "^8.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
     "node_modules/which": {
     "node_modules/which": {
       "version": "2.0.2",
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7153,6 +7798,23 @@
         "repeat-string": "^1.5.2"
         "repeat-string": "^1.5.2"
       }
       }
     },
     },
+    "node_modules/xml-name-validator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/yallist": {
     "node_modules/yallist": {
       "version": "3.1.1",
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

+ 3 - 0
frontend/package.json

@@ -43,6 +43,8 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@eslint/js": "^10.0.1",
     "@eslint/js": "^10.0.1",
+    "@testing-library/dom": "^10.4.1",
+    "@testing-library/react": "^16.3.2",
     "@types/react": "^19.2.15",
     "@types/react": "^19.2.15",
     "@types/react-dom": "^19.2.3",
     "@types/react-dom": "^19.2.3",
     "@types/swagger-ui-react": "^5.18.0",
     "@types/swagger-ui-react": "^5.18.0",
@@ -50,6 +52,7 @@
     "eslint": "^10.4.0",
     "eslint": "^10.4.0",
     "eslint-plugin-react-hooks": "^7.1.1",
     "eslint-plugin-react-hooks": "^7.1.1",
     "globals": "^17.6.0",
     "globals": "^17.6.0",
+    "jsdom": "^29.1.1",
     "typescript": "^6.0.3",
     "typescript": "^6.0.3",
     "typescript-eslint": "^8.60.0",
     "typescript-eslint": "^8.60.0",
     "vite": "8.0.14",
     "vite": "8.0.14",

+ 0 - 0
frontend/src/components/PromptModal.tsx → frontend/src/components/feedback/PromptModal.tsx


+ 0 - 0
frontend/src/components/TextModal.tsx → frontend/src/components/feedback/TextModal.tsx


+ 2 - 0
frontend/src/components/feedback/index.ts

@@ -0,0 +1,2 @@
+export { default as PromptModal } from './PromptModal';
+export { default as TextModal } from './TextModal';

+ 0 - 0
frontend/src/components/DateTimePicker.css → frontend/src/components/form/DateTimePicker.css


+ 0 - 0
frontend/src/components/DateTimePicker.tsx → frontend/src/components/form/DateTimePicker.tsx


+ 1 - 1
frontend/src/components/HeaderMapEditor.tsx → frontend/src/components/form/HeaderMapEditor.tsx

@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
 import { Button, Input, Space } from 'antd';
 import { Button, Input, Space } from 'antd';
 import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
 import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
 
 
-import InputAddon from '@/components/InputAddon';
+import { InputAddon } from '@/components/ui';
 
 
 // Reusable header-map editor. Handles the two wire shapes Xray uses for
 // Reusable header-map editor. Handles the two wire shapes Xray uses for
 // HTTP-style header maps:
 // HTTP-style header maps:

+ 0 - 0
frontend/src/components/JsonEditor.css → frontend/src/components/form/JsonEditor.css


+ 0 - 0
frontend/src/components/JsonEditor.tsx → frontend/src/components/form/JsonEditor.tsx


+ 3 - 0
frontend/src/components/form/index.ts

@@ -0,0 +1,3 @@
+export { default as DateTimePicker } from './DateTimePicker';
+export { default as JsonEditor } from './JsonEditor';
+export { default as HeaderMapEditor } from './HeaderMapEditor';

+ 0 - 0
frontend/src/components/InfinityIcon.tsx → frontend/src/components/ui/InfinityIcon.tsx


+ 0 - 0
frontend/src/components/InputAddon.css → frontend/src/components/ui/InputAddon.css


+ 0 - 0
frontend/src/components/InputAddon.tsx → frontend/src/components/ui/InputAddon.tsx


+ 0 - 0
frontend/src/components/SettingListItem.css → frontend/src/components/ui/SettingListItem.css


+ 0 - 0
frontend/src/components/SettingListItem.tsx → frontend/src/components/ui/SettingListItem.tsx


+ 3 - 0
frontend/src/components/ui/index.ts

@@ -0,0 +1,3 @@
+export { default as InputAddon } from './InputAddon';
+export { default as InfinityIcon } from './InfinityIcon';
+export { default as SettingListItem } from './SettingListItem';

+ 0 - 0
frontend/src/components/LazyMount.tsx → frontend/src/components/utility/LazyMount.tsx


+ 1 - 0
frontend/src/components/utility/index.ts

@@ -0,0 +1 @@
+export { default as LazyMount } from './LazyMount';

+ 0 - 0
frontend/src/components/Sparkline.css → frontend/src/components/viz/Sparkline.css


+ 0 - 0
frontend/src/components/Sparkline.tsx → frontend/src/components/viz/Sparkline.tsx


+ 1 - 0
frontend/src/components/viz/index.ts

@@ -0,0 +1 @@
+export { default as Sparkline } from './Sparkline';

+ 0 - 0
frontend/src/components/AppSidebar.css → frontend/src/layouts/AppSidebar.css


+ 0 - 0
frontend/src/components/AppSidebar.tsx → frontend/src/layouts/AppSidebar.tsx


+ 0 - 0
frontend/src/components/FinalMaskForm.tsx → frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx


+ 1 - 0
frontend/src/lib/xray/forms/transport/index.ts

@@ -0,0 +1 @@
+export { default as FinalMaskForm } from './FinalMaskForm';

+ 1 - 1
frontend/src/lib/xray/inbound-link.ts

@@ -2,7 +2,7 @@ import { Base64, Wireguard } from '@/utils';
 
 
 import type { Inbound } from '@/schemas/api/inbound';
 import type { Inbound } from '@/schemas/api/inbound';
 import type { VlessClient } from '@/schemas/protocols/inbound/vless';
 import type { VlessClient } from '@/schemas/protocols/inbound/vless';
-import type { VmessSecurity } from '@/schemas/protocols/inbound/vmess';
+import type { VmessSecurity } from '@/schemas/protocols/shared/vmess';
 import type {
 import type {
   WireguardInboundPeer,
   WireguardInboundPeer,
   WireguardInboundSettings,
   WireguardInboundSettings,

+ 1 - 1
frontend/src/pages/api-docs/ApiDocsPage.tsx

@@ -4,7 +4,7 @@ import SwaggerUI from 'swagger-ui-react';
 import 'swagger-ui-react/swagger-ui.css';
 import 'swagger-ui-react/swagger-ui.css';
 
 
 import { useTheme } from '@/hooks/useTheme';
 import { useTheme } from '@/hooks/useTheme';
-import AppSidebar from '@/components/AppSidebar';
+import AppSidebar from '@/layouts/AppSidebar';
 import './ApiDocsPage.css';
 import './ApiDocsPage.css';
 
 
 const basePath = window.X_UI_BASE_PATH || '';
 const basePath = window.X_UI_BASE_PATH || '';

+ 1 - 1
frontend/src/pages/clients/ClientBulkAddModal.tsx

@@ -7,7 +7,7 @@ import type { Dayjs } from 'dayjs';
 
 
 import { RandomUtil, SizeFormatter } from '@/utils';
 import { RandomUtil, SizeFormatter } from '@/utils';
 import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
 import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
-import DateTimePicker from '@/components/DateTimePicker';
+import { DateTimePicker } from '@/components/form';
 import { useClients, type InboundOption } from '@/hooks/useClients';
 import { useClients, type InboundOption } from '@/hooks/useClients';
 import { ClientBulkAddFormSchema, type ClientBulkAddFormValues } from '@/schemas/client';
 import { ClientBulkAddFormSchema, type ClientBulkAddFormValues } from '@/schemas/client';
 
 

+ 1 - 1
frontend/src/pages/clients/ClientFormModal.tsx

@@ -20,7 +20,7 @@ import dayjs from 'dayjs';
 import type { Dayjs } from 'dayjs';
 import type { Dayjs } from 'dayjs';
 
 
 import { HttpUtil, RandomUtil } from '@/utils';
 import { HttpUtil, RandomUtil } from '@/utils';
-import DateTimePicker from '@/components/DateTimePicker';
+import { DateTimePicker } from '@/components/form';
 import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
 import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';
 import { ClientFormSchema, ClientCreateFormSchema } from '@/schemas/client';

+ 1 - 1
frontend/src/pages/clients/ClientInfoModal.tsx

@@ -7,7 +7,7 @@ import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 import { isPostQuantumLink } from '@/lib/xray/inbound-link';
 import { isPostQuantumLink } from '@/lib/xray/inbound-link';
-import QrPanel from '@/pages/inbounds/QrPanel';
+import { QrPanel } from '@/pages/inbounds/qr';
 import './ClientInfoModal.css';
 import './ClientInfoModal.css';
 
 
 const PROTOCOL_COLORS: Record<string, string> = {
 const PROTOCOL_COLORS: Record<string, string> = {

+ 1 - 1
frontend/src/pages/clients/ClientQrModal.tsx

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 import { Collapse, Modal, Spin } from 'antd';
 import { Collapse, Modal, Spin } from 'antd';
 import { HttpUtil } from '@/utils';
 import { HttpUtil } from '@/utils';
 import { isPostQuantumLink } from '@/lib/xray/inbound-link';
 import { isPostQuantumLink } from '@/lib/xray/inbound-link';
-import QrPanel from '@/pages/inbounds/QrPanel';
+import { QrPanel } from '@/pages/inbounds/qr';
 import type { ClientRecord } from '@/hooks/useClients';
 import type { ClientRecord } from '@/hooks/useClients';
 
 
 interface SubSettings {
 interface SubSettings {

+ 2 - 2
frontend/src/pages/clients/ClientsPage.tsx

@@ -51,10 +51,10 @@ import { useWebSocket } from '@/hooks/useWebSocket';
 import { useClients } from '@/hooks/useClients';
 import { useClients } from '@/hooks/useClients';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
-import AppSidebar from '@/components/AppSidebar';
+import AppSidebar from '@/layouts/AppSidebar';
 import { IntlUtil, SizeFormatter } from '@/utils';
 import { IntlUtil, SizeFormatter } from '@/utils';
 import { setMessageInstance } from '@/utils/messageBus';
 import { setMessageInstance } from '@/utils/messageBus';
-import LazyMount from '@/components/LazyMount';
+import { LazyMount } from '@/components/utility';
 const ClientFormModal = lazy(() => import('./ClientFormModal'));
 const ClientFormModal = lazy(() => import('./ClientFormModal'));
 const ClientInfoModal = lazy(() => import('./ClientInfoModal'));
 const ClientInfoModal = lazy(() => import('./ClientInfoModal'));
 const ClientQrModal = lazy(() => import('./ClientQrModal'));
 const ClientQrModal = lazy(() => import('./ClientQrModal'));

+ 2 - 2
frontend/src/pages/groups/GroupsPage.tsx

@@ -42,8 +42,8 @@ import { usePageTitle } from '@/hooks/usePageTitle';
 import { useClients } from '@/hooks/useClients';
 import { useClients } from '@/hooks/useClients';
 import { HttpUtil } from '@/utils';
 import { HttpUtil } from '@/utils';
 import { setMessageInstance } from '@/utils/messageBus';
 import { setMessageInstance } from '@/utils/messageBus';
-import AppSidebar from '@/components/AppSidebar';
-import LazyMount from '@/components/LazyMount';
+import AppSidebar from '@/layouts/AppSidebar';
+import { LazyMount } from '@/components/utility';
 import { keys } from '@/api/queryKeys';
 import { keys } from '@/api/queryKeys';
 import {
 import {
   ClientRecordSchema,
   ClientRecordSchema,

+ 0 - 3129
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -1,3129 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import dayjs from 'dayjs';
-import {
-  Button,
-  Card,
-  Checkbox,
-  Divider,
-  Empty,
-  Form,
-  Input,
-  InputNumber,
-  Modal,
-  Radio,
-  Select,
-  Space,
-  Switch,
-  Tabs,
-  Tooltip,
-  Typography,
-  message,
-} from 'antd';
-import {
-  ArrowDownOutlined,
-  ArrowUpOutlined,
-  DeleteOutlined,
-  MinusOutlined,
-  PlusOutlined,
-  ReloadOutlined,
-} from '@ant-design/icons';
-
-import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
-import {
-  rawInboundToFormValues,
-  formValuesToWirePayload,
-  pruneEmpty,
-  normalizeSniffing,
-  normalizeClients,
-  dropLegacyOptionalEmpties,
-} from '@/lib/xray/inbound-form-adapter';
-import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
-import {
-  canEnableReality,
-  canEnableStream,
-  canEnableTls,
-  isSS2022,
-} from '@/lib/xray/protocol-capabilities';
-import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
-import { getRandomRealityTarget } from '@/models/reality-targets';
-import {
-  InboundFormBaseSchema,
-  InboundFormSchema,
-  type FallbackRow,
-  type InboundFormValues,
-} from '@/schemas/forms/inbound-form';
-import { antdRule } from '@/utils/zodForm';
-import {
-  ALPN_OPTION,
-  Address_Port_Strategy,
-  DOMAIN_STRATEGY_OPTION,
-  Protocols,
-  SNIFFING_OPTION,
-  TCP_CONGESTION_OPTION,
-  TLS_CIPHER_OPTION,
-  TLS_VERSION_OPTION,
-  USAGE_OPTION,
-  UTLS_FINGERPRINT,
-} from '@/schemas/primitives';
-import {
-  HappyEyeballsSchema,
-  SockoptStreamSettingsSchema,
-} from '@/schemas/protocols/stream/sockopt';
-import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
-import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
-import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
-import { SniffingSchema } from '@/schemas/primitives/sniffing';
-import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
-import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
-import { WsStreamSettingsSchema } from '@/schemas/protocols/stream/ws';
-import { GrpcStreamSettingsSchema } from '@/schemas/protocols/stream/grpc';
-import { HttpUpgradeStreamSettingsSchema } from '@/schemas/protocols/stream/httpupgrade';
-import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
-import DateTimePicker from '@/components/DateTimePicker';
-import FinalMaskForm from '@/components/FinalMaskForm';
-import HeaderMapEditor from '@/components/HeaderMapEditor';
-import HysteriaMasqueradeForm from '@/components/HysteriaMasqueradeForm';
-import InputAddon from '@/components/InputAddon';
-import JsonEditor from '@/components/JsonEditor';
-import './InboundFormModal.css';
-import type { FormInstance } from 'antd';
-import type { NamePath } from 'antd/es/form/interface';
-
-const { TextArea } = Input;
-import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
-import type { NodeRecord } from '@/api/queries/useNodesQuery';
-
-// Pattern A rewrite of InboundFormModal. Built as a sibling file so the
-// build stays green while the rewrite progresses section by section.
-// InboundsPage continues to render the old InboundFormModal.tsx until the
-// atomic swap at the end (Core Decision 7).
-
-const { Text } = Typography;
-
-// Sub-editor for one slice of the form (settings, streamSettings, sniffing).
-// Holds a local text buffer so the user can type freely; on every keystroke
-// we try to JSON.parse and forward the result to form state. Invalid JSON
-// is held in the buffer until the next valid moment — no panic on partial
-// input. The buffer seeds once on mount; the modal's destroyOnHidden makes
-// each open a fresh editor instance, so we don't need to re-sync on outer
-// form changes.
-function AdvancedSliceEditor({
-  form,
-  path,
-  wrapKey,
-  minHeight,
-  maxHeight,
-}: {
-  form: FormInstance<InboundFormValues>;
-  path: NamePath;
-  // When set, the editor wraps the inner value with `{ [wrapKey]: ... }` so
-  // the JSON the user sees matches the wire shape's slice envelope (e.g.
-  // `{ "settings": { ... } }`). Edits unwrap the outer key before writing
-  // back to the form. Mirrors the legacy modal's wrappedConfigValue.
-  wrapKey?: string;
-  minHeight?: string;
-  maxHeight?: string;
-}) {
-  const serialize = (value: unknown): string => {
-    const inner = value ?? {};
-    return JSON.stringify(wrapKey ? { [wrapKey]: inner } : inner, null, 2);
-  };
-
-  // preserve: true so useWatch returns the full subtree from the form
-  // store — without it, useWatch goes through getFieldsValue() which
-  // filters out unregistered fields. Slices like `settings` would lose
-  // their `clients` / `fallbacks` sub-trees because those aren't bound
-  // to any Form.Item.
-  const watched = Form.useWatch(path, { form, preserve: true });
-  const lastEmitRef = useRef<string>('');
-  const [text, setText] = useState(() => {
-    const initial = serialize(form.getFieldValue(path));
-    lastEmitRef.current = initial;
-    return initial;
-  });
-
-  useEffect(() => {
-    const formStr = serialize(watched);
-    if (formStr === lastEmitRef.current) return;
-    setText(formStr);
-    lastEmitRef.current = formStr;
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [watched, wrapKey]);
-
-  return (
-    <JsonEditor
-      value={text}
-      minHeight={minHeight}
-      maxHeight={maxHeight}
-      onChange={(next) => {
-        setText(next);
-        try {
-          const parsed = JSON.parse(next);
-          const toWrite = wrapKey && parsed && typeof parsed === 'object' && !Array.isArray(parsed)
-            ? (parsed as Record<string, unknown>)[wrapKey] ?? {}
-            : parsed;
-          form.setFieldValue(path, toWrite);
-          lastEmitRef.current = JSON.stringify(wrapKey ? { [wrapKey]: toWrite } : toWrite, null, 2);
-        } catch {
-          // invalid JSON; keep buffer, don't push to form
-        }
-      }}
-    />
-  );
-}
-
-// The "All" editor shows the full inbound JSON in one editor: top-level
-// connection fields plus the three nested sub-objects (settings,
-// streamSettings, sniffing). Edits round-trip back to the form's slices,
-// mirroring the legacy modal's setAdvancedAllValue behavior. Reactivity
-// works the same way as AdvancedSliceEditor: useWatch on the slices we
-// care about, lastEmitRef as the "we wrote this" guard.
-function AdvancedAllEditor({
-  form,
-  streamEnabled,
-}: {
-  form: FormInstance<InboundFormValues>;
-  streamEnabled: boolean;
-}) {
-  // preserve: true — default useWatch returns only registered fields, so
-  // sub-trees we never bound (settings.clients/fallbacks, sniffing
-  // defaults, etc.) wouldn't show up. preserve switches the read to
-  // getFieldsValue(true) which returns the full form store.
-  const wListen = Form.useWatch('listen', { form, preserve: true });
-  const wPort = Form.useWatch('port', { form, preserve: true });
-  const wProtocol = Form.useWatch('protocol', { form, preserve: true });
-  const wTag = Form.useWatch('tag', { form, preserve: true });
-  const wSettings = Form.useWatch('settings', { form, preserve: true });
-  const wSniffing = Form.useWatch('sniffing', { form, preserve: true });
-  const wStream = Form.useWatch('streamSettings', { form, preserve: true });
-
-  const serialize = () => {
-    // Apply the same prune/normalize as the wire payload so the JSON
-    // shown here is what the panel actually POSTs (no empty defaults,
-    // disabled sniffing as { enabled: false }, finalmask dropped when
-    // there are no masks).
-    const settingsView = (pruneEmpty(wSettings ?? {}) ?? {}) as Record<string, unknown>;
-    if (typeof wProtocol === 'string' && Array.isArray(settingsView.clients)) {
-      settingsView.clients = normalizeClients(wProtocol, settingsView.clients);
-    }
-    const streamView = streamEnabled
-      ? ((pruneEmpty(wStream ?? {}) ?? {}) as Record<string, unknown>)
-      : undefined;
-    dropLegacyOptionalEmpties(settingsView, streamView);
-    const out: Record<string, unknown> = {
-      listen: wListen ?? '',
-      port: wPort ?? 0,
-      protocol: wProtocol ?? '',
-      tag: wTag ?? '',
-      settings: settingsView,
-      sniffing: normalizeSniffing(wSniffing as Parameters<typeof normalizeSniffing>[0]),
-    };
-    if (streamView) out.streamSettings = streamView;
-    return JSON.stringify(out, null, 2);
-  };
-
-  const lastEmitRef = useRef<string>('');
-  const [text, setText] = useState(() => {
-    const initial = serialize();
-    lastEmitRef.current = initial;
-    return initial;
-  });
-
-  useEffect(() => {
-    const formStr = serialize();
-    if (formStr === lastEmitRef.current) return;
-    setText(formStr);
-    lastEmitRef.current = formStr;
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [wListen, wPort, wProtocol, wTag, wSettings, wSniffing, wStream, streamEnabled]);
-
-  return (
-    <JsonEditor
-      value={text}
-      minHeight="340px"
-      maxHeight="560px"
-      onChange={(next) => {
-        setText(next);
-        let parsed: Record<string, unknown>;
-        try {
-          parsed = JSON.parse(next) as Record<string, unknown>;
-        } catch {
-          return;
-        }
-        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return;
-        if (typeof parsed.listen === 'string') form.setFieldValue('listen', parsed.listen);
-        if (typeof parsed.port === 'number' && Number.isFinite(parsed.port)) {
-          form.setFieldValue('port', parsed.port);
-        }
-        if (typeof parsed.protocol === 'string') form.setFieldValue('protocol', parsed.protocol);
-        if (typeof parsed.tag === 'string') form.setFieldValue('tag', parsed.tag);
-        if (parsed.settings && typeof parsed.settings === 'object') {
-          form.setFieldValue('settings', parsed.settings);
-        }
-        if (parsed.sniffing && typeof parsed.sniffing === 'object') {
-          form.setFieldValue('sniffing', parsed.sniffing);
-        }
-        if (streamEnabled && parsed.streamSettings && typeof parsed.streamSettings === 'object') {
-          form.setFieldValue('streamSettings', parsed.streamSettings);
-        }
-        lastEmitRef.current = next;
-      }}
-    />
-  );
-}
-
-const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
-const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
-const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
-  Protocols.VLESS,
-  Protocols.VMESS,
-  Protocols.TROJAN,
-  Protocols.SHADOWSOCKS,
-  Protocols.HYSTERIA,
-  Protocols.WIREGUARD,
-]);
-
-interface InboundFormModalProps {
-  open: boolean;
-  onClose: () => void;
-  onSaved: () => void;
-  mode: 'add' | 'edit';
-  dbInbound: DBInbound | null;
-  dbInbounds: DBInbound[];
-  availableNodes?: NodeRecord[];
-}
-
-function buildAddModeValues(): InboundFormValues {
-  const settings = createDefaultInboundSettings('vless') ?? undefined;
-  return rawInboundToFormValues({
-    protocol: 'vless',
-    settings,
-    streamSettings: {
-      network: 'tcp',
-      security: 'none',
-      tcpSettings: TcpStreamSettingsSchema.parse({ header: { type: 'none' } }),
-    },
-    sniffing: SniffingSchema.parse({}),
-    port: RandomUtil.randomInteger(10000, 60000),
-    listen: '',
-    tag: '',
-    enable: true,
-    trafficReset: 'never',
-  });
-}
-
-export default function InboundFormModal({
-  open,
-  onClose,
-  onSaved,
-  mode,
-  dbInbound,
-  dbInbounds,
-  availableNodes,
-}: InboundFormModalProps) {
-  const { t } = useTranslation();
-  const [messageApi, messageContextHolder] = message.useMessage();
-  const [form] = Form.useForm<InboundFormValues>();
-  const [saving, setSaving] = useState(false);
-  const fallbackKeyRef = useRef(0);
-  const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
-
-  const selectableNodes = (availableNodes || []).filter((n) => n.enable);
-  const protocol = (Form.useWatch('protocol', form) ?? '') as string;
-  const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol);
-  const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false;
-  const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? '';
-  const ssMethod = Form.useWatch(['settings', 'method'], form);
-  const isSSWith2022 = isSS2022({
-    protocol,
-    settings: typeof ssMethod === 'string' ? { method: ssMethod } : {},
-  });
-  const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false;
-  const network = Form.useWatch(['streamSettings', 'network'], form) ?? '';
-  const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none';
-  const streamEnabled = canEnableStream({ protocol });
-  const isFallbackHost =
-    (protocol === Protocols.VLESS || protocol === Protocols.TROJAN)
-    && network === 'tcp'
-    && (security === 'tls' || security === 'reality');
-
-  const fallbackChildOptions = (dbInbounds || [])
-    .filter((ib) => ib.id !== dbInbound?.id)
-    .map((ib) => ({
-      label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
-      value: ib.id,
-    }));
-
-  const loadFallbacks = async (masterId: number | null) => {
-    if (!masterId) {
-      setFallbacks([]);
-      return;
-    }
-    const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
-    if (!msg?.success || !Array.isArray(msg.obj)) {
-      setFallbacks([]);
-      return;
-    }
-    setFallbacks(
-      (msg.obj as {
-        childId: number;
-        name?: string;
-        alpn?: string;
-        path?: string;
-        dest?: string;
-        xver?: number;
-      }[])
-        .map((r) => ({
-          rowKey: `fb-${++fallbackKeyRef.current}`,
-          childId: r.childId,
-          name: r.name || '',
-          alpn: r.alpn || '',
-          path: r.path || '',
-          dest: r.dest || '',
-          xver: r.xver || 0,
-        })),
-    );
-  };
-
-  const saveFallbacks = async (masterId: number) => {
-    if (!masterId) return true;
-    const payload = {
-      fallbacks: fallbacks.filter((c) => c.childId).map((c, i) => ({
-        childId: c.childId,
-        name: c.name,
-        alpn: c.alpn,
-        path: c.path,
-        dest: c.dest,
-        xver: Number(c.xver) || 0,
-        sortOrder: i,
-      })),
-    };
-    const msg = await HttpUtil.post(
-      `/panel/api/inbounds/${masterId}/fallbacks`,
-      payload,
-      { headers: { 'Content-Type': 'application/json' } },
-    );
-    return !!msg?.success;
-  };
-
-  // Derive a fallback row's SNI / ALPN / Path / xver from a child
-  // inbound's streamSettings — what the legacy panel auto-filled when an
-  // operator wired a fallback target. SNI/ALPN come straight off the
-  // child's TLS block; path depends on the child's transport (ws/grpc
-  // /httpupgrade carry an explicit path; tcp/kcp/xhttp have no path of
-  // their own). xver stays 0 unless the child explicitly opts in via
-  // PROXY-protocol sockopt.
-  const deriveFallbackDefaults = (childId: number): Partial<FallbackRow> => {
-    const child = (dbInbounds || []).find((ib) => ib.id === childId);
-    if (!child) return {};
-    const stream = coerceInboundJsonField(child.streamSettings);
-    const tls = (stream.tlsSettings as Record<string, unknown> | undefined) ?? {};
-    const network = typeof stream.network === 'string' ? stream.network : '';
-    const sni = typeof tls.serverName === 'string' ? tls.serverName : '';
-    const alpnArr = Array.isArray(tls.alpn) ? tls.alpn : [];
-    const alpn = alpnArr.filter((v) => typeof v === 'string').join(',');
-    let path = '';
-    if (network === 'ws') {
-      const ws = (stream.wsSettings as Record<string, unknown> | undefined) ?? {};
-      if (typeof ws.path === 'string') path = ws.path;
-    } else if (network === 'grpc') {
-      const grpc = (stream.grpcSettings as Record<string, unknown> | undefined) ?? {};
-      if (typeof grpc.serviceName === 'string') path = grpc.serviceName;
-    } else if (network === 'httpupgrade') {
-      const hu = (stream.httpupgradeSettings as Record<string, unknown> | undefined) ?? {};
-      if (typeof hu.path === 'string') path = hu.path;
-    } else if (network === 'xhttp') {
-      const xh = (stream.xhttpSettings as Record<string, unknown> | undefined) ?? {};
-      if (typeof xh.path === 'string') path = xh.path;
-    }
-    return { name: sni, alpn, path, xver: 0 };
-  };
-
-  const addFallback = () => {
-    setFallbacks((prev) => [...prev, {
-      rowKey: `fb-${++fallbackKeyRef.current}`,
-      childId: null,
-      name: '',
-      alpn: '',
-      path: '',
-      dest: '',
-      xver: 0,
-    }]);
-  };
-
-  const updateFallback = (rowKey: string, patch: Partial<FallbackRow>) => {
-    setFallbacks((prev) => prev.map((r) => {
-      if (r.rowKey !== rowKey) return r;
-      // When the picker selects a new child inbound and the row hasn't
-      // been hand-edited yet (sni/alpn/path/dest all blank, xver = 0),
-      // pull the SNI/ALPN/Path defaults off that child. Operators who
-      // intentionally typed values keep them — we only fill the empties.
-      if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
-        const isPristine = !r.name && !r.alpn && !r.path && !r.dest && r.xver === 0;
-        if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
-      }
-      return { ...r, ...patch };
-    }));
-  };
-
-  const removeFallback = (idx: number) => {
-    setFallbacks((prev) => prev.filter((_, i) => i !== idx));
-  };
-
-  // Move a fallback row up/down by swapping adjacent indices. The order
-  // is persisted via the fallback row's sortOrder (rebuilt by index on
-  // save), so reordering survives reloads.
-  const moveFallback = (idx: number, direction: -1 | 1) => {
-    setFallbacks((prev) => {
-      const target = idx + direction;
-      if (target < 0 || target >= prev.length) return prev;
-      const next = prev.slice();
-      [next[idx], next[target]] = [next[target], next[idx]];
-      return next;
-    });
-  };
-
-  // One-shot: add a fresh fallback row for every eligible inbound (i.e.
-  // every option in fallbackChildOptions) that is not already wired up.
-  // Convenient for operators who want catch-all routing to every host
-  // they manage on the panel.
-  const addAllFallbacks = () => {
-    setFallbacks((prev) => {
-      const alreadyHave = new Set(prev.map((r) => r.childId));
-      const additions = fallbackChildOptions
-        .filter((opt) => !alreadyHave.has(opt.value))
-        .map<FallbackRow>((opt) => {
-          const derived = deriveFallbackDefaults(opt.value);
-          return {
-            rowKey: `fb-${++fallbackKeyRef.current}`,
-            childId: opt.value,
-            name: derived.name ?? '',
-            alpn: derived.alpn ?? '',
-            path: derived.path ?? '',
-            dest: '',
-            xver: derived.xver ?? 0,
-          };
-        });
-      if (additions.length === 0) return prev;
-      return [...prev, ...additions];
-    });
-  };
-
-  const genRealityKeypair = async () => {
-    setSaving(true);
-    try {
-      const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
-      if (msg?.success) {
-        const obj = msg.obj as { privateKey: string; publicKey: string };
-        form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
-        form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
-      }
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  const clearRealityKeypair = () => {
-    form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], '');
-    form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], '');
-  };
-
-  const genMldsa65 = async () => {
-    setSaving(true);
-    try {
-      const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
-      if (msg?.success) {
-        const obj = msg.obj as { seed: string; verify: string };
-        form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], obj.seed);
-        form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], obj.verify);
-      }
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  const clearMldsa65 = () => {
-    form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], '');
-    form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], '');
-  };
-
-  const randomizeRealityTarget = () => {
-    const tgt = getRandomRealityTarget() as { target: string; sni: string };
-    form.setFieldValue(['streamSettings', 'realitySettings', 'target'], tgt.target);
-    form.setFieldValue(
-      ['streamSettings', 'realitySettings', 'serverNames'],
-      tgt.sni.split(',').map((s) => s.trim()).filter(Boolean),
-    );
-  };
-
-  const randomizeShortIds = () => {
-    form.setFieldValue(
-      ['streamSettings', 'realitySettings', 'shortIds'],
-      RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean),
-    );
-  };
-
-  const getNewEchCert = async () => {
-    const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']);
-    setSaving(true);
-    try {
-      const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni });
-      if (msg?.success) {
-        const obj = msg.obj as { echServerKeys: string; echConfigList: string };
-        form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], obj.echServerKeys);
-        form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], obj.echConfigList);
-      }
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  const clearEchCert = () => {
-    form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], '');
-    form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
-  };
-
-  const generateRandomPinHash = () => {
-    const bytes = new Uint8Array(32);
-    crypto.getRandomValues(bytes);
-    let binary = '';
-    for (const b of bytes) binary += String.fromCharCode(b);
-    const hash = btoa(binary);
-    const current = (form.getFieldValue(
-      ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
-    ) as string[] | undefined) ?? [];
-    form.setFieldValue(
-      ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
-      [...current, hash],
-    );
-  };
-
-  const setCertFromPanel = async (certName: number) => {
-    setSaving(true);
-    try {
-      const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
-      if (msg?.success) {
-        const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
-        if (!obj.webCertFile && !obj.webKeyFile) {
-          messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
-          return;
-        }
-        form.setFieldValue(
-          ['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
-          obj.webCertFile ?? '',
-        );
-        form.setFieldValue(
-          ['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
-          obj.webKeyFile ?? '',
-        );
-      }
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  const clearCertFiles = (certName: number) => {
-    form.setFieldValue(
-      ['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
-      '',
-    );
-    form.setFieldValue(
-      ['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
-      '',
-    );
-  };
-
-  const onSecurityChange = async (next: string) => {
-    const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
-    const cleaned: Record<string, unknown> = { ...current, security: next };
-    delete cleaned.tlsSettings;
-    delete cleaned.realitySettings;
-    if (next === 'tls') {
-      const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
-      tls.certificates = [{
-        useFile: true,
-        certificateFile: '',
-        keyFile: '',
-        certificate: [],
-        key: [],
-        oneTimeLoading: false,
-        usage: 'encipherment',
-        buildChain: false,
-      }];
-      cleaned.tlsSettings = tls;
-    }
-    if (next === 'reality') {
-      const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
-      const tgt = getRandomRealityTarget() as { target: string; sni: string };
-      reality.target = tgt.target;
-      reality.serverNames = tgt.sni.split(',').map((s) => s.trim()).filter(Boolean);
-      reality.shortIds = RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean);
-      cleaned.realitySettings = reality;
-    }
-    form.setFieldValue('streamSettings', cleaned);
-    if (next === 'reality') {
-      try {
-        const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
-        if (msg?.success) {
-          const obj = msg.obj as { privateKey: string; publicKey: string };
-          form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
-          form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
-        }
-      } catch {
-        // best-effort: leave keypair fields empty if server call fails
-      }
-    }
-  };
-  const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
-  const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false;
-  const xhttpSessionPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionPlacement'], form);
-  const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form);
-  const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form);
-
-  const toggleExternalProxy = (on: boolean) => {
-    if (on) {
-      const port = (form.getFieldValue('port') as number) ?? 443;
-      form.setFieldValue(['streamSettings', 'externalProxy'], [{
-        forceTls: 'same',
-        dest: typeof window !== 'undefined' ? window.location.hostname : '',
-        port,
-        remark: '',
-        sni: '',
-        fingerprint: '',
-        alpn: [],
-      }]);
-    } else {
-      form.setFieldValue(['streamSettings', 'externalProxy'], []);
-    }
-  };
-
-  const toggleSockopt = (on: boolean) => {
-    if (on) {
-      form.setFieldValue(
-        ['streamSettings', 'sockopt'],
-        SockoptStreamSettingsSchema.parse({}),
-      );
-    } else {
-      form.setFieldValue(['streamSettings', 'sockopt'], undefined);
-    }
-  };
-  const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form);
-  const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0
-    ? Wireguard.generateKeypair(wgSecretKey).publicKey
-    : '';
-
-  const regenInboundWg = () => {
-    const kp = Wireguard.generateKeypair();
-    form.setFieldValue(['settings', 'secretKey'], kp.privateKey);
-  };
-
-  const regenWgPeerKeypair = (peerName: number) => {
-    const kp = Wireguard.generateKeypair();
-    form.setFieldValue(['settings', 'peers', peerName, 'privateKey'], kp.privateKey);
-    form.setFieldValue(['settings', 'peers', peerName, 'publicKey'], kp.publicKey);
-  };
-
-  const matchesVlessAuth = (
-    block: { id?: string; label?: string } | undefined | null,
-    authId: string,
-  ) => {
-    if (block?.id === authId) return true;
-    const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
-    if (authId === 'mlkem768') return label.includes('mlkem768');
-    if (authId === 'x25519') return label.includes('x25519');
-    return false;
-  };
-
-  const getNewVlessEnc = async (authId: string) => {
-    if (!authId) return;
-    setSaving(true);
-    try {
-      const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
-      if (!msg?.success) return;
-      const obj = msg.obj as {
-        auths?: { decryption: string; encryption: string; label?: string; id?: string }[];
-      };
-      const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
-      if (!block) return;
-      form.setFieldValue(['settings', 'decryption'], block.decryption);
-      form.setFieldValue(['settings', 'encryption'], block.encryption);
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  const clearVlessEnc = () => {
-    form.setFieldValue(['settings', 'decryption'], 'none');
-    form.setFieldValue(['settings', 'encryption'], 'none');
-  };
-
-  const selectedVlessAuth = (() => {
-    const enc = typeof vlessEncryption === 'string' ? vlessEncryption : '';
-    if (!enc || enc === 'none') return 'None';
-    const parts = enc.split('.').filter(Boolean);
-    const authKey = parts[parts.length - 1] || '';
-    if (!authKey) return t('pages.inbounds.vlessAuthCustom');
-    return authKey.length > 300
-      ? t('pages.inbounds.vlessAuthMlkem768')
-      : t('pages.inbounds.vlessAuthX25519');
-  })();
-
-  useEffect(() => {
-    if (!open) return;
-    const initial = mode === 'edit' && dbInbound
-      ? rawInboundToFormValues(dbInbound)
-      : buildAddModeValues();
-    form.resetFields();
-    form.setFieldsValue(initial);
-    if (
-      mode === 'edit'
-      && dbInbound
-      && (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN)
-    ) {
-      loadFallbacks(dbInbound.id);
-    } else {
-      setFallbacks([]);
-    }
-
-  }, [open, mode, dbInbound, form]);
-
-  // Why: protocol picker reset cascades through the form — clearing the
-  // settings DU branch and dropping a nodeId that no longer applies. The
-  // legacy modal did this imperatively in onProtocolChange; here we hook
-  // into AntD's onValuesChange and let setFieldValue keep the rest of
-  // the form state intact.
-  const onValuesChange = (changed: Partial<InboundFormValues>) => {
-    if (mode === 'edit') return;
-    if ('protocol' in changed && typeof changed.protocol === 'string') {
-      const next = changed.protocol;
-      const settings = createDefaultInboundSettings(next) ?? undefined;
-      form.setFieldValue('settings', settings);
-      if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
-        form.setFieldValue('nodeId', null);
-      }
-      // Hysteria uses its dedicated transport — force the network branch
-      // so the stream tab renders the hysteria sub-form, not the leftover
-      // tcpSettings from the previous protocol. When leaving hysteria,
-      // snap back to TCP so the standard network selector has a valid
-      // starting point.
-      if (next === Protocols.HYSTERIA) {
-        const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
-        tls.certificates = [{
-          useFile: true,
-          certificateFile: '',
-          keyFile: '',
-          certificate: [],
-          key: [],
-          oneTimeLoading: false,
-          usage: 'encipherment',
-          buildChain: false,
-        }];
-        form.setFieldValue('streamSettings', {
-          network: 'hysteria',
-          security: 'tls',
-          hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
-          tlsSettings: tls,
-          // Hysteria2 needs an obfs wrapper on the FinalMask side; seed
-          // it with salamander + a random password so the listener boots
-          // with a usable default. Re-selecting Hysteria from another
-          // protocol re-runs this and refreshes the password — that's
-          // intentional, the form was already being reset.
-          finalmask: {
-            tcp: [],
-            udp: [{
-              type: 'salamander',
-              settings: { password: RandomUtil.randomLowerAndNum(16) },
-            }],
-          },
-        });
-      } else {
-        const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
-        if (current?.network === 'hysteria') {
-          form.setFieldValue('streamSettings', { network: 'tcp', security: 'none', tcpSettings: {} });
-        }
-      }
-    }
-  };
-
-  const submit = async () => {
-    try {
-      await form.validateFields();
-    } catch {
-      return;
-    }
-    // Why getFieldsValue(true) instead of the validateFields return value:
-    // rc-component/form's validateFields filters its output by REGISTERED
-    // name paths. settings.clients and settings.fallbacks have no Form.Item
-    // bound to them (clients are managed via the standalone Client modal,
-    // not inside this inbound modal) — so validateFields would drop them
-    // and the update wire payload would silently delete every client on
-    // every save. getFieldsValue(true) returns the entire form store and
-    // keeps those sub-trees intact.
-    const values = form.getFieldsValue(true) as InboundFormValues;
-    const parsed = InboundFormSchema.safeParse(values);
-    if (!parsed.success) {
-      const issue = parsed.error.issues[0];
-      const path = Array.isArray(issue?.path) && issue.path.length > 0
-        ? issue.path.join('.')
-        : '';
-      const baseMsg = issue?.message ?? 'somethingWentWrong';
-      const display = path ? `${path}: ${baseMsg}` : baseMsg;
-      messageApi.error(t(baseMsg, { defaultValue: display }));
-      console.error('[InboundFormModal] schema validation failed', {
-        path: issue?.path,
-        message: issue?.message,
-        values,
-      });
-      return;
-    }
-    setSaving(true);
-    try {
-      const payload = formValuesToWirePayload(parsed.data);
-      const url = mode === 'edit' && dbInbound
-        ? `/panel/api/inbounds/update/${dbInbound.id}`
-        : '/panel/api/inbounds/add';
-      const msg = await HttpUtil.post(url, payload);
-      if (msg?.success) {
-        if (isFallbackHost) {
-          const obj = msg.obj as { id?: number; Id?: number } | null;
-          const masterId = mode === 'edit'
-            ? dbInbound!.id
-            : (obj?.id ?? obj?.Id ?? 0);
-          if (masterId) await saveFallbacks(masterId);
-        }
-        onSaved();
-        onClose();
-      }
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  const title = mode === 'edit'
-    ? t('pages.inbounds.modifyInbound')
-    : t('pages.inbounds.addInbound');
-
-  const okText = mode === 'edit'
-    ? t('pages.clients.submitEdit')
-    : t('create');
-
-  const basicTab = (
-    <>
-      <Form.Item name="tag" hidden noStyle><Input /></Form.Item>
-      <Form.Item name="up" hidden noStyle><InputNumber /></Form.Item>
-      <Form.Item name="down" hidden noStyle><InputNumber /></Form.Item>
-      <Form.Item name="total" hidden noStyle><InputNumber /></Form.Item>
-      <Form.Item name="expiryTime" hidden noStyle><InputNumber /></Form.Item>
-      <Form.Item name="lastTrafficResetTime" hidden noStyle><InputNumber /></Form.Item>
-      <Form.Item name="clientStats" hidden noStyle><Input /></Form.Item>
-
-      <Form.Item name="enable" label={t('enable')} valuePropName="checked">
-        <Switch />
-      </Form.Item>
-
-      <Form.Item name="remark" label={t('pages.inbounds.remark')}>
-        <Input />
-      </Form.Item>
-
-      {selectableNodes.length > 0 && isNodeEligible && (
-        <Form.Item name="nodeId" label={t('pages.inbounds.deployTo')}>
-          <Select
-            disabled={mode === 'edit'}
-            placeholder={t('pages.inbounds.localPanel')}
-            allowClear
-            options={selectableNodes.map((n) => ({
-              value: n.id,
-              label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
-              disabled: n.status === 'offline',
-            }))}
-          />
-        </Form.Item>
-      )}
-
-      <Form.Item name="protocol" label={t('pages.inbounds.protocol')}>
-        <Select disabled={mode === 'edit'} options={PROTOCOL_OPTIONS} />
-      </Form.Item>
-
-      <Form.Item name="listen" label={t('pages.inbounds.address')}>
-        <Input placeholder={t('pages.inbounds.monitorDesc')} />
-      </Form.Item>
-
-      <Form.Item
-        name="port"
-        label={t('pages.inbounds.port')}
-        rules={[antdRule(InboundFormBaseSchema.shape.port, t)]}
-      >
-        <InputNumber min={1} max={65535} />
-      </Form.Item>
-
-      <Form.Item
-        label={
-          <Tooltip title={t('pages.inbounds.meansNoLimit')}>
-            {t('pages.inbounds.totalFlow')}
-          </Tooltip>
-        }
-      >
-        <Form.Item
-          noStyle
-          shouldUpdate={(prev, curr) => prev.total !== curr.total}
-        >
-          {({ getFieldValue, setFieldValue }) => {
-            const totalBytes = (getFieldValue('total') as number) ?? 0;
-            const totalGB = totalBytes
-              ? Math.round((totalBytes / SizeFormatter.ONE_GB) * 100) / 100
-              : 0;
-            return (
-              <InputNumber
-                value={totalGB}
-                min={0}
-                step={1}
-                onChange={(v) => {
-                  const bytes = NumberFormatter.toFixed(
-                    (Number(v) || 0) * SizeFormatter.ONE_GB,
-                    0,
-                  );
-                  setFieldValue('total', bytes);
-                }}
-              />
-            );
-          }}
-        </Form.Item>
-      </Form.Item>
-
-      <Form.Item name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
-        <Select
-          options={TRAFFIC_RESETS.map((r) => ({
-            value: r,
-            label: t(`pages.inbounds.periodicTrafficReset.${r}`),
-          }))}
-        />
-      </Form.Item>
-
-      <Form.Item
-        label={
-          <Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
-            {t('pages.inbounds.expireDate')}
-          </Tooltip>
-        }
-      >
-        <Form.Item
-          noStyle
-          shouldUpdate={(prev, curr) => prev.expiryTime !== curr.expiryTime}
-        >
-          {({ getFieldValue, setFieldValue }) => {
-            const expiry = (getFieldValue('expiryTime') as number) ?? 0;
-            return (
-              <DateTimePicker
-                value={expiry > 0 ? dayjs(expiry) : null}
-                onChange={(d) => setFieldValue('expiryTime', d ? d.valueOf() : 0)}
-              />
-            );
-          }}
-        </Form.Item>
-      </Form.Item>
-    </>
-  );
-
-  const fallbacksCard = (
-    <Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
-      {fallbacks.length === 0 && (
-        <Empty
-          description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'}
-          styles={{ image: { height: 40 } }}
-          style={{ margin: '8px 0 12px' }}
-        />
-      )}
-      {fallbacks.map((record, idx) => (
-        <div
-          key={record.rowKey}
-          style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}
-        >
-          <Space.Compact block style={{ marginBottom: 6 }}>
-            <Select
-              value={record.childId}
-              options={fallbackChildOptions}
-              placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
-              showSearch={{
-                filterOption: (input, option) =>
-                  ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
-              }}
-              style={{ width: '100%' }}
-              onChange={(v) => updateFallback(record.rowKey, { childId: v })}
-            />
-            <Button
-              disabled={idx === 0}
-              onClick={() => moveFallback(idx, -1)}
-              title={t('pages.inbounds.form.moveUp')}
-            >
-              <ArrowUpOutlined />
-            </Button>
-            <Button
-              disabled={idx === fallbacks.length - 1}
-              onClick={() => moveFallback(idx, 1)}
-              title={t('pages.inbounds.form.moveDown')}
-            >
-              <ArrowDownOutlined />
-            </Button>
-            <Button danger onClick={() => removeFallback(idx)}>
-              <DeleteOutlined />
-            </Button>
-          </Space.Compact>
-          <Space.Compact block>
-            <InputAddon>SNI</InputAddon>
-            <Input
-              placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
-              value={record.name}
-              onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
-            />
-            <InputAddon>ALPN</InputAddon>
-            <Input
-              placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
-              value={record.alpn}
-              onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
-            />
-            <InputAddon>Path</InputAddon>
-            <Input
-              placeholder="/"
-              value={record.path}
-              onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
-            />
-            <InputAddon>Dest</InputAddon>
-            <Input
-              placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
-              value={record.dest}
-              onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
-            />
-            <InputAddon>xver</InputAddon>
-            <InputNumber
-              min={0}
-              max={2}
-              value={record.xver}
-              onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
-            />
-          </Space.Compact>
-        </div>
-      ))}
-      <Space>
-        <Button size="small" onClick={addFallback}>
-          <PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
-        </Button>
-        <Button
-          size="small"
-          onClick={addAllFallbacks}
-          disabled={fallbackChildOptions.length === 0
-            || fallbacks.length >= fallbackChildOptions.length}
-          title={t('pages.inbounds.form.addAllFallbackTooltip')}
-        >
-          {t('pages.inbounds.form.addAll')}
-        </Button>
-      </Space>
-    </Card>
-  );
-
-  const protocolTab = (
-    <>
-      {protocol === Protocols.WIREGUARD && (
-        <>
-          <Form.Item label={t('pages.xray.wireguard.secretKey')}>
-            <Space.Compact block>
-              <Form.Item name={['settings', 'secretKey']} noStyle>
-                <Input style={{ width: 'calc(100% - 32px)' }} />
-              </Form.Item>
-              <Button icon={<ReloadOutlined />} onClick={regenInboundWg} />
-            </Space.Compact>
-          </Form.Item>
-          <Form.Item label={t('pages.xray.wireguard.publicKey')}>
-            <Input value={wgPubKey} disabled />
-          </Form.Item>
-          <Form.Item name={['settings', 'mtu']} label="MTU">
-            <InputNumber />
-          </Form.Item>
-          <Form.Item
-            name={['settings', 'noKernelTun']}
-            label={t('pages.inbounds.info.noKernelTun')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-          <Form.List name={['settings', 'peers']}>
-            {(fields, { add, remove }) => (
-              <>
-                <Form.Item label={t('pages.inbounds.form.peers')}>
-                  <Button
-                    size="small"
-                    onClick={() => {
-                      const kp = Wireguard.generateKeypair();
-                      add({
-                        privateKey: kp.privateKey,
-                        publicKey: kp.publicKey,
-                        allowedIPs: ['10.0.0.2/32'],
-                        keepAlive: 0,
-                      });
-                    }}
-                  >
-                    <PlusOutlined /> {t('pages.inbounds.form.addPeer')}
-                  </Button>
-                </Form.Item>
-                {fields.map((field, idx) => (
-                  <div key={field.key} className="wg-peer">
-                    <Divider titlePlacement="center">
-                      <Space>
-                        <span>{t('pages.inbounds.info.peerNumber', { n: idx + 1 })}</span>
-                        {fields.length > 1 && (
-                          <Button
-                            size="small"
-                            danger
-                            icon={<MinusOutlined />}
-                            onClick={() => remove(field.name)}
-                          />
-                        )}
-                      </Space>
-                    </Divider>
-                    <Form.Item label={t('pages.xray.wireguard.secretKey')}>
-                      <Space.Compact block>
-                        <Form.Item name={[field.name, 'privateKey']} noStyle>
-                          <Input style={{ width: 'calc(100% - 32px)' }} />
-                        </Form.Item>
-                        <Button
-                          icon={<ReloadOutlined />}
-                          onClick={() => regenWgPeerKeypair(field.name)}
-                        />
-                      </Space.Compact>
-                    </Form.Item>
-                    <Form.Item name={[field.name, 'publicKey']} label={t('pages.xray.wireguard.publicKey')}>
-                      <Input />
-                    </Form.Item>
-                    <Form.Item name={[field.name, 'preSharedKey']} label="PSK">
-                      <Input />
-                    </Form.Item>
-                    <Form.List name={[field.name, 'allowedIPs']}>
-                      {(ipFields, { add: addIp, remove: removeIp }) => (
-                        <Form.Item label={t('pages.xray.wireguard.allowedIPs')}>
-                          <Button size="small" onClick={() => addIp('')}>
-                            <PlusOutlined />
-                          </Button>
-                          {ipFields.map((ipField) => (
-                            <Space.Compact key={ipField.key} block className="mt-4">
-                              <Form.Item name={ipField.name} noStyle>
-                                <Input />
-                              </Form.Item>
-                              {ipFields.length > 1 && (
-                                <Button size="small" onClick={() => removeIp(ipField.name)}>
-                                  <MinusOutlined />
-                                </Button>
-                              )}
-                            </Space.Compact>
-                          ))}
-                        </Form.Item>
-                      )}
-                    </Form.List>
-                    <Form.Item name={[field.name, 'keepAlive']} label={t('pages.inbounds.form.keepAlive')}>
-                      <InputNumber min={0} />
-                    </Form.Item>
-                  </div>
-                ))}
-              </>
-            )}
-          </Form.List>
-        </>
-      )}
-
-      {protocol === Protocols.TUN && (
-        <>
-          <Form.Item name={['settings', 'name']} label={t('pages.inbounds.info.interfaceName')}>
-            <Input placeholder="xray0" />
-          </Form.Item>
-          <Form.Item name={['settings', 'mtu']} label="MTU">
-            <InputNumber min={0} />
-          </Form.Item>
-          <Form.List name={['settings', 'gateway']}>
-            {(fields, { add, remove }) => (
-              <Form.Item label={t('pages.inbounds.info.gateway')}>
-                <Button size="small" onClick={() => add('')}>
-                  <PlusOutlined />
-                </Button>
-                {fields.map((field, j) => (
-                  <Space.Compact key={field.key} block className="mt-4">
-                    <Form.Item name={field.name} noStyle>
-                      <Input placeholder={j === 0 ? '10.0.0.1/16' : 'fc00::1/64'} />
-                    </Form.Item>
-                    <Button size="small" onClick={() => remove(field.name)}>
-                      <MinusOutlined />
-                    </Button>
-                  </Space.Compact>
-                ))}
-              </Form.Item>
-            )}
-          </Form.List>
-          <Form.List name={['settings', 'dns']}>
-            {(fields, { add, remove }) => (
-              <Form.Item label="DNS">
-                <Button size="small" onClick={() => add('')}>
-                  <PlusOutlined />
-                </Button>
-                {fields.map((field, j) => (
-                  <Space.Compact key={field.key} block className="mt-4">
-                    <Form.Item name={field.name} noStyle>
-                      <Input placeholder={j === 0 ? '1.1.1.1' : '8.8.8.8'} />
-                    </Form.Item>
-                    <Button size="small" onClick={() => remove(field.name)}>
-                      <MinusOutlined />
-                    </Button>
-                  </Space.Compact>
-                ))}
-              </Form.Item>
-            )}
-          </Form.List>
-          <Form.Item name={['settings', 'userLevel']} label={t('pages.xray.tun.userLevel')}>
-            <InputNumber min={0} />
-          </Form.Item>
-          <Form.List name={['settings', 'autoSystemRoutingTable']}>
-            {(fields, { add, remove }) => (
-              <Form.Item
-                label={
-                  <Tooltip title={t('pages.inbounds.form.autoSystemRoutesTooltip')}>
-                    {t('pages.inbounds.info.autoSystemRoutes')}
-                  </Tooltip>
-                }
-              >
-                <Button size="small" onClick={() => add('')}>
-                  <PlusOutlined />
-                </Button>
-                {fields.map((field, j) => (
-                  <Space.Compact key={field.key} block className="mt-4">
-                    <Form.Item name={field.name} noStyle>
-                      <Input placeholder={j === 0 ? '0.0.0.0/0' : '::/0'} />
-                    </Form.Item>
-                    <Button size="small" onClick={() => remove(field.name)}>
-                      <MinusOutlined />
-                    </Button>
-                  </Space.Compact>
-                ))}
-              </Form.Item>
-            )}
-          </Form.List>
-          <Form.Item
-            name={['settings', 'autoOutboundsInterface']}
-            label={
-              <Tooltip title={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}>
-                {t('pages.inbounds.form.autoOutboundsInterface')}
-              </Tooltip>
-            }
-          >
-            <Input placeholder="auto" />
-          </Form.Item>
-        </>
-      )}
-
-      {protocol === Protocols.TUNNEL && (
-        <>
-          <Form.Item name={['settings', 'rewriteAddress']} label={t('pages.inbounds.form.rewriteAddress')}>
-            <Input />
-          </Form.Item>
-          <Form.Item name={['settings', 'rewritePort']} label={t('pages.inbounds.form.rewritePort')}>
-            <InputNumber min={0} max={65535} />
-          </Form.Item>
-          <Form.Item name={['settings', 'allowedNetwork']} label={t('pages.inbounds.form.allowedNetwork')}>
-            <Select
-              options={[
-                { value: 'tcp,udp', label: 'TCP, UDP' },
-                { value: 'tcp', label: 'TCP' },
-                { value: 'udp', label: 'UDP' },
-              ]}
-            />
-          </Form.Item>
-          <Form.Item label={t('pages.inbounds.portMap')} name={['settings', 'portMap']}>
-            <HeaderMapEditor mode="v1" />
-          </Form.Item>
-          <Form.Item
-            name={['settings', 'followRedirect']}
-            label={t('pages.inbounds.form.followRedirect')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-        </>
-      )}
-
-      {(protocol === Protocols.HTTP || protocol === Protocols.MIXED) && (
-        <>
-          <Form.List name={['settings', 'accounts']}>
-            {(fields, { add, remove }) => (
-              <>
-                <Form.Item label={t('pages.inbounds.form.accounts')}>
-                  <Button
-                    size="small"
-                    onClick={() => add({
-                      user: RandomUtil.randomLowerAndNum(8),
-                      pass: RandomUtil.randomLowerAndNum(12),
-                    })}
-                  >
-                    <PlusOutlined /> {t('add')}
-                  </Button>
-                </Form.Item>
-                {fields.length > 0 && (
-                  <Form.Item wrapperCol={{ span: 24 }}>
-                    {fields.map((field, idx) => (
-                      <Space.Compact key={field.key} className="mb-8" block>
-                        <InputAddon>{String(idx + 1)}</InputAddon>
-                        <Form.Item name={[field.name, 'user']} noStyle>
-                          <Input placeholder={t('username')} />
-                        </Form.Item>
-                        <Form.Item name={[field.name, 'pass']} noStyle>
-                          <Input placeholder={t('password')} />
-                        </Form.Item>
-                        <Button onClick={() => remove(field.name)}>
-                          <MinusOutlined />
-                        </Button>
-                      </Space.Compact>
-                    ))}
-                  </Form.Item>
-                )}
-              </>
-            )}
-          </Form.List>
-          {protocol === Protocols.HTTP && (
-            <Form.Item
-              name={['settings', 'allowTransparent']}
-              label={t('pages.inbounds.form.allowTransparent')}
-              valuePropName="checked"
-            >
-              <Switch />
-            </Form.Item>
-          )}
-          {protocol === Protocols.MIXED && (
-            <>
-              <Form.Item name={['settings', 'auth']} label={t('pages.inbounds.info.auth')}>
-                <Select
-                  options={[
-                    { value: 'noauth', label: 'noauth' },
-                    { value: 'password', label: 'password' },
-                  ]}
-                />
-              </Form.Item>
-              <Form.Item
-                name={['settings', 'udp']}
-                label="UDP"
-                valuePropName="checked"
-              >
-                <Switch />
-              </Form.Item>
-              {mixedUdpOn && (
-                <Form.Item name={['settings', 'ip']} label="UDP IP">
-                  <Input />
-                </Form.Item>
-              )}
-            </>
-          )}
-        </>
-      )}
-
-      {protocol === Protocols.SHADOWSOCKS && (
-        <>
-          <Form.Item name={['settings', 'method']} label={t('pages.inbounds.form.encryptionMethod')}>
-            <Select
-              onChange={(v) => {
-                form.setFieldValue(
-                  ['settings', 'password'],
-                  RandomUtil.randomShadowsocksPassword(v as string),
-                );
-              }}
-              options={SSMethodSchema.options.map((m) => ({ value: m, label: m }))}
-            />
-          </Form.Item>
-          {isSSWith2022 && (
-            <Form.Item label={t('password')}>
-              <Space.Compact block>
-                <Form.Item name={['settings', 'password']} noStyle>
-                  <Input style={{ width: 'calc(100% - 32px)' }} />
-                </Form.Item>
-                <Button
-                  icon={<ReloadOutlined />}
-                  onClick={() => {
-                    const method = form.getFieldValue(['settings', 'method']);
-                    form.setFieldValue(
-                      ['settings', 'password'],
-                      RandomUtil.randomShadowsocksPassword(method as string),
-                    );
-                  }}
-                />
-              </Space.Compact>
-            </Form.Item>
-          )}
-          <Form.Item name={['settings', 'network']} label={t('pages.inbounds.network')}>
-            <Select
-              style={{ width: 120 }}
-              options={[
-                { value: 'tcp,udp', label: 'TCP, UDP' },
-                { value: 'tcp', label: 'TCP' },
-                { value: 'udp', label: 'UDP' },
-              ]}
-            />
-          </Form.Item>
-          <Form.Item
-            name={['settings', 'ivCheck']}
-            label="ivCheck"
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-        </>
-      )}
-
-      {protocol === Protocols.VLESS && (
-        <>
-          <Form.Item name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
-            <Input />
-          </Form.Item>
-          <Form.Item name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
-            <Input />
-          </Form.Item>
-          <Form.Item label=" ">
-            <Space size={8} wrap>
-              <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
-                {t('pages.inbounds.vlessAuthX25519')}
-              </Button>
-              <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
-                {t('pages.inbounds.vlessAuthMlkem768')}
-              </Button>
-              <Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
-            </Space>
-            <Text type="secondary" className="vless-auth-state">
-              {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
-            </Text>
-          </Form.Item>
-          {network === 'tcp' && (security === 'tls' || security === 'reality') && (
-            <Form.Item
-              label={t('pages.inbounds.form.visionTestseed')}
-              extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
-            >
-              <Space.Compact block>
-                {[900, 500, 900, 256].map((def, i) => (
-                  <Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
-                    <InputNumber min={1} style={{ width: '25%' }} />
-                  </Form.Item>
-                ))}
-              </Space.Compact>
-            </Form.Item>
-          )}
-        </>
-      )}
-
-      {isFallbackHost && fallbacksCard}
-    </>
-  );
-
-  // Switching `network` swaps which per-network key (tcpSettings,
-  // wsSettings, grpcSettings, ...) appears on the wire. Clear the old
-  // network's blob and seed the new one with the schema defaults so the
-  // Form.Items inside it have valid initial values (KCP needs MTU=1350
-  // etc., not empty strings).
-  // Seed each network's settings blob with its Zod schema defaults so
-  // every Form.Item inside the network sub-form has a defined starting
-  // value. XHTTP in particular has ~20 fields (sessionPlacement,
-  // seqPlacement, xPaddingMethod, uplinkHTTPMethod, ...) whose value
-  // is the literal "" sentinel meaning "let xray-core pick its
-  // default". Without seeding "", the Form.Item reads `undefined` and
-  // the Select shows blank instead of the "Default (path)" option.
-  const newStreamSlice = (n: string): Record<string, unknown> => {
-    switch (n) {
-      case 'tcp': return TcpStreamSettingsSchema.parse({ header: { type: 'none' } });
-      case 'kcp': return KcpStreamSettingsSchema.parse({});
-      case 'ws': return WsStreamSettingsSchema.parse({});
-      case 'grpc': return GrpcStreamSettingsSchema.parse({});
-      case 'httpupgrade': return HttpUpgradeStreamSettingsSchema.parse({});
-      case 'xhttp': return XHttpStreamSettingsSchema.parse({});
-      default: return {};
-    }
-  };
-  const onNetworkChange = (next: string) => {
-    const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings'];
-    const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
-    const cleaned: Record<string, unknown> = { ...current, network: next };
-    for (const k of ALL) {
-      if (k !== `${next}Settings`) delete cleaned[k];
-    }
-    cleaned[`${next}Settings`] = newStreamSlice(next);
-    // mKCP wants a UDP mask wrapper on the FinalMask side; seed it with
-    // `mkcp-original` so the inbound boots with a sensible default
-    // instead of unobfuscated mKCP traffic. The user can still edit or
-    // clear the mask via the FinalMask section.
-    if (next === 'kcp') {
-      const fm = (cleaned.finalmask as Record<string, unknown> | undefined) ?? {};
-      const udp = Array.isArray(fm.udp) ? (fm.udp as unknown[]) : [];
-      const hasMkcp = udp.some((m) => {
-        const entry = m as { type?: string };
-        return entry?.type === 'mkcp-original';
-      });
-      if (!hasMkcp) {
-        cleaned.finalmask = {
-          ...fm,
-          udp: [...udp, { type: 'mkcp-original', settings: {} }],
-        };
-      }
-    }
-    form.setFieldValue('streamSettings', cleaned);
-  };
-
-  const streamTab = (
-    <>
-      {protocol !== Protocols.HYSTERIA && (
-        <Form.Item label={t('transmission')} name={['streamSettings', 'network']}>
-          <Select
-            style={{ width: '75%' }}
-            onChange={onNetworkChange}
-            options={[
-              { value: 'tcp', label: 'RAW' },
-              { value: 'kcp', label: 'mKCP' },
-              { value: 'ws', label: 'WebSocket' },
-              { value: 'grpc', label: 'gRPC' },
-              { value: 'httpupgrade', label: 'HTTPUpgrade' },
-              { value: 'xhttp', label: 'XHTTP' },
-            ]}
-          />
-        </Form.Item>
-      )}
-
-      {/* Inbound Hysteria stream sub-form. The transport for hysteria
-          isn't user-selectable (always 'hysteria'), so the network
-          dropdown is hidden above. Fields here mirror the legacy
-          HysteriaStreamSettings inbound class: version is locked to 2,
-          auth + udpIdleTimeout are required, masquerade is an optional
-          sub-object that lets xray-core disguise the listener as an
-          HTTP server when probed. */}
-      {protocol === Protocols.HYSTERIA && (
-        <>
-          <Form.Item
-            label={t('pages.inbounds.form.version')}
-            name={['streamSettings', 'hysteriaSettings', 'version']}
-          >
-            <InputNumber min={2} max={2} disabled />
-          </Form.Item>
-          <Form.Item
-            label={t('pages.inbounds.form.udpIdleTimeout')}
-            name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
-          >
-            <InputNumber min={1} style={{ width: '100%' }} />
-          </Form.Item>
-
-          <HysteriaMasqueradeForm form={form} />
-        </>
-      )}
-
-      {network === 'tcp' && (
-        <>
-          <Form.Item
-            name={['streamSettings', 'tcpSettings', 'acceptProxyProtocol']}
-            label={t('pages.inbounds.form.proxyProtocol')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-          <Form.Item label={`HTTP ${t('camouflage')}`}>
-            <Form.Item
-              noStyle
-              shouldUpdate={(prev, curr) =>
-                prev.streamSettings?.tcpSettings?.header?.type
-                !== curr.streamSettings?.tcpSettings?.header?.type
-              }
-            >
-              {({ getFieldValue, setFieldValue }) => {
-                const headerType = getFieldValue(
-                  ['streamSettings', 'tcpSettings', 'header', 'type'],
-                ) as string | undefined;
-                return (
-                  <Switch
-                    checked={headerType === 'http'}
-                    onChange={(v) => {
-                      setFieldValue(
-                        ['streamSettings', 'tcpSettings', 'header'],
-                        v
-                          ? {
-                            type: 'http',
-                            request: {
-                              version: '1.1',
-                              method: 'GET',
-                              path: ['/'],
-                              headers: {},
-                            },
-                            response: {
-                              version: '1.1',
-                              status: '200',
-                              reason: 'OK',
-                              headers: {},
-                            },
-                          }
-                          : { type: 'none' },
-                      );
-                    }}
-                  />
-                );
-              }}
-            </Form.Item>
-          </Form.Item>
-          {/* Per Xray docs (transports/raw.html#httpheaderobject), the
-              `request` object is honored only by outbound proxies; the
-              inbound listener reads `response`. Showing Host / Path /
-              Method / Version / request-headers on the inbound side was
-              a regression from this modal's earlier iteration — those
-              inputs wrote to the wire but xray-core ignored them. The
-              inbound modal now only exposes the response side. */}
-          <Form.Item
-            noStyle
-            shouldUpdate={(prev, curr) =>
-              prev.streamSettings?.tcpSettings?.header?.type
-              !== curr.streamSettings?.tcpSettings?.header?.type
-            }
-          >
-            {({ getFieldValue }) => {
-              const headerType = getFieldValue(
-                ['streamSettings', 'tcpSettings', 'header', 'type'],
-              ) as string | undefined;
-              if (headerType !== 'http') return null;
-              return (
-                <>
-                  <Form.Item
-                    label={t('pages.inbounds.form.requestVersion')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'request', 'version',
-                    ]}
-                  >
-                    <Input placeholder="1.1" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.requestMethod')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'request', 'method',
-                    ]}
-                  >
-                    <Input placeholder="GET" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.requestPath')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'request', 'path',
-                    ]}
-                    getValueProps={(v) => ({ value: Array.isArray(v) ? v.join(',') : v })}
-                    getValueFromEvent={(e) => {
-                      const raw = (e?.target?.value ?? '') as string;
-                      const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
-                      return parts.length > 0 ? parts : ['/'];
-                    }}
-                  >
-                    <Input placeholder="/" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.requestHeaders')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'request', 'headers',
-                    ]}
-                  >
-                    <HeaderMapEditor mode="v2" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.responseVersion')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'response', 'version',
-                    ]}
-                  >
-                    <Input placeholder="1.1" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.responseStatus')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'response', 'status',
-                    ]}
-                  >
-                    <Input placeholder="200" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.responseReason')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'response', 'reason',
-                    ]}
-                  >
-                    <Input placeholder="OK" />
-                  </Form.Item>
-                  <Form.Item
-                    label={t('pages.inbounds.form.responseHeaders')}
-                    name={[
-                      'streamSettings', 'tcpSettings', 'header',
-                      'response', 'headers',
-                    ]}
-                  >
-                    <HeaderMapEditor mode="v2" />
-                  </Form.Item>
-                </>
-              );
-            }}
-          </Form.Item>
-        </>
-      )}
-
-      {network === 'ws' && (
-        <>
-          <Form.Item
-            name={['streamSettings', 'wsSettings', 'acceptProxyProtocol']}
-            label={t('pages.inbounds.form.proxyProtocol')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'wsSettings', 'host']} label={t('host')}>
-            <Input />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'wsSettings', 'path']} label={t('path')}>
-            <Input />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
-            label={t('pages.inbounds.form.heartbeatPeriod')}
-          >
-            <InputNumber min={0} />
-          </Form.Item>
-          <Form.Item
-            label={t('pages.inbounds.form.headers')}
-            name={['streamSettings', 'wsSettings', 'headers']}
-          >
-            <HeaderMapEditor mode="v1" />
-          </Form.Item>
-        </>
-      )}
-
-      {network === 'grpc' && (
-        <>
-          <Form.Item
-            name={['streamSettings', 'grpcSettings', 'serviceName']}
-            label={t('pages.inbounds.form.serviceName')}
-          >
-            <Input />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'grpcSettings', 'authority']}
-            label={t('pages.inbounds.form.authority')}
-          >
-            <Input />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'grpcSettings', 'multiMode']}
-            label={t('pages.inbounds.form.multiMode')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-        </>
-      )}
-
-      {network === 'xhttp' && (
-        <>
-          <Form.Item name={['streamSettings', 'xhttpSettings', 'host']} label={t('host')}>
-            <Input />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'xhttpSettings', 'path']} label={t('path')}>
-            <Input />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'xhttpSettings', 'mode']} label={t('pages.inbounds.info.mode')}>
-            <Select
-              style={{ width: '50%' }}
-              options={(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => ({
-                value: m,
-                label: m,
-              }))}
-            />
-          </Form.Item>
-          {xhttpMode === 'packet-up' && (
-            <>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'scMaxBufferedPosts']}
-                label={t('pages.inbounds.form.maxBufferedUpload')}
-              >
-                <InputNumber />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
-                label={t('pages.inbounds.form.maxUploadSize')}
-              >
-                <Input />
-              </Form.Item>
-            </>
-          )}
-          {xhttpMode === 'stream-up' && (
-            <Form.Item
-              name={['streamSettings', 'xhttpSettings', 'scStreamUpServerSecs']}
-              label={t('pages.inbounds.form.streamUpServer')}
-            >
-              <Input />
-            </Form.Item>
-          )}
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'serverMaxHeaderBytes']}
-            label={t('pages.inbounds.form.serverMaxHeaderBytes')}
-          >
-            <InputNumber min={0} placeholder="0 (default)" />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
-            label={t('pages.inbounds.form.paddingBytes')}
-          >
-            <Input />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'headers']}
-            label={t('pages.inbounds.form.headers')}
-          >
-            <HeaderMapEditor mode="v1" />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
-            label={t('pages.inbounds.form.uplinkHttpMethod')}
-          >
-            <Select
-              options={[
-                { value: '', label: 'Default (POST)' },
-                { value: 'POST', label: 'POST' },
-                { value: 'PUT', label: 'PUT' },
-                {
-                  value: 'GET',
-                  label: 'GET (packet-up only)',
-                  disabled: xhttpMode !== 'packet-up',
-                },
-              ]}
-            />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
-            label={t('pages.inbounds.form.paddingObfsMode')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-          {xhttpObfsMode && (
-            <>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
-                label={t('pages.inbounds.form.paddingKey')}
-              >
-                <Input placeholder="x_padding" />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
-                label={t('pages.inbounds.form.paddingHeader')}
-              >
-                <Input placeholder="X-Padding" />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
-                label={t('pages.inbounds.form.paddingPlacement')}
-              >
-                <Select
-                  options={[
-                    { value: '', label: 'Default (queryInHeader)' },
-                    { value: 'queryInHeader', label: 'queryInHeader' },
-                    { value: 'header', label: 'header' },
-                    { value: 'cookie', label: 'cookie' },
-                    { value: 'query', label: 'query' },
-                  ]}
-                />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
-                label={t('pages.inbounds.form.paddingMethod')}
-              >
-                <Select
-                  options={[
-                    { value: '', label: 'Default (repeat-x)' },
-                    { value: 'repeat-x', label: 'repeat-x' },
-                    { value: 'tokenish', label: 'tokenish' },
-                  ]}
-                />
-              </Form.Item>
-            </>
-          )}
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
-            label={t('pages.inbounds.form.sessionPlacement')}
-          >
-            <Select
-              options={[
-                { value: '', label: 'Default (path)' },
-                { value: 'path', label: 'path' },
-                { value: 'header', label: 'header' },
-                { value: 'cookie', label: 'cookie' },
-                { value: 'query', label: 'query' },
-              ]}
-            />
-          </Form.Item>
-          {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && (
-            <Form.Item
-              name={['streamSettings', 'xhttpSettings', 'sessionKey']}
-              label={t('pages.inbounds.form.sessionKey')}
-            >
-              <Input placeholder="x_session" />
-            </Form.Item>
-          )}
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
-            label={t('pages.inbounds.form.sequencePlacement')}
-          >
-            <Select
-              options={[
-                { value: '', label: 'Default (path)' },
-                { value: 'path', label: 'path' },
-                { value: 'header', label: 'header' },
-                { value: 'cookie', label: 'cookie' },
-                { value: 'query', label: 'query' },
-              ]}
-            />
-          </Form.Item>
-          {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && (
-            <Form.Item
-              name={['streamSettings', 'xhttpSettings', 'seqKey']}
-              label={t('pages.inbounds.form.sequenceKey')}
-            >
-              <Input placeholder="x_seq" />
-            </Form.Item>
-          )}
-          {xhttpMode === 'packet-up' && (
-            <>
-              <Form.Item
-                name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
-                label={t('pages.inbounds.form.uplinkDataPlacement')}
-              >
-                <Select
-                  options={[
-                    { value: '', label: 'Default (body)' },
-                    { value: 'body', label: 'body' },
-                    { value: 'header', label: 'header' },
-                    { value: 'cookie', label: 'cookie' },
-                    { value: 'query', label: 'query' },
-                  ]}
-                />
-              </Form.Item>
-              {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && (
-                <Form.Item
-                  name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
-                  label={t('pages.inbounds.form.uplinkDataKey')}
-                >
-                  <Input placeholder="x_data" />
-                </Form.Item>
-              )}
-            </>
-          )}
-          <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'noSSEHeader']}
-            label={t('pages.inbounds.form.noSseHeader')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-        </>
-      )}
-
-      {network === 'httpupgrade' && (
-        <>
-          <Form.Item
-            name={['streamSettings', 'httpupgradeSettings', 'acceptProxyProtocol']}
-            label={t('pages.inbounds.form.proxyProtocol')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'httpupgradeSettings', 'host']}
-            label={t('host')}
-          >
-            <Input />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'httpupgradeSettings', 'path']}
-            label={t('path')}
-          >
-            <Input />
-          </Form.Item>
-          <Form.Item
-            label={t('pages.inbounds.form.headers')}
-            name={['streamSettings', 'httpupgradeSettings', 'headers']}
-          >
-            <HeaderMapEditor mode="v1" />
-          </Form.Item>
-        </>
-      )}
-
-      {network === 'kcp' && (
-        <>
-          <Form.Item name={['streamSettings', 'kcpSettings', 'mtu']} label="MTU">
-            <InputNumber min={576} max={1460} />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'kcpSettings', 'tti']} label={t('pages.inbounds.form.ttiMs')}>
-            <InputNumber min={10} max={100} />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'kcpSettings', 'uplinkCapacity']} label={t('pages.inbounds.form.uplinkMbps')}>
-            <InputNumber min={0} />
-          </Form.Item>
-          <Form.Item name={['streamSettings', 'kcpSettings', 'downlinkCapacity']} label={t('pages.inbounds.form.downlinkMbps')}>
-            <InputNumber min={0} />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
-            label={t('pages.inbounds.form.cwndMultiplier')}
-          >
-            <InputNumber min={1} />
-          </Form.Item>
-          <Form.Item
-            name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
-            label={t('pages.inbounds.form.maxSendingWindow')}
-          >
-            <InputNumber min={0} />
-          </Form.Item>
-        </>
-      )}
-
-      <Form.Item
-        noStyle
-        shouldUpdate={(prev, curr) => {
-          const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
-          const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
-          return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
-        }}
-      >
-        {({ getFieldValue }) => {
-          const arr = getFieldValue(['streamSettings', 'externalProxy']);
-          const on = Array.isArray(arr) && arr.length > 0;
-          return (
-            <>
-              <Form.Item label={t('pages.inbounds.form.externalProxy')}>
-                <Switch checked={on} onChange={toggleExternalProxy} />
-              </Form.Item>
-              {on && (
-                <Form.List name={['streamSettings', 'externalProxy']}>
-                  {(fields, { add, remove }) => (
-                    <>
-                      <Form.Item label=" " colon={false}>
-                        <Button
-                          size="small"
-                          type="primary"
-                          onClick={() => add({
-                            forceTls: 'same',
-                            dest: '',
-                            port: 443,
-                            remark: '',
-                            sni: '',
-                            fingerprint: '',
-                            alpn: [],
-                          })}
-                        >
-                          <PlusOutlined />
-                        </Button>
-                      </Form.Item>
-                      <Form.Item wrapperCol={{ span: 24 }}>
-                        {fields.map((field) => (
-                          <div key={field.key} style={{ margin: '8px 0' }}>
-                            <Space.Compact block>
-                              <Form.Item name={[field.name, 'forceTls']} noStyle>
-                                <Select
-                                  style={{ width: '20%' }}
-                                  options={[
-                                    { value: 'same', label: t('pages.inbounds.same') },
-                                    { value: 'none', label: t('none') },
-                                    { value: 'tls', label: 'TLS' },
-                                  ]}
-                                />
-                              </Form.Item>
-                              <Form.Item name={[field.name, 'dest']} noStyle>
-                                <Input style={{ width: '30%' }} placeholder={t('host')} />
-                              </Form.Item>
-                              <Form.Item name={[field.name, 'port']} noStyle>
-                                <InputNumber style={{ width: '15%' }} min={1} max={65535} />
-                              </Form.Item>
-                              <Form.Item name={[field.name, 'remark']} noStyle>
-                                <Input style={{ width: '25%' }} placeholder={t('pages.inbounds.remark')} />
-                              </Form.Item>
-                              <InputAddon onClick={() => remove(field.name)}>
-                                <MinusOutlined />
-                              </InputAddon>
-                            </Space.Compact>
-                            <Form.Item
-                              noStyle
-                              shouldUpdate={(prev, curr) =>
-                                prev.streamSettings?.externalProxy?.[field.name]?.forceTls
-                                !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
-                              }
-                            >
-                              {({ getFieldValue }) => {
-                                const ft = getFieldValue([
-                                  'streamSettings', 'externalProxy', field.name, 'forceTls',
-                                ]);
-                                if (ft !== 'tls') return null;
-                                return (
-                                  <Space.Compact style={{ marginTop: 6 }} block>
-                                    <Form.Item name={[field.name, 'sni']} noStyle>
-                                      <Input style={{ width: '30%' }} placeholder={t('pages.inbounds.form.sniPlaceholder')} />
-                                    </Form.Item>
-                                    <Form.Item name={[field.name, 'fingerprint']} noStyle>
-                                      <Select
-                                        style={{ width: '30%' }}
-                                        placeholder={t('pages.inbounds.form.fingerprint')}
-                                        options={[
-                                          { value: '', label: t('pages.inbounds.form.defaultOption') },
-                                          ...Object.values(UTLS_FINGERPRINT).map((fp) => ({
-                                            value: fp,
-                                            label: fp,
-                                          })),
-                                        ]}
-                                      />
-                                    </Form.Item>
-                                    <Form.Item name={[field.name, 'alpn']} noStyle>
-                                      <Select
-                                        mode="multiple"
-                                        style={{ width: '40%' }}
-                                        placeholder="ALPN"
-                                        options={Object.values(ALPN_OPTION).map((a) => ({
-                                          value: a,
-                                          label: a,
-                                        }))}
-                                      />
-                                    </Form.Item>
-                                  </Space.Compact>
-                                );
-                              }}
-                            </Form.Item>
-                          </div>
-                        ))}
-                      </Form.Item>
-                    </>
-                  )}
-                </Form.List>
-              )}
-            </>
-          );
-        }}
-      </Form.Item>
-
-      <Form.Item
-        noStyle
-        shouldUpdate={(prev, curr) => {
-          const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
-          const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
-          return !!a !== !!b;
-        }}
-      >
-        {({ getFieldValue }) => {
-          const sock = getFieldValue(['streamSettings', 'sockopt']);
-          const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
-          return (
-            <>
-              <Form.Item label="Sockopt">
-                <Switch checked={on} onChange={toggleSockopt} />
-              </Form.Item>
-              {on && (
-                <>
-                  <Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
-                    <InputNumber min={0} />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
-                    label={t('pages.inbounds.form.tcpKeepAliveInterval')}
-                  >
-                    <InputNumber min={0} />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
-                    label={t('pages.inbounds.form.tcpKeepAliveIdle')}
-                  >
-                    <InputNumber min={0} />
-                  </Form.Item>
-                  <Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label={t('pages.inbounds.form.tcpMaxSeg')}>
-                    <InputNumber min={0} />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
-                    label={t('pages.inbounds.form.tcpUserTimeout')}
-                  >
-                    <InputNumber min={0} />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
-                    label={t('pages.inbounds.form.tcpWindowClamp')}
-                  >
-                    <InputNumber min={0} />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
-                    label={t('pages.inbounds.form.proxyProtocol')}
-                    valuePropName="checked"
-                  >
-                    <Switch />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpFastOpen']}
-                    label={t('pages.inbounds.form.tcpFastOpen')}
-                    valuePropName="checked"
-                  >
-                    <Switch />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpMptcp']}
-                    label={t('pages.inbounds.form.multipathTcp')}
-                    valuePropName="checked"
-                  >
-                    <Switch />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'penetrate']}
-                    label={t('pages.inbounds.form.penetrate')}
-                    valuePropName="checked"
-                  >
-                    <Switch />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'V6Only']}
-                    label={t('pages.inbounds.form.v6Only')}
-                    valuePropName="checked"
-                  >
-                    <Switch />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'domainStrategy']}
-                    label={t('pages.xray.wireguard.domainStrategy')}
-                  >
-                    <Select
-                      style={{ width: '50%' }}
-                      options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
-                    />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'tcpcongestion']}
-                    label={t('pages.inbounds.form.tcpCongestion')}
-                  >
-                    <Select
-                      style={{ width: '50%' }}
-                      options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
-                    />
-                  </Form.Item>
-                  <Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
-                    <Select
-                      style={{ width: '50%' }}
-                      options={[
-                        { value: 'off', label: 'Off' },
-                        { value: 'redirect', label: 'Redirect' },
-                        { value: 'tproxy', label: 'TProxy' },
-                      ]}
-                    />
-                  </Form.Item>
-                  <Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label={t('pages.inbounds.form.dialerProxy')}>
-                    <Input />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'interfaceName']}
-                    label={t('pages.inbounds.info.interfaceName')}
-                  >
-                    <Input />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
-                    label={t('pages.inbounds.form.trustedXForwardedFor')}
-                  >
-                    <Select
-                      mode="tags"
-                      style={{ width: '100%' }}
-                      tokenSeparators={[',']}
-                      options={[
-                        { value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
-                        { value: 'X-Real-IP', label: 'X-Real-IP' },
-                        { value: 'True-Client-IP', label: 'True-Client-IP' },
-                        { value: 'X-Client-IP', label: 'X-Client-IP' },
-                      ]}
-                    />
-                  </Form.Item>
-                  <Form.Item
-                    name={['streamSettings', 'sockopt', 'addressPortStrategy']}
-                    label={t('pages.inbounds.form.addressPortStrategy')}
-                  >
-                    <Select
-                      style={{ width: '50%' }}
-                      options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
-                    />
-                  </Form.Item>
-                  <Form.Item shouldUpdate noStyle>
-                    {({ getFieldValue, setFieldValue }) => {
-                      const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
-                      const hasHe = he != null;
-                      return (
-                        <>
-                          <Form.Item label="Happy Eyeballs">
-                            <Switch
-                              checked={hasHe}
-                              onChange={(v) => {
-                                setFieldValue(
-                                  ['streamSettings', 'sockopt', 'happyEyeballs'],
-                                  v ? HappyEyeballsSchema.parse({}) : undefined,
-                                );
-                              }}
-                            />
-                          </Form.Item>
-                          {hasHe && (
-                            <>
-                              <Form.Item
-                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
-                                label={t('pages.inbounds.form.tryDelayMs')}
-                              >
-                                <InputNumber min={0} placeholder="0 disabled — 250 recommended" />
-                              </Form.Item>
-                              <Form.Item
-                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
-                                label={t('pages.inbounds.form.prioritizeIPv6')}
-                                valuePropName="checked"
-                              >
-                                <Switch />
-                              </Form.Item>
-                              <Form.Item
-                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
-                                label={t('pages.inbounds.form.interleave')}
-                              >
-                                <InputNumber min={1} />
-                              </Form.Item>
-                              <Form.Item
-                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
-                                label={t('pages.inbounds.form.maxConcurrentTry')}
-                              >
-                                <InputNumber min={0} />
-                              </Form.Item>
-                            </>
-                          )}
-                        </>
-                      );
-                    }}
-                  </Form.Item>
-                  <Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
-                    {(fields, { add, remove }) => (
-                      <>
-                        <Form.Item label={t('pages.inbounds.form.customSockopt')}>
-                          <Button
-                            type="dashed"
-                            size="small"
-                            onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
-                          >
-                            + {t('pages.inbounds.form.addCustomOption')}
-                          </Button>
-                        </Form.Item>
-                        {fields.map((field) => (
-                          <Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
-                            <Form.Item name={[field.name, 'system']} noStyle>
-                              <Select
-                                placeholder="all"
-                                allowClear
-                                style={{ width: 100 }}
-                                options={[
-                                  { value: 'linux', label: 'linux' },
-                                  { value: 'windows', label: 'windows' },
-                                  { value: 'darwin', label: 'darwin' },
-                                ]}
-                              />
-                            </Form.Item>
-                            <Form.Item name={[field.name, 'type']} noStyle>
-                              <Select
-                                style={{ width: 80 }}
-                                options={[
-                                  { value: 'int', label: 'int' },
-                                  { value: 'str', label: 'str' },
-                                ]}
-                              />
-                            </Form.Item>
-                            <Form.Item name={[field.name, 'level']} noStyle>
-                              <Input placeholder="level (6=TCP)" style={{ width: 100 }} />
-                            </Form.Item>
-                            <Form.Item name={[field.name, 'opt']} noStyle>
-                              <Input placeholder="opt" style={{ width: 120 }} />
-                            </Form.Item>
-                            <Form.Item name={[field.name, 'value']} noStyle>
-                              <Input placeholder="value" style={{ flex: 1 }} />
-                            </Form.Item>
-                            <Button danger onClick={() => remove(field.name)}>−</Button>
-                          </Space.Compact>
-                        ))}
-                      </>
-                    )}
-                  </Form.List>
-                </>
-              )}
-            </>
-          );
-        }}
-      </Form.Item>
-
-      <FinalMaskForm
-        name={['streamSettings', 'finalmask']}
-        network={network as string}
-        protocol={protocol}
-        form={form}
-      />
-    </>
-  );
-
-  const securityTab = (
-    <>
-      <Form.Item name={['streamSettings', 'security']} hidden noStyle>
-        <Input />
-      </Form.Item>
-      <Form.Item label={t('pages.inbounds.securityTab')}>
-        <Form.Item
-          noStyle
-          shouldUpdate={(prev, curr) =>
-            prev.streamSettings?.security !== curr.streamSettings?.security
-            || prev.streamSettings?.network !== curr.streamSettings?.network
-            || prev.protocol !== curr.protocol
-          }
-        >
-          {({ getFieldValue }) => {
-            const sec = getFieldValue(['streamSettings', 'security']) ?? 'none';
-            const net = getFieldValue(['streamSettings', 'network']) ?? '';
-            const proto = getFieldValue('protocol') ?? '';
-            const tlsOk = canEnableTls({ protocol: proto, streamSettings: { network: net, security: sec } });
-            const realityOk = canEnableReality({ protocol: proto, streamSettings: { network: net, security: sec } });
-            const tlsOnly = proto === Protocols.HYSTERIA;
-            return (
-              <Radio.Group
-                value={sec}
-                buttonStyle="solid"
-                disabled={!tlsOk}
-                onChange={(e) => onSecurityChange(e.target.value)}
-              >
-                {!tlsOnly && <Radio.Button value="none">{t('none')}</Radio.Button>}
-                <Radio.Button value="tls">TLS</Radio.Button>
-                {realityOk && <Radio.Button value="reality">Reality</Radio.Button>}
-              </Radio.Group>
-            );
-          }}
-        </Form.Item>
-      </Form.Item>
-
-      <Form.Item
-        noStyle
-        shouldUpdate={(prev, curr) =>
-          prev.streamSettings?.security !== curr.streamSettings?.security
-        }
-      >
-        {({ getFieldValue }) => {
-          const sec = getFieldValue(['streamSettings', 'security']);
-          if (sec !== 'tls') return null;
-          return (
-            <>
-              <Form.Item name={['streamSettings', 'tlsSettings', 'serverName']} label="SNI">
-                <Input placeholder={t('pages.inbounds.form.serverNameIndication')} />
-              </Form.Item>
-              <Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label={t('pages.inbounds.form.cipherSuites')}>
-                <Select
-                  options={[
-                    { value: '', label: t('pages.inbounds.form.autoOption') },
-                    ...Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ({ value: v, label: k })),
-                  ]}
-                />
-              </Form.Item>
-              <Form.Item label={t('pages.inbounds.form.minMaxVersion')}>
-                <Space.Compact block>
-                  <Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
-                    <Select
-                      style={{ width: '50%' }}
-                      options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
-                    />
-                  </Form.Item>
-                  <Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
-                    <Select
-                      style={{ width: '50%' }}
-                      options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
-                    />
-                  </Form.Item>
-                </Space.Compact>
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
-                label="uTLS"
-              >
-                <Select
-                  options={[
-                    { value: '', label: 'None' },
-                    ...Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp })),
-                  ]}
-                />
-              </Form.Item>
-              <Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
-                <Select
-                  mode="multiple"
-                  tokenSeparators={[',']}
-                  style={{ width: '100%' }}
-                  options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
-                />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
-                label={t('pages.inbounds.form.rejectUnknownSni')}
-                valuePropName="checked"
-              >
-                <Switch />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'tlsSettings', 'disableSystemRoot']}
-                label={t('pages.inbounds.form.disableSystemRoot')}
-                valuePropName="checked"
-              >
-                <Switch />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'tlsSettings', 'enableSessionResumption']}
-                label={t('pages.inbounds.form.sessionResumption')}
-                valuePropName="checked"
-              >
-                <Switch />
-              </Form.Item>
-
-              <Form.List name={['streamSettings', 'tlsSettings', 'certificates']}>
-                {(certFields, { add, remove }) => (
-                  <>
-                    <Form.Item label={t('certificate')}>
-                      <Button
-                        type="primary"
-                        size="small"
-                        onClick={() => add({
-                          useFile: true,
-                          certificateFile: '',
-                          keyFile: '',
-                          certificate: [],
-                          key: [],
-                          oneTimeLoading: false,
-                          usage: 'encipherment',
-                          buildChain: false,
-                        })}
-                      >
-                        <PlusOutlined />
-                      </Button>
-                    </Form.Item>
-                    {certFields.map((certField, idx) => (
-                      <div key={certField.key}>
-                        <Form.Item
-                          name={[certField.name, 'useFile']}
-                          label={`${t('certificate')} ${idx + 1}`}
-                        >
-                          <Radio.Group buttonStyle="solid">
-                            <Radio.Button value={true}>
-                              {t('pages.inbounds.certificatePath')}
-                            </Radio.Button>
-                            <Radio.Button value={false}>
-                              {t('pages.inbounds.certificateContent')}
-                            </Radio.Button>
-                          </Radio.Group>
-                        </Form.Item>
-                        {certFields.length > 1 && (
-                          <Form.Item label=" ">
-                            <Button
-                              size="small"
-                              danger
-                              onClick={() => remove(certField.name)}
-                            >
-                              <MinusOutlined /> {t('remove')}
-                            </Button>
-                          </Form.Item>
-                        )}
-                        <Form.Item
-                          noStyle
-                          shouldUpdate={(prev, curr) =>
-                            prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
-                            !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
-                          }
-                        >
-                          {({ getFieldValue }) => {
-                            const useFile = getFieldValue([
-                              'streamSettings', 'tlsSettings', 'certificates',
-                              certField.name, 'useFile',
-                            ]);
-                            return useFile ? (
-                              <>
-                                <Form.Item
-                                  name={[certField.name, 'certificateFile']}
-                                  label={t('pages.inbounds.publicKey')}
-                                >
-                                  <Input />
-                                </Form.Item>
-                                <Form.Item
-                                  name={[certField.name, 'keyFile']}
-                                  label={t('pages.inbounds.privatekey')}
-                                >
-                                  <Input />
-                                </Form.Item>
-                                <Form.Item label=" ">
-                                  <Space>
-                                    <Button
-                                      type="primary"
-                                      loading={saving}
-                                      onClick={() => setCertFromPanel(certField.name)}
-                                    >
-                                      {t('pages.inbounds.setDefaultCert')}
-                                    </Button>
-                                    <Button danger onClick={() => clearCertFiles(certField.name)}>
-                                      {t('clear')}
-                                    </Button>
-                                  </Space>
-                                </Form.Item>
-                              </>
-                            ) : (
-                              <>
-                                <Form.Item
-                                  name={[certField.name, 'certificate']}
-                                  label={t('pages.inbounds.publicKey')}
-                                  normalize={(v) => typeof v === 'string'
-                                    ? v.split('\n')
-                                    : v}
-                                  getValueProps={(v) => ({
-                                    value: Array.isArray(v) ? v.join('\n') : v,
-                                  })}
-                                >
-                                  <TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
-                                </Form.Item>
-                                <Form.Item
-                                  name={[certField.name, 'key']}
-                                  label={t('pages.inbounds.privatekey')}
-                                  normalize={(v) => typeof v === 'string'
-                                    ? v.split('\n')
-                                    : v}
-                                  getValueProps={(v) => ({
-                                    value: Array.isArray(v) ? v.join('\n') : v,
-                                  })}
-                                >
-                                  <TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
-                                </Form.Item>
-                              </>
-                            );
-                          }}
-                        </Form.Item>
-                        <Form.Item
-                          name={[certField.name, 'oneTimeLoading']}
-                          label={t('pages.inbounds.form.oneTimeLoading')}
-                          valuePropName="checked"
-                        >
-                          <Switch />
-                        </Form.Item>
-                        <Form.Item
-                          name={[certField.name, 'usage']}
-                          label={t('pages.inbounds.form.usageOption')}
-                        >
-                          <Select
-                            style={{ width: '50%' }}
-                            options={Object.values(USAGE_OPTION).map((u) => ({ value: u, label: u }))}
-                          />
-                        </Form.Item>
-                        <Form.Item
-                          noStyle
-                          shouldUpdate={(prev, curr) =>
-                            prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
-                            !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
-                          }
-                        >
-                          {({ getFieldValue }) => {
-                            const usage = getFieldValue([
-                              'streamSettings', 'tlsSettings', 'certificates',
-                              certField.name, 'usage',
-                            ]);
-                            if (usage !== 'issue') return null;
-                            return (
-                              <Form.Item
-                                name={[certField.name, 'buildChain']}
-                                label={t('pages.inbounds.form.buildChain')}
-                                valuePropName="checked"
-                              >
-                                <Switch />
-                              </Form.Item>
-                            );
-                          }}
-                        </Form.Item>
-                      </div>
-                    ))}
-                  </>
-                )}
-              </Form.List>
-
-              <Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label={t('pages.inbounds.form.echKey')}>
-                <Input />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
-                label={t('pages.inbounds.form.echConfig')}
-              >
-                <Input />
-              </Form.Item>
-              <Form.Item
-                label={t('pages.inbounds.form.pinnedPeerCertSha256')}
-                tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
-              >
-                <Space.Compact block>
-                  <Form.Item
-                    name={['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256']}
-                    noStyle
-                  >
-                    <Select
-                      mode="tags"
-                      tokenSeparators={[',', ' ']}
-                      placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
-                      style={{ width: 'calc(100% - 32px)' }}
-                    />
-                  </Form.Item>
-                  <Button
-                    icon={<ReloadOutlined />}
-                    onClick={generateRandomPinHash}
-                    title={t('pages.inbounds.form.generateRandomPin')}
-                  />
-                </Space.Compact>
-              </Form.Item>
-              <Form.Item label=" ">
-                <Space>
-                  <Button type="primary" loading={saving} onClick={getNewEchCert}>
-                    {t('pages.inbounds.form.getNewEchCert')}
-                  </Button>
-                  <Button danger onClick={clearEchCert}>{t('clear')}</Button>
-                </Space>
-              </Form.Item>
-            </>
-          );
-        }}
-      </Form.Item>
-
-      <Form.Item
-        noStyle
-        shouldUpdate={(prev, curr) =>
-          prev.streamSettings?.security !== curr.streamSettings?.security
-        }
-      >
-        {({ getFieldValue }) => {
-          const sec = getFieldValue(['streamSettings', 'security']);
-          if (sec !== 'reality') return null;
-          return (
-            <>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'show']}
-                label={t('pages.inbounds.form.show')}
-                valuePropName="checked"
-              >
-                <Switch />
-              </Form.Item>
-              <Form.Item name={['streamSettings', 'realitySettings', 'xver']} label={t('pages.inbounds.form.xver')}>
-                <InputNumber min={0} />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
-                label="uTLS"
-              >
-                <Select
-                  options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
-                />
-              </Form.Item>
-              <Form.Item label={t('pages.inbounds.form.target')}>
-                <Space.Compact block>
-                  <Form.Item name={['streamSettings', 'realitySettings', 'target']} noStyle>
-                    <Input style={{ width: 'calc(100% - 32px)' }} />
-                  </Form.Item>
-                  <Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
-                </Space.Compact>
-              </Form.Item>
-              <Form.Item label="SNI">
-                <Space.Compact block style={{ display: 'flex' }}>
-                  <Form.Item
-                    name={['streamSettings', 'realitySettings', 'serverNames']}
-                    noStyle
-                  >
-                    <Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
-                  </Form.Item>
-                  <Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
-                </Space.Compact>
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'maxTimediff']}
-                label={t('pages.inbounds.form.maxTimeDiff')}
-              >
-                <InputNumber min={0} />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'minClientVer']}
-                label={t('pages.inbounds.form.minClientVer')}
-              >
-                <Input placeholder="25.9.11" />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'maxClientVer']}
-                label={t('pages.inbounds.form.maxClientVer')}
-              >
-                <Input placeholder="25.9.11" />
-              </Form.Item>
-              <Form.Item label={t('pages.inbounds.form.shortIds')}>
-                <Space.Compact block style={{ display: 'flex' }}>
-                  <Form.Item
-                    name={['streamSettings', 'realitySettings', 'shortIds']}
-                    noStyle
-                  >
-                    <Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
-                  </Form.Item>
-                  <Button icon={<ReloadOutlined />} onClick={randomizeShortIds} />
-                </Space.Compact>
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
-                label={t('pages.inbounds.form.spiderX')}
-              >
-                <Input />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
-                label={t('pages.inbounds.publicKey')}
-              >
-                <Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'privateKey']}
-                label={t('pages.inbounds.privatekey')}
-              >
-                <Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
-              </Form.Item>
-              <Form.Item label=" ">
-                <Space>
-                  <Button type="primary" loading={saving} onClick={genRealityKeypair}>
-                    {t('pages.inbounds.form.getNewCert')}
-                  </Button>
-                  <Button danger onClick={clearRealityKeypair}>{t('clear')}</Button>
-                </Space>
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
-                label={t('pages.inbounds.form.mldsa65Seed')}
-              >
-                <Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
-              </Form.Item>
-              <Form.Item
-                name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
-                label={t('pages.inbounds.form.mldsa65Verify')}
-              >
-                <Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
-              </Form.Item>
-              <Form.Item label=" ">
-                <Space>
-                  <Button type="primary" loading={saving} onClick={genMldsa65}>
-                    {t('pages.inbounds.form.getNewSeed')}
-                  </Button>
-                  <Button danger onClick={clearMldsa65}>{t('clear')}</Button>
-                </Space>
-              </Form.Item>
-            </>
-          );
-        }}
-      </Form.Item>
-    </>
-  );
-
-  const advancedTab = (
-    <div className="advanced-shell">
-      <div className="advanced-panel">
-        <div className="advanced-panel__header">
-          <div>
-            <div className="advanced-panel__title">{t('pages.inbounds.advanced.title')}</div>
-            <div className="advanced-panel__subtitle">{t('pages.inbounds.advanced.subtitle')}</div>
-          </div>
-        </div>
-        <Tabs
-          className="advanced-inner-tabs"
-          items={[
-            {
-              key: 'all',
-              label: t('pages.inbounds.advanced.all'),
-              children: (
-                <>
-                  <div className="advanced-editor-meta">
-                    {t('pages.inbounds.advanced.allHelp')}
-                  </div>
-                  <AdvancedAllEditor form={form} streamEnabled={streamEnabled} />
-                </>
-              ),
-            },
-            {
-              key: 'settings',
-              label: t('pages.inbounds.advanced.settings'),
-              children: (
-                <>
-                  <div className="advanced-editor-meta">
-                    {t('pages.inbounds.advanced.settingsHelp')}{' '}
-                    <code>{'{ settings: { ... } }'}</code>.
-                  </div>
-                  <AdvancedSliceEditor
-                    form={form}
-                    path="settings"
-                    wrapKey="settings"
-                    minHeight="320px"
-                    maxHeight="540px"
-                  />
-                </>
-              ),
-            },
-            ...(streamEnabled
-              ? [{
-                key: 'stream',
-                label: t('pages.inbounds.advanced.stream'),
-                children: (
-                  <>
-                    <div className="advanced-editor-meta">
-                      {t('pages.inbounds.advanced.streamHelp')}{' '}
-                      <code>{'{ streamSettings: { ... } }'}</code>.
-                    </div>
-                    <AdvancedSliceEditor
-                      form={form}
-                      path="streamSettings"
-                      wrapKey="streamSettings"
-                      minHeight="320px"
-                      maxHeight="540px"
-                    />
-                  </>
-                ),
-              }]
-              : []),
-            {
-              key: 'sniffing',
-              label: t('pages.inbounds.advanced.sniffing'),
-              children: (
-                <>
-                  <div className="advanced-editor-meta">
-                    {t('pages.inbounds.advanced.sniffingHelp')}{' '}
-                    <code>{'{ sniffing: { ... } }'}</code>.
-                  </div>
-                  <AdvancedSliceEditor
-                    form={form}
-                    path="sniffing"
-                    wrapKey="sniffing"
-                    minHeight="240px"
-                    maxHeight="420px"
-                  />
-                </>
-              ),
-            },
-          ]}
-        />
-      </div>
-    </div>
-  );
-
-  const sniffingTab = (
-    <>
-      <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
-        <Switch />
-      </Form.Item>
-
-      {sniffingEnabled && (
-        <>
-          <Form.Item name={['sniffing', 'destOverride']} wrapperCol={{ span: 24 }}>
-            <Checkbox.Group>
-              {Object.entries(SNIFFING_OPTION).map(([key, value]) => (
-                <Checkbox key={key} value={value}>{key}</Checkbox>
-              ))}
-            </Checkbox.Group>
-          </Form.Item>
-
-          <Form.Item
-            name={['sniffing', 'metadataOnly']}
-            label={t('pages.inbounds.sniffingMetadataOnly')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-
-          <Form.Item
-            name={['sniffing', 'routeOnly']}
-            label={t('pages.inbounds.sniffingRouteOnly')}
-            valuePropName="checked"
-          >
-            <Switch />
-          </Form.Item>
-
-          <Form.Item
-            name={['sniffing', 'ipsExcluded']}
-            label={t('pages.inbounds.sniffingIpsExcluded')}
-          >
-            <Select
-              mode="tags"
-              tokenSeparators={[',']}
-              placeholder="IP/CIDR/geoip:*/ext:*"
-              style={{ width: '100%' }}
-            />
-          </Form.Item>
-
-          <Form.Item
-            name={['sniffing', 'domainsExcluded']}
-            label={t('pages.inbounds.sniffingDomainsExcluded')}
-          >
-            <Select
-              mode="tags"
-              tokenSeparators={[',']}
-              placeholder="domain:*/ext:*"
-              style={{ width: '100%' }}
-            />
-          </Form.Item>
-        </>
-      )}
-    </>
-  );
-
-  return (
-    <>
-      {messageContextHolder}
-      <Modal
-        open={open}
-        title={title}
-        okText={okText}
-        cancelText={t('close')}
-        confirmLoading={saving}
-        mask={{ closable: false }}
-        width={780}
-        onOk={submit}
-        onCancel={onClose}
-        destroyOnHidden
-      >
-        <Form
-          form={form}
-          colon={false}
-          labelCol={{ sm: { span: 8 } }}
-          wrapperCol={{ sm: { span: 14 } }}
-          onValuesChange={onValuesChange}
-        >
-          <Tabs items={[
-            // forceRender on every tab so all Form.Items register at modal
-            // open, not lazily on first visit. Without it, AntD's items API
-            // lazy-mounts inactive tabs — their fields don't register, so
-            // Form.useWatch on a parent path (e.g. 'sniffing') returns the
-            // partial-view {} until the user touches the tab and the
-            // inner Form.Item for `sniffing.enabled` registers.
-            { key: 'basic', label: t('pages.xray.basicTemplate'), children: basicTab, forceRender: true },
-            ...(([
-              Protocols.VLESS,
-              Protocols.SHADOWSOCKS,
-              Protocols.HTTP,
-              Protocols.MIXED,
-              Protocols.TUNNEL,
-              Protocols.TUN,
-              Protocols.WIREGUARD,
-            ] as string[]).includes(protocol) || isFallbackHost
-              ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab, forceRender: true }]
-              : []),
-            ...(streamEnabled
-              ? [
-                { key: 'stream', label: t('pages.inbounds.streamTab'), children: streamTab, forceRender: true },
-                { key: 'security', label: t('pages.inbounds.securityTab'), children: securityTab, forceRender: true },
-              ]
-              : []),
-            { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab, forceRender: true },
-            { key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab, forceRender: true },
-          ]} />
-        </Form>
-      </Modal>
-    </>
-  );
-}

+ 0 - 781
frontend/src/pages/inbounds/InboundList.tsx

@@ -1,781 +0,0 @@
-import { useCallback, useMemo, useState, type ReactElement } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
-  Button,
-  Card,
-  Dropdown,
-  Modal,
-  Popover,
-  Space,
-  Switch,
-  Table,
-  Tag,
-  Tooltip,
-  type TableColumnType,
-  type MenuProps,
-} from 'antd';
-import {
-  PlusOutlined,
-  MenuOutlined,
-  MoreOutlined,
-  EditOutlined,
-  QrcodeOutlined,
-  CopyOutlined,
-  ExportOutlined,
-  ImportOutlined,
-  ReloadOutlined,
-  RetweetOutlined,
-  BlockOutlined,
-  DeleteOutlined,
-  InfoCircleOutlined,
-  TagsOutlined,
-  UsergroupAddOutlined,
-  UsergroupDeleteOutlined,
-} from '@ant-design/icons';
-
-import { HttpUtil, SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
-import InfinityIcon from '@/components/InfinityIcon';
-import { useDatepicker } from '@/hooks/useDatepicker';
-import type { NodeRecord } from '@/api/queries/useNodesQuery';
-import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
-import { coerceInboundJsonField } from '@/models/dbinbound';
-import './InboundList.css';
-
-interface StreamHints {
-  network: string;
-  isTls: boolean;
-  isReality: boolean;
-}
-
-function readStreamHints(streamSettings: unknown): StreamHints {
-  const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
-  return {
-    network: stream.network ?? '',
-    isTls: stream.security === 'tls',
-    isReality: stream.security === 'reality',
-  };
-}
-
-// Display label for a network value. All known transports render in
-// upper-case for visual consistency with the TCP/UDP/TLS/Reality tags
-// already shown alongside; compound names (`httpupgrade`, `splithttp`,
-// `xhttp`) get a tiny touch of casing so they don't read as one word.
-function networkLabel(network: string): string {
-  const n = (network || '').toLowerCase();
-  if (!n) return 'TCP';
-  switch (n) {
-    case 'httpupgrade': return 'HTTPUpgrade';
-    case 'splithttp': return 'SplitHTTP';
-    case 'xhttp': return 'XHTTP';
-  }
-  return n.toUpperCase();
-}
-
-// Returns the underlying L4 protocol for transports whose name isn't
-// already TCP/UDP. `kcp` and `quic` both ride on UDP; everything else
-// (`ws`, `grpc`, `http`, `httpupgrade`, `xhttp`) is TCP-based and gets
-// no extra tag (the transport name implies TCP).
-function networkL4(network: string): 'UDP' | '' {
-  const n = (network || '').toLowerCase();
-  if (n === 'kcp' || n === 'quic') return 'UDP';
-  return '';
-}
-
-// Shadowsocks settings.network ("tcp" / "udp" / "tcp,udp") and Tunnel
-// settings.allowedNetwork (same shape, different field name) both carry
-// the L4 transport list independent of streamSettings. Returns a
-// comma-separated label.
-function commaNetworkLabel(raw: string): string {
-  const parts = (raw || 'tcp').toLowerCase().split(',').map((p) => p.trim()).filter(Boolean);
-  if (parts.length === 0) return 'TCP';
-  return parts.map(networkLabel).join(',');
-}
-
-function shadowsocksNetworkLabel(settings: unknown): string {
-  return commaNetworkLabel(readSettings(settings).network || '');
-}
-
-function tunnelNetworkLabel(settings: unknown): string {
-  return commaNetworkLabel(readSettings(settings).allowedNetwork || '');
-}
-
-// Mixed (socks+http combo) is always TCP at L4; settings.udp=true adds
-// UDP-associate support on the same port (SOCKS5 UDP).
-function mixedNetworkLabel(settings: unknown): string {
-  const st = coerceInboundJsonField(settings) as { udp?: boolean };
-  return st.udp ? 'TCP,UDP' : 'TCP';
-}
-
-function readSettings(settings: unknown): { method?: string; network?: string; allowedNetwork?: string } {
-  return coerceInboundJsonField(settings) as { method?: string; network?: string; allowedNetwork?: string };
-}
-
-export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
-  switch (record.protocol) {
-    case 'vmess':
-    case 'vless':
-    case 'trojan':
-    case 'hysteria':
-      return true;
-    case 'shadowsocks':
-      return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
-    default:
-      return false;
-  }
-}
-
-type ProtocolFlags = {
-  isVMess?: boolean;
-  isVLess?: boolean;
-  isTrojan?: boolean;
-  isSS?: boolean;
-  isHysteria?: boolean;
-  isMixed?: boolean;
-  isHTTP?: boolean;
-  isWireguard?: boolean;
-  isTunnel?: boolean;
-};
-
-interface DBInboundRecord extends ProtocolFlags {
-  id: number;
-  enable: boolean;
-  remark: string;
-  port: number;
-  protocol: string;
-  up: number;
-  down: number;
-  total: number;
-  expiryTime: number;
-  _expiryTime: { valueOf(): number } | null;
-  nodeId?: number | null;
-  settings: unknown;
-  streamSettings: unknown;
-}
-
-export interface ClientCountEntry {
-  clients: number;
-  active: string[];
-  deactive: string[];
-  depleted: string[];
-  expiring: string[];
-  online: string[];
-}
-
-export type RowAction =
-  | 'edit'
-  | 'showInfo'
-  | 'qrcode'
-  | 'export'
-  | 'subs'
-  | 'clipboard'
-  | 'delete'
-  | 'resetTraffic'
-  | 'delAllClients'
-  | 'clone';
-
-export type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
-
-interface InboundListProps {
-  dbInbounds: DBInboundRecord[];
-  clientCount: Record<number, ClientCountEntry>;
-  onlineClients: string[];
-  lastOnlineMap: Record<string, number>;
-  expireDiff: number;
-  trafficDiff: number;
-  pageSize: number;
-  isMobile: boolean;
-  subEnable: boolean;
-  nodesById: Map<number, NodeRecord>;
-  hasActiveNode: boolean;
-  onAddInbound: () => void;
-  onGeneralAction: (key: GeneralAction) => void;
-  onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void;
-}
-
-type SortKey =
-  | 'id'
-  | 'enable'
-  | 'remark'
-  | 'port'
-  | 'protocol'
-  | 'traffic'
-  | 'expiryTime'
-  | 'node'
-  | 'clients';
-
-type SortOrder = 'ascend' | 'descend' | null;
-
-const SORT_FNS: Record<SortKey, (a: DBInboundRecord, b: DBInboundRecord, ctx: { nodesById: Map<number, NodeRecord>; clientCount: Record<number, ClientCountEntry> }) => number> = {
-  id: (a, b) => a.id - b.id,
-  enable: (a, b) => Number(a.enable) - Number(b.enable),
-  remark: (a, b) => (a.remark || '').localeCompare(b.remark || ''),
-  port: (a, b) => a.port - b.port,
-  protocol: (a, b) => a.protocol.localeCompare(b.protocol),
-  traffic: (a, b) => (a.up + a.down) - (b.up + b.down),
-  expiryTime: (a, b) => (a.expiryTime || Infinity) - (b.expiryTime || Infinity),
-  node: (a, b, ctx) => {
-    const nameA = ctx.nodesById.get(a.nodeId ?? -1)?.name ?? (a.nodeId == null ? '￿' : `node #${a.nodeId}`);
-    const nameB = ctx.nodesById.get(b.nodeId ?? -1)?.name ?? (b.nodeId == null ? '￿' : `node #${b.nodeId}`);
-    return nameA.localeCompare(nameB);
-  },
-  clients: (a, b, ctx) => (ctx.clientCount[a.id]?.clients || 0) - (ctx.clientCount[b.id]?.clients || 0),
-};
-
-function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
-  if (dbInbound.isWireguard) return true;
-  if (dbInbound.isSS) {
-    return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
-  }
-  return false;
-}
-
-interface RowActionsMenuProps {
-  record: DBInboundRecord;
-  subEnable: boolean;
-  hasClients: boolean;
-  onClick: (key: RowAction) => void;
-  isMobile?: boolean;
-}
-
-function buildRowActionsMenu({ record, subEnable, t, isMobile, hasClients }: { record: DBInboundRecord; subEnable: boolean; t: (k: string) => string; isMobile?: boolean; hasClients?: boolean }): MenuProps['items'] {
-  const items: MenuProps['items'] = [];
-  if (isMobile) {
-    items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
-  }
-  if (showQrCodeMenu(record)) {
-    items.push({ key: 'qrcode', icon: <QrcodeOutlined />, label: t('qrCode') });
-  }
-  if (isInboundMultiUser(record)) {
-    items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
-    if (subEnable) {
-      items.push({
-        key: 'subs',
-        icon: <ExportOutlined />,
-        label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
-      });
-    }
-  } else {
-    items.push({ key: 'showInfo', icon: <InfoCircleOutlined />, label: t('pages.inbounds.inboundInfo') });
-  }
-  items.push({ key: 'clipboard', icon: <CopyOutlined />, label: t('pages.inbounds.exportInbound') });
-  items.push({ key: 'resetTraffic', icon: <RetweetOutlined />, label: t('pages.inbounds.resetTraffic') });
-  items.push({ key: 'clone', icon: <BlockOutlined />, label: t('pages.inbounds.clone') });
-  if (isInboundMultiUser(record) && hasClients) {
-    items.push({ key: 'attachClients', icon: <UsergroupAddOutlined />, label: t('pages.inbounds.attachClients') });
-    items.push({ key: 'detachClients', icon: <UsergroupDeleteOutlined />, label: t('pages.inbounds.detachClients') });
-    items.push({ key: 'addToGroup', icon: <TagsOutlined />, label: t('pages.inbounds.addClientsToGroup') });
-    items.push({ type: 'divider' });
-    items.push({ key: 'delAllClients', icon: <UsergroupDeleteOutlined />, danger: true, label: t('pages.inbounds.delAllClients') });
-  } else {
-    items.push({ type: 'divider' });
-  }
-  items.push({ key: 'delete', icon: <DeleteOutlined />, danger: true, label: t('delete') });
-  return items;
-}
-
-function RowActionsCell({ record, subEnable, hasClients, onClick }: RowActionsMenuProps) {
-  const { t } = useTranslation();
-  return (
-    <div className="action-buttons">
-      <Button type="text" size="small" icon={<EditOutlined />} onClick={() => onClick('edit')} />
-      <Dropdown
-        trigger={['click']}
-        menu={{
-          items: buildRowActionsMenu({ record, subEnable, t, hasClients }),
-          onClick: ({ key }) => onClick(key as RowAction),
-        }}
-      >
-        <Button type="text" size="small" icon={<MoreOutlined />} />
-      </Dropdown>
-    </div>
-  );
-}
-
-export default function InboundList({
-  dbInbounds,
-  clientCount,
-  lastOnlineMap: _lastOnlineMap,
-  expireDiff,
-  trafficDiff,
-  pageSize,
-  isMobile,
-  subEnable,
-  nodesById,
-  hasActiveNode,
-  onAddInbound,
-  onGeneralAction,
-  onRowAction,
-}: InboundListProps) {
-  const { t } = useTranslation();
-  const { datepicker } = useDatepicker();
-  const [sortKey, setSortKey] = useState<SortKey | null>(null);
-  const [sortOrder, setSortOrder] = useState<SortOrder>(null);
-  const [statsRecord, setStatsRecord] = useState<DBInboundRecord | null>(null);
-
-  const onSwitchEnable = useCallback(async (dbInbound: DBInboundRecord, next: boolean) => {
-    const previous = dbInbound.enable;
-    dbInbound.enable = next;
-    try {
-      const formData = new FormData();
-      formData.append('enable', String(next));
-      const msg = await HttpUtil.post(`/panel/api/inbounds/setEnable/${dbInbound.id}`, formData);
-      if (!msg?.success) dbInbound.enable = previous;
-    } catch {
-      dbInbound.enable = previous;
-    }
-  }, []);
-
-  const sortedInbounds = useMemo(() => {
-    if (!sortKey || !sortOrder) return dbInbounds;
-    const fn = SORT_FNS[sortKey];
-    if (!fn) return dbInbounds;
-    const sorted = [...dbInbounds].sort((a, b) => fn(a, b, { nodesById, clientCount }));
-    return sortOrder === 'descend' ? sorted.reverse() : sorted;
-  }, [dbInbounds, sortKey, sortOrder, nodesById, clientCount]);
-
-  const hasAnyRemark = useMemo(
-    () => dbInbounds.some((i) => typeof i.remark === 'string' && i.remark.trim() !== ''),
-    [dbInbounds],
-  );
-
-  const sorterFor = useCallback((key: SortKey) => ({
-    sorter: true as const,
-    showSorterTooltip: false,
-    sortOrder: sortKey === key ? sortOrder : null,
-    sortDirections: ['ascend' as const, 'descend' as const],
-  }), [sortKey, sortOrder]);
-
-  const columns: TableColumnType<DBInboundRecord>[] = useMemo(() => {
-    const cols: TableColumnType<DBInboundRecord>[] = [
-      {
-        title: 'ID',
-        dataIndex: 'id',
-        key: 'id',
-        align: 'right',
-        width: 30,
-        ...sorterFor('id'),
-      },
-      {
-        title: t('pages.inbounds.operate'),
-        key: 'action',
-        align: 'center',
-        width: 60,
-        render: (_, record) => (
-          <RowActionsCell
-            record={record}
-            subEnable={subEnable}
-            hasClients={(clientCount[record.id]?.clients || 0) > 0}
-            onClick={(key) => onRowAction({ key, dbInbound: record })}
-          />
-        ),
-      },
-      {
-        title: t('pages.inbounds.enable'),
-        key: 'enable',
-        align: 'center',
-        width: 35,
-        ...sorterFor('enable'),
-        render: (_, record) => (
-          <Switch
-            checked={record.enable}
-            onChange={(next) => onSwitchEnable(record, next)}
-          />
-        ),
-      },
-    ];
-
-    if (hasAnyRemark) {
-      cols.push({
-        title: t('pages.inbounds.remark'),
-        dataIndex: 'remark',
-        key: 'remark',
-        align: 'center',
-        width: 60,
-        ...sorterFor('remark'),
-      });
-    }
-
-    if (hasActiveNode) {
-      cols.push({
-        title: t('pages.inbounds.node'),
-        key: 'node',
-        align: 'center',
-        width: 60,
-        ...sorterFor('node'),
-        render: (_, record) => {
-          if (record.nodeId == null) {
-            return <Tag color="default">{t('pages.inbounds.localPanel')}</Tag>;
-          }
-          const node = nodesById.get(record.nodeId);
-          if (!node) {
-            return <Tag color="orange">node #{record.nodeId}</Tag>;
-          }
-          return (
-            <Tag color={node.status === 'online' ? 'blue' : 'red'}>{node.name}</Tag>
-          );
-        },
-      });
-    }
-
-    cols.push(
-      {
-        title: t('pages.inbounds.port'),
-        dataIndex: 'port',
-        key: 'port',
-        align: 'center',
-        width: 40,
-        ...sorterFor('port'),
-      },
-      {
-        title: t('pages.inbounds.protocol'),
-        key: 'protocol',
-        align: 'left',
-        width: 130,
-        ...sorterFor('protocol'),
-        render: (_, record) => {
-          const tags: ReactElement[] = [<Tag key="p" color="purple">{record.protocol}</Tag>];
-          if (record.isWireguard || record.isHysteria) {
-            tags.push(<Tag key="n" color="green">UDP</Tag>);
-          } else if (record.isSS) {
-            const stream = readStreamHints(record.streamSettings);
-            tags.push(<Tag key="n" color="green">{shadowsocksNetworkLabel(record.settings)}</Tag>);
-            if (stream.isTls) tags.push(<Tag key="tls" color="blue">TLS</Tag>);
-          } else if (record.isTunnel) {
-            tags.push(<Tag key="n" color="green">{tunnelNetworkLabel(record.settings)}</Tag>);
-          } else if (record.isMixed) {
-            tags.push(<Tag key="n" color="green">{mixedNetworkLabel(record.settings)}</Tag>);
-          } else if (record.isVMess || record.isVLess || record.isTrojan) {
-            const stream = readStreamHints(record.streamSettings);
-            tags.push(<Tag key="n" color="green">{networkLabel(stream.network)}</Tag>);
-            const l4 = networkL4(stream.network);
-            if (l4) tags.push(<Tag key="l4" color="green">{l4}</Tag>);
-            if (stream.isTls) tags.push(<Tag key="tls" color="blue">TLS</Tag>);
-            if (stream.isReality) tags.push(<Tag key="reality" color="blue">Reality</Tag>);
-          }
-          return <div className="protocol-tags">{tags}</div>;
-        },
-      },
-      {
-        title: t('clients'),
-        key: 'clients',
-        align: 'left',
-        width: 50,
-        ...sorterFor('clients'),
-        render: (_, record) => {
-          const cc = clientCount[record.id];
-          if (!cc) return null;
-          return (
-            <>
-              <Tag color="green" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>
-                {cc.clients}
-              </Tag>
-              {cc.deactive.length > 0 && (
-                <Popover
-                  title={t('disabled')}
-                  content={(
-                    <div className="client-email-list">
-                      {cc.deactive.map((e) => <div key={e}>{e}</div>)}
-                    </div>
-                  )}
-                >
-                  <Tag className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.deactive.length}</Tag>
-                </Popover>
-              )}
-              {cc.depleted.length > 0 && (
-                <Popover
-                  title={t('depleted')}
-                  content={(
-                    <div className="client-email-list">
-                      {cc.depleted.map((e) => <div key={e}>{e}</div>)}
-                    </div>
-                  )}
-                >
-                  <Tag color="red" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.depleted.length}</Tag>
-                </Popover>
-              )}
-              {cc.expiring.length > 0 && (
-                <Popover
-                  title={t('depletingSoon')}
-                  content={(
-                    <div className="client-email-list">
-                      {cc.expiring.map((e) => <div key={e}>{e}</div>)}
-                    </div>
-                  )}
-                >
-                  <Tag color="orange" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.expiring.length}</Tag>
-                </Popover>
-              )}
-              {cc.online.length > 0 && (
-                <Popover
-                  title={t('online')}
-                  content={(
-                    <div className="client-email-list">
-                      {cc.online.map((e) => <div key={e}>{e}</div>)}
-                    </div>
-                  )}
-                >
-                  <Tag color="blue" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.online.length}</Tag>
-                </Popover>
-              )}
-            </>
-          );
-        },
-      },
-      {
-        title: t('pages.inbounds.traffic'),
-        key: 'traffic',
-        align: 'center',
-        width: 90,
-        ...sorterFor('traffic'),
-        render: (_, record) => (
-          <Popover
-            content={(
-              <table cellPadding={2}>
-                <tbody>
-                  <tr>
-                    <td>↑ {SizeFormatter.sizeFormat(record.up)}</td>
-                    <td>↓ {SizeFormatter.sizeFormat(record.down)}</td>
-                  </tr>
-                  {record.total > 0 && record.up + record.down < record.total && (
-                    <tr>
-                      <td>{t('remained')}</td>
-                      <td>{SizeFormatter.sizeFormat(record.total - record.up - record.down)}</td>
-                    </tr>
-                  )}
-                </tbody>
-              </table>
-            )}
-          >
-            <Tag color={ColorUtils.usageColor(record.up + record.down, trafficDiff, record.total)}>
-              {SizeFormatter.sizeFormat(record.up + record.down)} /
-              {' '}
-              {record.total > 0 ? SizeFormatter.sizeFormat(record.total) : <InfinityIcon />}
-            </Tag>
-          </Popover>
-        ),
-      },
-      {
-        title: t('pages.inbounds.expireDate'),
-        key: 'expiryTime',
-        align: 'center',
-        width: 40,
-        ...sorterFor('expiryTime'),
-        render: (_, record) => {
-          if (record.expiryTime > 0) {
-            return (
-              <Popover content={IntlUtil.formatDate(record.expiryTime, datepicker)}>
-                <Tag color={ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)} style={{ minWidth: 50 }}>
-                  {IntlUtil.formatRelativeTime(record.expiryTime)}
-                </Tag>
-              </Popover>
-            );
-          }
-          return <Tag color="purple"><InfinityIcon /></Tag>;
-        },
-      },
-    );
-
-    return cols;
-  }, [t, hasAnyRemark, hasActiveNode, nodesById, clientCount, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable, sorterFor]);
-
-  const paginationFor = (rows: DBInboundRecord[]) => {
-    const size = pageSize > 0 ? pageSize : rows.length || 1;
-    return { pageSize: size, showSizeChanger: false, hideOnSinglePage: true };
-  };
-
-  const generalActionsMenu: MenuProps = {
-    items: [
-      { key: 'import', icon: <ImportOutlined />, label: t('pages.inbounds.importInbound') },
-      { key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') },
-      ...(subEnable
-        ? [{ key: 'subs', icon: <ExportOutlined />, label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}` }]
-        : []),
-      { key: 'resetInbounds', icon: <ReloadOutlined />, label: t('pages.inbounds.resetAllTraffic') },
-    ],
-    onClick: ({ key }) => onGeneralAction(key as GeneralAction),
-  };
-
-  return (
-    <Card
-      hoverable
-      title={(
-        <Space>
-          <Button type="primary" onClick={onAddInbound} icon={<PlusOutlined />}>
-            {!isMobile && t('pages.inbounds.addInbound')}
-          </Button>
-          <Dropdown trigger={['click']} menu={generalActionsMenu}>
-            <Button type="primary" icon={<MenuOutlined />}>
-              {!isMobile && t('pages.inbounds.generalActions')}
-            </Button>
-          </Dropdown>
-        </Space>
-      )}
-    >
-      <Space orientation="vertical" style={{ width: '100%' }}>
-        {isMobile ? (
-          <div className="inbound-cards">
-            {sortedInbounds.length === 0 ? (
-              <div className="card-empty">
-                <ImportOutlined style={{ fontSize: 28, opacity: 0.5 }} />
-                <div>{t('noData')}</div>
-              </div>
-            ) : (
-              sortedInbounds.map((record) => (
-                <div key={record.id} className="inbound-card">
-                  <div className="card-head">
-                    <span className="card-id">#{record.id}</span>
-                    <span className="tag-name">{record.remark}</span>
-                    <div className="card-actions" onClick={(e) => e.stopPropagation()}>
-                      <Tooltip title={t('pages.inbounds.inboundInfo')}>
-                        <InfoCircleOutlined className="row-action-trigger" onClick={() => setStatsRecord(record)} />
-                      </Tooltip>
-                      <Switch
-                        checked={record.enable}
-                        size="small"
-                        onChange={(next) => onSwitchEnable(record, next)}
-                      />
-                      <Dropdown
-                        trigger={['click']}
-                        placement="bottomRight"
-                        menu={{
-                          items: buildRowActionsMenu({ record, subEnable, t, isMobile: true, hasClients: (clientCount[record.id]?.clients || 0) > 0 }),
-                          onClick: ({ key }) => onRowAction({ key: key as RowAction, dbInbound: record }),
-                        }}
-                      >
-                        <MoreOutlined className="row-action-trigger" onClick={(e) => e.preventDefault()} />
-                      </Dropdown>
-                    </div>
-                  </div>
-                </div>
-              ))
-            )}
-          </div>
-        ) : (
-          <Table
-            columns={columns}
-            dataSource={sortedInbounds}
-            rowKey={(r) => r.id}
-            pagination={paginationFor(sortedInbounds)}
-            scroll={{ x: 1000 }}
-            style={{ marginTop: 10 }}
-            size="small"
-            locale={{
-              emptyText: (
-                <div className="card-empty">
-                  <ImportOutlined style={{ fontSize: 32, marginBottom: 8 }} />
-                  <div>{t('noData')}</div>
-                </div>
-              ),
-            }}
-            onChange={(_p, _f, sorter) => {
-              const single = Array.isArray(sorter) ? sorter[0] : sorter;
-              const colKey = (single?.columnKey || single?.field) as SortKey | undefined;
-              setSortKey(colKey || null);
-              setSortOrder((single?.order as SortOrder) || null);
-            }}
-          />
-        )}
-      </Space>
-
-      <Modal
-        open={isMobile && !!statsRecord}
-        footer={null}
-        width={360}
-        centered
-        title={statsRecord ? `#${statsRecord.id} ${statsRecord.remark || ''}`.trim() : ''}
-        onCancel={() => setStatsRecord(null)}
-        destroyOnHidden
-      >
-        {statsRecord && (
-          <div className="card-stats">
-            <div className="stat-row">
-              <span className="stat-label">{t('pages.inbounds.protocol')}</span>
-              <Tag color="purple">{statsRecord.protocol}</Tag>
-              {(statsRecord.isWireguard || statsRecord.isHysteria) && (
-                <Tag color="green">UDP</Tag>
-              )}
-              {statsRecord.isSS && (() => {
-                const stream = readStreamHints(statsRecord.streamSettings);
-                return (
-                  <>
-                    <Tag color="green">{shadowsocksNetworkLabel(statsRecord.settings)}</Tag>
-                    {stream.isTls && <Tag color="blue">TLS</Tag>}
-                  </>
-                );
-              })()}
-              {statsRecord.isTunnel && (
-                <Tag color="green">{tunnelNetworkLabel(statsRecord.settings)}</Tag>
-              )}
-              {statsRecord.isMixed && (
-                <Tag color="green">{mixedNetworkLabel(statsRecord.settings)}</Tag>
-              )}
-              {(statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan) && (() => {
-                const stream = readStreamHints(statsRecord.streamSettings);
-                const l4 = networkL4(stream.network);
-                return (
-                  <>
-                    <Tag color="green">{networkLabel(stream.network)}</Tag>
-                    {l4 && <Tag color="green">{l4}</Tag>}
-                    {stream.isTls && <Tag color="blue">TLS</Tag>}
-                    {stream.isReality && <Tag color="blue">Reality</Tag>}
-                  </>
-                );
-              })()}
-            </div>
-            <div className="stat-row">
-              <span className="stat-label">{t('pages.inbounds.port')}</span>
-              <Tag>{statsRecord.port}</Tag>
-            </div>
-            {hasActiveNode && (
-              <div className="stat-row">
-                <span className="stat-label">{t('pages.inbounds.node')}</span>
-                {statsRecord.nodeId == null ? (
-                  <Tag color="default">{t('pages.inbounds.localPanel')}</Tag>
-                ) : nodesById.get(statsRecord.nodeId) ? (
-                  <Tag color={nodesById.get(statsRecord.nodeId)!.status === 'online' ? 'blue' : 'red'}>
-                    {nodesById.get(statsRecord.nodeId)!.name}
-                  </Tag>
-                ) : (
-                  <Tag color="orange">#{statsRecord.nodeId}</Tag>
-                )}
-              </div>
-            )}
-            <div className="stat-row">
-              <span className="stat-label">{t('pages.inbounds.traffic')}</span>
-              <Tag color={ColorUtils.usageColor(statsRecord.up + statsRecord.down, trafficDiff, statsRecord.total)}>
-                {SizeFormatter.sizeFormat(statsRecord.up + statsRecord.down)} /
-                {' '}
-                {statsRecord.total > 0 ? SizeFormatter.sizeFormat(statsRecord.total) : <InfinityIcon />}
-              </Tag>
-            </div>
-            {clientCount[statsRecord.id] && (
-              <div className="stat-row">
-                <span className="stat-label">{t('clients')}</span>
-                <Tag color="green" className="client-count-tag">{clientCount[statsRecord.id].clients}</Tag>
-                {clientCount[statsRecord.id].online.length > 0 && (
-                  <Tag color="blue">{clientCount[statsRecord.id].online.length} {t('online')}</Tag>
-                )}
-                {clientCount[statsRecord.id].depleted.length > 0 && (
-                  <Tag color="red">{clientCount[statsRecord.id].depleted.length} {t('depleted')}</Tag>
-                )}
-                {clientCount[statsRecord.id].expiring.length > 0 && (
-                  <Tag color="orange">{clientCount[statsRecord.id].expiring.length} {t('depletingSoon')}</Tag>
-                )}
-              </div>
-            )}
-            <div className="stat-row">
-              <span className="stat-label">{t('pages.inbounds.expireDate')}</span>
-              {statsRecord.expiryTime > 0 ? (
-                <Tag color={ColorUtils.usageColor(Date.now(), expireDiff, statsRecord._expiryTime)}>
-                  {IntlUtil.formatRelativeTime(statsRecord.expiryTime)}
-                </Tag>
-              ) : (
-                <Tag color="purple"><InfinityIcon /></Tag>
-              )}
-            </div>
-          </div>
-        )}
-      </Modal>
-    </Card>
-  );
-}

+ 11 - 11
frontend/src/pages/inbounds/InboundsPage.tsx

@@ -28,19 +28,19 @@ import { useTheme } from '@/hooks/useTheme';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useWebSocket } from '@/hooks/useWebSocket';
 import { useWebSocket } from '@/hooks/useWebSocket';
 import { useNodesQuery } from '@/api/queries/useNodesQuery';
 import { useNodesQuery } from '@/api/queries/useNodesQuery';
-import AppSidebar from '@/components/AppSidebar';
-const TextModal = lazy(() => import('@/components/TextModal'));
-const PromptModal = lazy(() => import('@/components/PromptModal'));
+import AppSidebar from '@/layouts/AppSidebar';
+const TextModal = lazy(() => import('@/components/feedback/TextModal'));
+const PromptModal = lazy(() => import('@/components/feedback/PromptModal'));
 
 
 import { useInbounds } from './useInbounds';
 import { useInbounds } from './useInbounds';
-import InboundList from './InboundList';
-import LazyMount from '@/components/LazyMount';
-const InboundFormModal = lazy(() => import('./InboundFormModal'));
-const InboundInfoModal = lazy(() => import('./InboundInfoModal'));
-const QrCodeModal = lazy(() => import('./QrCodeModal'));
-const AttachClientsModal = lazy(() => import('./AttachClientsModal'));
-const DetachClientsModal = lazy(() => import('./DetachClientsModal'));
-const AddClientsToGroupModal = lazy(() => import('./AddClientsToGroupModal'));
+import { InboundList } from './list';
+import { LazyMount } from '@/components/utility';
+const InboundFormModal = lazy(() => import('./form/InboundFormModal'));
+const InboundInfoModal = lazy(() => import('./info/InboundInfoModal'));
+const QrCodeModal = lazy(() => import('./qr/QrCodeModal'));
+const AttachClientsModal = lazy(() => import('./clients/AttachClientsModal'));
+const DetachClientsModal = lazy(() => import('./clients/DetachClientsModal'));
+const AddClientsToGroupModal = lazy(() => import('./clients/AddClientsToGroupModal'));
 
 
 type RowAction =
 type RowAction =
   | 'edit'
   | 'edit'

+ 0 - 0
frontend/src/pages/inbounds/AddClientsToGroupModal.tsx → frontend/src/pages/inbounds/clients/AddClientsToGroupModal.tsx


+ 1 - 1
frontend/src/pages/inbounds/AttachClientsModal.tsx → frontend/src/pages/inbounds/clients/AttachClientsModal.tsx

@@ -5,7 +5,7 @@ import type { ColumnsType } from 'antd/es/table';
 
 
 import { HttpUtil } from '@/utils';
 import { HttpUtil } from '@/utils';
 import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
 import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
-import { isInboundMultiUser } from './InboundList';
+import { isInboundMultiUser } from '../list';
 
 
 interface AttachClientsModalProps {
 interface AttachClientsModalProps {
   open: boolean;
   open: boolean;

+ 0 - 0
frontend/src/pages/inbounds/DetachClientsModal.tsx → frontend/src/pages/inbounds/clients/DetachClientsModal.tsx


+ 3 - 0
frontend/src/pages/inbounds/clients/index.ts

@@ -0,0 +1,3 @@
+export { default as AttachClientsModal } from './AttachClientsModal';
+export { default as DetachClientsModal } from './DetachClientsModal';
+export { default as AddClientsToGroupModal } from './AddClientsToGroupModal';

+ 123 - 0
frontend/src/pages/inbounds/form/FallbacksCard.tsx

@@ -0,0 +1,123 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Card, Empty, Input, InputNumber, Select, Space } from 'antd';
+import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
+
+import { InputAddon } from '@/components/ui';
+import type { FallbackRow } from '@/schemas/forms/inbound-form';
+
+interface FallbacksCardProps {
+  fallbacks: FallbackRow[];
+  fallbackChildOptions: { label: string; value: number }[];
+  addFallback: () => void;
+  updateFallback: (rowKey: string, patch: Partial<FallbackRow>) => void;
+  removeFallback: (idx: number) => void;
+  moveFallback: (idx: number, direction: -1 | 1) => void;
+  addAllFallbacks: () => void;
+}
+
+export default function FallbacksCard({
+  fallbacks,
+  fallbackChildOptions,
+  addFallback,
+  updateFallback,
+  removeFallback,
+  moveFallback,
+  addAllFallbacks,
+}: FallbacksCardProps) {
+  const { t } = useTranslation();
+  return (
+    <Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
+      {fallbacks.length === 0 && (
+        <Empty
+          description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'}
+          styles={{ image: { height: 40 } }}
+          style={{ margin: '8px 0 12px' }}
+        />
+      )}
+      {fallbacks.map((record, idx) => (
+        <div
+          key={record.rowKey}
+          style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}
+        >
+          <Space.Compact block style={{ marginBottom: 6 }}>
+            <Select
+              value={record.childId}
+              options={fallbackChildOptions}
+              placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
+              showSearch={{
+                filterOption: (input, option) =>
+                  ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
+              }}
+              style={{ width: '100%' }}
+              onChange={(v) => updateFallback(record.rowKey, { childId: v })}
+            />
+            <Button
+              disabled={idx === 0}
+              onClick={() => moveFallback(idx, -1)}
+              title={t('pages.inbounds.form.moveUp')}
+            >
+              <ArrowUpOutlined />
+            </Button>
+            <Button
+              disabled={idx === fallbacks.length - 1}
+              onClick={() => moveFallback(idx, 1)}
+              title={t('pages.inbounds.form.moveDown')}
+            >
+              <ArrowDownOutlined />
+            </Button>
+            <Button danger onClick={() => removeFallback(idx)}>
+              <DeleteOutlined />
+            </Button>
+          </Space.Compact>
+          <Space.Compact block>
+            <InputAddon>SNI</InputAddon>
+            <Input
+              placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
+              value={record.name}
+              onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
+            />
+            <InputAddon>ALPN</InputAddon>
+            <Input
+              placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
+              value={record.alpn}
+              onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
+            />
+            <InputAddon>Path</InputAddon>
+            <Input
+              placeholder="/"
+              value={record.path}
+              onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
+            />
+            <InputAddon>Dest</InputAddon>
+            <Input
+              placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
+              value={record.dest}
+              onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
+            />
+            <InputAddon>xver</InputAddon>
+            <InputNumber
+              min={0}
+              max={2}
+              value={record.xver}
+              onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
+            />
+          </Space.Compact>
+        </div>
+      ))}
+      <Space>
+        <Button size="small" onClick={addFallback}>
+          <PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
+        </Button>
+        <Button
+          size="small"
+          onClick={addAllFallbacks}
+          disabled={fallbackChildOptions.length === 0
+            || fallbacks.length >= fallbackChildOptions.length}
+          title={t('pages.inbounds.form.addAllFallbackTooltip')}
+        >
+          {t('pages.inbounds.form.addAll')}
+        </Button>
+      </Space>
+    </Card>
+  );
+}

+ 0 - 0
frontend/src/pages/inbounds/InboundFormModal.css → frontend/src/pages/inbounds/form/InboundFormModal.css


+ 868 - 0
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -0,0 +1,868 @@
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import dayjs from 'dayjs';
+import {
+  Form,
+  Input,
+  InputNumber,
+  Modal,
+  Radio,
+  Select,
+  Switch,
+  Tabs,
+  Tooltip,
+  message,
+} from 'antd';
+
+import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
+import {
+  rawInboundToFormValues,
+  formValuesToWirePayload,
+} from '@/lib/xray/inbound-form-adapter';
+import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
+import {
+  canEnableReality,
+  canEnableStream,
+  canEnableTls,
+  isSS2022,
+} from '@/lib/xray/protocol-capabilities';
+import {
+  InboundFormBaseSchema,
+  InboundFormSchema,
+  type InboundFormValues,
+} from '@/schemas/forms/inbound-form';
+import { antdRule } from '@/utils/zodForm';
+import { Protocols } from '@/schemas/primitives';
+import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
+import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
+import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
+import { SniffingSchema } from '@/schemas/primitives/sniffing';
+import { TcpStreamSettingsSchema } from '@/schemas/protocols/stream/tcp';
+import { KcpStreamSettingsSchema } from '@/schemas/protocols/stream/kcp';
+import { WsStreamSettingsSchema } from '@/schemas/protocols/stream/ws';
+import { GrpcStreamSettingsSchema } from '@/schemas/protocols/stream/grpc';
+import { HttpUpgradeStreamSettingsSchema } from '@/schemas/protocols/stream/httpupgrade';
+import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
+import { DateTimePicker } from '@/components/form';
+import { FinalMaskForm } from '@/lib/xray/forms/transport';
+import './InboundFormModal.css';
+
+import { AdvancedAllEditor, AdvancedSliceEditor } from './advanced-editors';
+import {
+  HttpFields,
+  HysteriaFields,
+  MixedFields,
+  ShadowsocksFields,
+  TunFields,
+  TunnelFields,
+  VlessFields,
+  WireguardFields,
+} from './protocols';
+import {
+  ExternalProxyForm,
+  GrpcForm,
+  HttpUpgradeForm,
+  KcpForm,
+  RawForm,
+  SockoptForm,
+  WsForm,
+  XhttpForm,
+} from './transport';
+import { RealityForm, TlsForm } from './security';
+import { useSecurityActions } from './useSecurityActions';
+import { useInboundFallbacks } from './useInboundFallbacks';
+import FallbacksCard from './FallbacksCard';
+import SniffingTab from './SniffingTab';
+
+import type { DBInbound } from '@/models/dbinbound';
+import type { NodeRecord } from '@/api/queries/useNodesQuery';
+
+
+const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
+const TRAFFIC_RESETS = ['never', 'hourly', 'daily', 'weekly', 'monthly'] as const;
+const NODE_ELIGIBLE_PROTOCOLS = new Set<string>([
+  Protocols.VLESS,
+  Protocols.VMESS,
+  Protocols.TROJAN,
+  Protocols.SHADOWSOCKS,
+  Protocols.HYSTERIA,
+  Protocols.WIREGUARD,
+]);
+
+interface InboundFormModalProps {
+  open: boolean;
+  onClose: () => void;
+  onSaved: () => void;
+  mode: 'add' | 'edit';
+  dbInbound: DBInbound | null;
+  dbInbounds: DBInbound[];
+  availableNodes?: NodeRecord[];
+}
+
+function buildAddModeValues(): InboundFormValues {
+  const settings = createDefaultInboundSettings('vless') ?? undefined;
+  return rawInboundToFormValues({
+    protocol: 'vless',
+    settings,
+    streamSettings: {
+      network: 'tcp',
+      security: 'none',
+      tcpSettings: TcpStreamSettingsSchema.parse({ header: { type: 'none' } }),
+    },
+    sniffing: SniffingSchema.parse({}),
+    port: RandomUtil.randomInteger(10000, 60000),
+    listen: '',
+    tag: '',
+    enable: true,
+    trafficReset: 'never',
+  });
+}
+
+export default function InboundFormModal({
+  open,
+  onClose,
+  onSaved,
+  mode,
+  dbInbound,
+  dbInbounds,
+  availableNodes,
+}: InboundFormModalProps) {
+  const { t } = useTranslation();
+  const [messageApi, messageContextHolder] = message.useMessage();
+  const [form] = Form.useForm<InboundFormValues>();
+  const [saving, setSaving] = useState(false);
+  const {
+    fallbacks,
+    fallbackChildOptions,
+    loadFallbacks,
+    saveFallbacks,
+    addFallback,
+    updateFallback,
+    removeFallback,
+    moveFallback,
+    addAllFallbacks,
+  } = useInboundFallbacks(dbInbound, dbInbounds);
+
+  const selectableNodes = (availableNodes || []).filter((n) => n.enable);
+  const protocol = (Form.useWatch('protocol', form) ?? '') as string;
+  const isNodeEligible = NODE_ELIGIBLE_PROTOCOLS.has(protocol);
+  const sniffingEnabled = Form.useWatch(['sniffing', 'enabled'], form) ?? false;
+  const vlessEncryption = Form.useWatch(['settings', 'encryption'], form) ?? '';
+  const ssMethod = Form.useWatch(['settings', 'method'], form);
+  const isSSWith2022 = isSS2022({
+    protocol,
+    settings: typeof ssMethod === 'string' ? { method: ssMethod } : {},
+  });
+  const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false;
+  const network = Form.useWatch(['streamSettings', 'network'], form) ?? '';
+  const security = Form.useWatch(['streamSettings', 'security'], form) ?? 'none';
+  const streamEnabled = canEnableStream({ protocol });
+  const isFallbackHost =
+    (protocol === Protocols.VLESS || protocol === Protocols.TROJAN)
+    && network === 'tcp'
+    && (security === 'tls' || security === 'reality');
+
+  const {
+    genRealityKeypair,
+    clearRealityKeypair,
+    genMldsa65,
+    clearMldsa65,
+    randomizeRealityTarget,
+    randomizeShortIds,
+    getNewEchCert,
+    clearEchCert,
+    generateRandomPinHash,
+    setCertFromPanel,
+    clearCertFiles,
+    onSecurityChange,
+  } = useSecurityActions({ form, setSaving, messageApi });
+
+  const toggleExternalProxy = (on: boolean) => {
+    if (on) {
+      const port = (form.getFieldValue('port') as number) ?? 443;
+      form.setFieldValue(['streamSettings', 'externalProxy'], [{
+        forceTls: 'same',
+        dest: typeof window !== 'undefined' ? window.location.hostname : '',
+        port,
+        remark: '',
+        sni: '',
+        fingerprint: '',
+        alpn: [],
+      }]);
+    } else {
+      form.setFieldValue(['streamSettings', 'externalProxy'], []);
+    }
+  };
+
+  const toggleSockopt = (on: boolean) => {
+    if (on) {
+      form.setFieldValue(
+        ['streamSettings', 'sockopt'],
+        SockoptStreamSettingsSchema.parse({}),
+      );
+    } else {
+      form.setFieldValue(['streamSettings', 'sockopt'], undefined);
+    }
+  };
+  const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form);
+  const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0
+    ? Wireguard.generateKeypair(wgSecretKey).publicKey
+    : '';
+
+  const regenInboundWg = () => {
+    const kp = Wireguard.generateKeypair();
+    form.setFieldValue(['settings', 'secretKey'], kp.privateKey);
+  };
+
+  const regenWgPeerKeypair = (peerName: number) => {
+    const kp = Wireguard.generateKeypair();
+    form.setFieldValue(['settings', 'peers', peerName, 'privateKey'], kp.privateKey);
+    form.setFieldValue(['settings', 'peers', peerName, 'publicKey'], kp.publicKey);
+  };
+
+  const matchesVlessAuth = (
+    block: { id?: string; label?: string } | undefined | null,
+    authId: string,
+  ) => {
+    if (block?.id === authId) return true;
+    const label = (block?.label || '').toLowerCase().replace(/[-_\s]/g, '');
+    if (authId === 'mlkem768') return label.includes('mlkem768');
+    if (authId === 'x25519') return label.includes('x25519');
+    return false;
+  };
+
+  const getNewVlessEnc = async (authId: string) => {
+    if (!authId) return;
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
+      if (!msg?.success) return;
+      const obj = msg.obj as {
+        auths?: { decryption: string; encryption: string; label?: string; id?: string }[];
+      };
+      const block = (obj.auths || []).find((a) => matchesVlessAuth(a, authId));
+      if (!block) return;
+      form.setFieldValue(['settings', 'decryption'], block.decryption);
+      form.setFieldValue(['settings', 'encryption'], block.encryption);
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const clearVlessEnc = () => {
+    form.setFieldValue(['settings', 'decryption'], 'none');
+    form.setFieldValue(['settings', 'encryption'], 'none');
+  };
+
+  const selectedVlessAuth = (() => {
+    const enc = typeof vlessEncryption === 'string' ? vlessEncryption : '';
+    if (!enc || enc === 'none') return 'None';
+    const parts = enc.split('.').filter(Boolean);
+    const authKey = parts[parts.length - 1] || '';
+    if (!authKey) return t('pages.inbounds.vlessAuthCustom');
+    return authKey.length > 300
+      ? t('pages.inbounds.vlessAuthMlkem768')
+      : t('pages.inbounds.vlessAuthX25519');
+  })();
+
+  useEffect(() => {
+    if (!open) return;
+    const initial = mode === 'edit' && dbInbound
+      ? rawInboundToFormValues(dbInbound)
+      : buildAddModeValues();
+    form.resetFields();
+    form.setFieldsValue(initial);
+    if (
+      mode === 'edit'
+      && dbInbound
+      && (dbInbound.protocol === Protocols.VLESS || dbInbound.protocol === Protocols.TROJAN)
+    ) {
+      loadFallbacks(dbInbound.id);
+    } else {
+      loadFallbacks(null);
+    }
+
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [open, mode, dbInbound, form]);
+
+  // Why: protocol picker reset cascades through the form — clearing the
+  // settings DU branch and dropping a nodeId that no longer applies. The
+  // legacy modal did this imperatively in onProtocolChange; here we hook
+  // into AntD's onValuesChange and let setFieldValue keep the rest of
+  // the form state intact.
+  const onValuesChange = (changed: Partial<InboundFormValues>) => {
+    if (mode === 'edit') return;
+    if ('protocol' in changed && typeof changed.protocol === 'string') {
+      const next = changed.protocol;
+      const settings = createDefaultInboundSettings(next) ?? undefined;
+      form.setFieldValue('settings', settings);
+      if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
+        form.setFieldValue('nodeId', null);
+      }
+      // Hysteria uses its dedicated transport — force the network branch
+      // so the stream tab renders the hysteria sub-form, not the leftover
+      // tcpSettings from the previous protocol. When leaving hysteria,
+      // snap back to TCP so the standard network selector has a valid
+      // starting point.
+      if (next === Protocols.HYSTERIA) {
+        const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
+        tls.certificates = [{
+          useFile: true,
+          certificateFile: '',
+          keyFile: '',
+          certificate: [],
+          key: [],
+          oneTimeLoading: false,
+          usage: 'encipherment',
+          buildChain: false,
+        }];
+        form.setFieldValue('streamSettings', {
+          network: 'hysteria',
+          security: 'tls',
+          hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
+          tlsSettings: tls,
+          // Hysteria2 needs an obfs wrapper on the FinalMask side; seed
+          // it with salamander + a random password so the listener boots
+          // with a usable default. Re-selecting Hysteria from another
+          // protocol re-runs this and refreshes the password — that's
+          // intentional, the form was already being reset.
+          finalmask: {
+            tcp: [],
+            udp: [{
+              type: 'salamander',
+              settings: { password: RandomUtil.randomLowerAndNum(16) },
+            }],
+          },
+        });
+      } else {
+        const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
+        if (current?.network === 'hysteria') {
+          form.setFieldValue('streamSettings', { network: 'tcp', security: 'none', tcpSettings: {} });
+        }
+      }
+    }
+  };
+
+  const submit = async () => {
+    try {
+      await form.validateFields();
+    } catch {
+      return;
+    }
+    // Why getFieldsValue(true) instead of the validateFields return value:
+    // rc-component/form's validateFields filters its output by REGISTERED
+    // name paths. settings.clients and settings.fallbacks have no Form.Item
+    // bound to them (clients are managed via the standalone Client modal,
+    // not inside this inbound modal) — so validateFields would drop them
+    // and the update wire payload would silently delete every client on
+    // every save. getFieldsValue(true) returns the entire form store and
+    // keeps those sub-trees intact.
+    const values = form.getFieldsValue(true) as InboundFormValues;
+    const parsed = InboundFormSchema.safeParse(values);
+    if (!parsed.success) {
+      const issue = parsed.error.issues[0];
+      const path = Array.isArray(issue?.path) && issue.path.length > 0
+        ? issue.path.join('.')
+        : '';
+      const baseMsg = issue?.message ?? 'somethingWentWrong';
+      const display = path ? `${path}: ${baseMsg}` : baseMsg;
+      messageApi.error(t(baseMsg, { defaultValue: display }));
+      console.error('[InboundFormModal] schema validation failed', {
+        path: issue?.path,
+        message: issue?.message,
+        values,
+      });
+      return;
+    }
+    setSaving(true);
+    try {
+      const payload = formValuesToWirePayload(parsed.data);
+      const url = mode === 'edit' && dbInbound
+        ? `/panel/api/inbounds/update/${dbInbound.id}`
+        : '/panel/api/inbounds/add';
+      const msg = await HttpUtil.post(url, payload);
+      if (msg?.success) {
+        if (isFallbackHost) {
+          const obj = msg.obj as { id?: number; Id?: number } | null;
+          const masterId = mode === 'edit'
+            ? dbInbound!.id
+            : (obj?.id ?? obj?.Id ?? 0);
+          if (masterId) await saveFallbacks(masterId);
+        }
+        onSaved();
+        onClose();
+      }
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const title = mode === 'edit'
+    ? t('pages.inbounds.modifyInbound')
+    : t('pages.inbounds.addInbound');
+
+  const okText = mode === 'edit'
+    ? t('pages.clients.submitEdit')
+    : t('create');
+
+  const basicTab = (
+    <>
+      <Form.Item name="tag" hidden noStyle><Input /></Form.Item>
+      <Form.Item name="up" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="down" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="total" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="expiryTime" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="lastTrafficResetTime" hidden noStyle><InputNumber /></Form.Item>
+      <Form.Item name="clientStats" hidden noStyle><Input /></Form.Item>
+
+      <Form.Item name="enable" label={t('enable')} valuePropName="checked">
+        <Switch />
+      </Form.Item>
+
+      <Form.Item name="remark" label={t('pages.inbounds.remark')}>
+        <Input />
+      </Form.Item>
+
+      {selectableNodes.length > 0 && isNodeEligible && (
+        <Form.Item name="nodeId" label={t('pages.inbounds.deployTo')}>
+          <Select
+            disabled={mode === 'edit'}
+            placeholder={t('pages.inbounds.localPanel')}
+            allowClear
+            options={selectableNodes.map((n) => ({
+              value: n.id,
+              label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
+              disabled: n.status === 'offline',
+            }))}
+          />
+        </Form.Item>
+      )}
+
+      <Form.Item name="protocol" label={t('pages.inbounds.protocol')}>
+        <Select disabled={mode === 'edit'} options={PROTOCOL_OPTIONS} />
+      </Form.Item>
+
+      <Form.Item name="listen" label={t('pages.inbounds.address')}>
+        <Input placeholder={t('pages.inbounds.monitorDesc')} />
+      </Form.Item>
+
+      <Form.Item
+        name="port"
+        label={t('pages.inbounds.port')}
+        rules={[antdRule(InboundFormBaseSchema.shape.port, t)]}
+      >
+        <InputNumber min={1} max={65535} />
+      </Form.Item>
+
+      <Form.Item
+        label={
+          <Tooltip title={t('pages.inbounds.meansNoLimit')}>
+            {t('pages.inbounds.totalFlow')}
+          </Tooltip>
+        }
+      >
+        <Form.Item
+          noStyle
+          shouldUpdate={(prev, curr) => prev.total !== curr.total}
+        >
+          {({ getFieldValue, setFieldValue }) => {
+            const totalBytes = (getFieldValue('total') as number) ?? 0;
+            const totalGB = totalBytes
+              ? Math.round((totalBytes / SizeFormatter.ONE_GB) * 100) / 100
+              : 0;
+            return (
+              <InputNumber
+                value={totalGB}
+                min={0}
+                step={1}
+                onChange={(v) => {
+                  const bytes = NumberFormatter.toFixed(
+                    (Number(v) || 0) * SizeFormatter.ONE_GB,
+                    0,
+                  );
+                  setFieldValue('total', bytes);
+                }}
+              />
+            );
+          }}
+        </Form.Item>
+      </Form.Item>
+
+      <Form.Item name="trafficReset" label={t('pages.inbounds.periodicTrafficResetTitle')}>
+        <Select
+          options={TRAFFIC_RESETS.map((r) => ({
+            value: r,
+            label: t(`pages.inbounds.periodicTrafficReset.${r}`),
+          }))}
+        />
+      </Form.Item>
+
+      <Form.Item
+        label={
+          <Tooltip title={t('pages.inbounds.leaveBlankToNeverExpire')}>
+            {t('pages.inbounds.expireDate')}
+          </Tooltip>
+        }
+      >
+        <Form.Item
+          noStyle
+          shouldUpdate={(prev, curr) => prev.expiryTime !== curr.expiryTime}
+        >
+          {({ getFieldValue, setFieldValue }) => {
+            const expiry = (getFieldValue('expiryTime') as number) ?? 0;
+            return (
+              <DateTimePicker
+                value={expiry > 0 ? dayjs(expiry) : null}
+                onChange={(d) => setFieldValue('expiryTime', d ? d.valueOf() : 0)}
+              />
+            );
+          }}
+        </Form.Item>
+      </Form.Item>
+    </>
+  );
+
+  const fallbacksCard = (
+    <FallbacksCard
+      fallbacks={fallbacks}
+      fallbackChildOptions={fallbackChildOptions}
+      addFallback={addFallback}
+      updateFallback={updateFallback}
+      removeFallback={removeFallback}
+      moveFallback={moveFallback}
+      addAllFallbacks={addAllFallbacks}
+    />
+  );
+
+  const protocolTab = (
+    <>
+      {protocol === Protocols.WIREGUARD && <WireguardFields wgPubKey={wgPubKey} regenInboundWg={regenInboundWg} regenWgPeerKeypair={regenWgPeerKeypair} />}
+
+      {protocol === Protocols.TUN && <TunFields />}
+
+      {protocol === Protocols.TUNNEL && <TunnelFields />}
+
+      {protocol === Protocols.HTTP && <HttpFields />}
+      {protocol === Protocols.MIXED && <MixedFields mixedUdpOn={mixedUdpOn} />}
+
+      {protocol === Protocols.SHADOWSOCKS && <ShadowsocksFields form={form} isSSWith2022={isSSWith2022} />}
+
+      {protocol === Protocols.VLESS && <VlessFields saving={saving} selectedVlessAuth={selectedVlessAuth} network={network} security={security} getNewVlessEnc={getNewVlessEnc} clearVlessEnc={clearVlessEnc} />}
+
+      {isFallbackHost && fallbacksCard}
+    </>
+  );
+
+  // Switching `network` swaps which per-network key (tcpSettings,
+  // wsSettings, grpcSettings, ...) appears on the wire. Clear the old
+  // network's blob and seed the new one with the schema defaults so the
+  // Form.Items inside it have valid initial values (KCP needs MTU=1350
+  // etc., not empty strings).
+  // Seed each network's settings blob with its Zod schema defaults so
+  // every Form.Item inside the network sub-form has a defined starting
+  // value. XHTTP in particular has ~20 fields (sessionPlacement,
+  // seqPlacement, xPaddingMethod, uplinkHTTPMethod, ...) whose value
+  // is the literal "" sentinel meaning "let xray-core pick its
+  // default". Without seeding "", the Form.Item reads `undefined` and
+  // the Select shows blank instead of the "Default (path)" option.
+  const newStreamSlice = (n: string): Record<string, unknown> => {
+    switch (n) {
+      case 'tcp': return TcpStreamSettingsSchema.parse({ header: { type: 'none' } });
+      case 'kcp': return KcpStreamSettingsSchema.parse({});
+      case 'ws': return WsStreamSettingsSchema.parse({});
+      case 'grpc': return GrpcStreamSettingsSchema.parse({});
+      case 'httpupgrade': return HttpUpgradeStreamSettingsSchema.parse({});
+      case 'xhttp': return XHttpStreamSettingsSchema.parse({});
+      default: return {};
+    }
+  };
+  const onNetworkChange = (next: string) => {
+    const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings'];
+    const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
+    const cleaned: Record<string, unknown> = { ...current, network: next };
+    for (const k of ALL) {
+      if (k !== `${next}Settings`) delete cleaned[k];
+    }
+    cleaned[`${next}Settings`] = newStreamSlice(next);
+    // mKCP wants a UDP mask wrapper on the FinalMask side; seed it with
+    // `mkcp-original` so the inbound boots with a sensible default
+    // instead of unobfuscated mKCP traffic. The user can still edit or
+    // clear the mask via the FinalMask section.
+    if (next === 'kcp') {
+      const fm = (cleaned.finalmask as Record<string, unknown> | undefined) ?? {};
+      const udp = Array.isArray(fm.udp) ? (fm.udp as unknown[]) : [];
+      const hasMkcp = udp.some((m) => {
+        const entry = m as { type?: string };
+        return entry?.type === 'mkcp-original';
+      });
+      if (!hasMkcp) {
+        cleaned.finalmask = {
+          ...fm,
+          udp: [...udp, { type: 'mkcp-original', settings: {} }],
+        };
+      }
+    }
+    form.setFieldValue('streamSettings', cleaned);
+  };
+
+  const streamTab = (
+    <>
+      {protocol !== Protocols.HYSTERIA && (
+        <Form.Item label={t('transmission')} name={['streamSettings', 'network']}>
+          <Select
+            style={{ width: '75%' }}
+            onChange={onNetworkChange}
+            options={[
+              { value: 'tcp', label: 'RAW' },
+              { value: 'kcp', label: 'mKCP' },
+              { value: 'ws', label: 'WebSocket' },
+              { value: 'grpc', label: 'gRPC' },
+              { value: 'httpupgrade', label: 'HTTPUpgrade' },
+              { value: 'xhttp', label: 'XHTTP' },
+            ]}
+          />
+        </Form.Item>
+      )}
+
+      {/* Inbound Hysteria stream sub-form. The transport for hysteria
+          isn't user-selectable (always 'hysteria'), so the network
+          dropdown is hidden above. Fields here mirror the legacy
+          HysteriaStreamSettings inbound class: version is locked to 2,
+          auth + udpIdleTimeout are required, masquerade is an optional
+          sub-object that lets xray-core disguise the listener as an
+          HTTP server when probed. */}
+      {protocol === Protocols.HYSTERIA && <HysteriaFields form={form} />}
+
+      {network === 'tcp' && <RawForm />}
+
+      {network === 'ws' && <WsForm />}
+
+      {network === 'grpc' && <GrpcForm />}
+
+      {network === 'xhttp' && <XhttpForm form={form} />}
+
+      {network === 'httpupgrade' && <HttpUpgradeForm />}
+
+      {network === 'kcp' && <KcpForm />}
+
+      <ExternalProxyForm toggleExternalProxy={toggleExternalProxy} />
+
+      <SockoptForm toggleSockopt={toggleSockopt} />
+
+      <FinalMaskForm
+        name={['streamSettings', 'finalmask']}
+        network={network as string}
+        protocol={protocol}
+        form={form}
+      />
+    </>
+  );
+
+  const securityTab = (
+    <>
+      <Form.Item name={['streamSettings', 'security']} hidden noStyle>
+        <Input />
+      </Form.Item>
+      <Form.Item label={t('pages.inbounds.securityTab')}>
+        <Form.Item
+          noStyle
+          shouldUpdate={(prev, curr) =>
+            prev.streamSettings?.security !== curr.streamSettings?.security
+            || prev.streamSettings?.network !== curr.streamSettings?.network
+            || prev.protocol !== curr.protocol
+          }
+        >
+          {({ getFieldValue }) => {
+            const sec = getFieldValue(['streamSettings', 'security']) ?? 'none';
+            const net = getFieldValue(['streamSettings', 'network']) ?? '';
+            const proto = getFieldValue('protocol') ?? '';
+            const tlsOk = canEnableTls({ protocol: proto, streamSettings: { network: net, security: sec } });
+            const realityOk = canEnableReality({ protocol: proto, streamSettings: { network: net, security: sec } });
+            const tlsOnly = proto === Protocols.HYSTERIA;
+            return (
+              <Radio.Group
+                value={sec}
+                buttonStyle="solid"
+                disabled={!tlsOk}
+                onChange={(e) => onSecurityChange(e.target.value)}
+              >
+                {!tlsOnly && <Radio.Button value="none">{t('none')}</Radio.Button>}
+                <Radio.Button value="tls">TLS</Radio.Button>
+                {realityOk && <Radio.Button value="reality">Reality</Radio.Button>}
+              </Radio.Group>
+            );
+          }}
+        </Form.Item>
+      </Form.Item>
+
+      {security === 'tls' && (
+        <TlsForm
+          saving={saving}
+          setCertFromPanel={setCertFromPanel}
+          clearCertFiles={clearCertFiles}
+          generateRandomPinHash={generateRandomPinHash}
+          getNewEchCert={getNewEchCert}
+          clearEchCert={clearEchCert}
+        />
+      )}
+
+      {security === 'reality' && (
+        <RealityForm
+          saving={saving}
+          randomizeRealityTarget={randomizeRealityTarget}
+          randomizeShortIds={randomizeShortIds}
+          genRealityKeypair={genRealityKeypair}
+          clearRealityKeypair={clearRealityKeypair}
+          genMldsa65={genMldsa65}
+          clearMldsa65={clearMldsa65}
+        />
+      )}
+    </>
+  );
+
+  const advancedTab = (
+    <div className="advanced-shell">
+      <div className="advanced-panel">
+        <div className="advanced-panel__header">
+          <div>
+            <div className="advanced-panel__title">{t('pages.inbounds.advanced.title')}</div>
+            <div className="advanced-panel__subtitle">{t('pages.inbounds.advanced.subtitle')}</div>
+          </div>
+        </div>
+        <Tabs
+          className="advanced-inner-tabs"
+          items={[
+            {
+              key: 'all',
+              label: t('pages.inbounds.advanced.all'),
+              children: (
+                <>
+                  <div className="advanced-editor-meta">
+                    {t('pages.inbounds.advanced.allHelp')}
+                  </div>
+                  <AdvancedAllEditor form={form} streamEnabled={streamEnabled} />
+                </>
+              ),
+            },
+            {
+              key: 'settings',
+              label: t('pages.inbounds.advanced.settings'),
+              children: (
+                <>
+                  <div className="advanced-editor-meta">
+                    {t('pages.inbounds.advanced.settingsHelp')}{' '}
+                    <code>{'{ settings: { ... } }'}</code>.
+                  </div>
+                  <AdvancedSliceEditor
+                    form={form}
+                    path="settings"
+                    wrapKey="settings"
+                    minHeight="320px"
+                    maxHeight="540px"
+                  />
+                </>
+              ),
+            },
+            ...(streamEnabled
+              ? [{
+                key: 'stream',
+                label: t('pages.inbounds.advanced.stream'),
+                children: (
+                  <>
+                    <div className="advanced-editor-meta">
+                      {t('pages.inbounds.advanced.streamHelp')}{' '}
+                      <code>{'{ streamSettings: { ... } }'}</code>.
+                    </div>
+                    <AdvancedSliceEditor
+                      form={form}
+                      path="streamSettings"
+                      wrapKey="streamSettings"
+                      minHeight="320px"
+                      maxHeight="540px"
+                    />
+                  </>
+                ),
+              }]
+              : []),
+            {
+              key: 'sniffing',
+              label: t('pages.inbounds.advanced.sniffing'),
+              children: (
+                <>
+                  <div className="advanced-editor-meta">
+                    {t('pages.inbounds.advanced.sniffingHelp')}{' '}
+                    <code>{'{ sniffing: { ... } }'}</code>.
+                  </div>
+                  <AdvancedSliceEditor
+                    form={form}
+                    path="sniffing"
+                    wrapKey="sniffing"
+                    minHeight="240px"
+                    maxHeight="420px"
+                  />
+                </>
+              ),
+            },
+          ]}
+        />
+      </div>
+    </div>
+  );
+
+  const sniffingTab = <SniffingTab sniffingEnabled={sniffingEnabled} />;
+
+  return (
+    <>
+      {messageContextHolder}
+      <Modal
+        open={open}
+        title={title}
+        okText={okText}
+        cancelText={t('close')}
+        confirmLoading={saving}
+        mask={{ closable: false }}
+        width={780}
+        onOk={submit}
+        onCancel={onClose}
+        destroyOnHidden
+      >
+        <Form
+          form={form}
+          colon={false}
+          labelCol={{ sm: { span: 8 } }}
+          wrapperCol={{ sm: { span: 14 } }}
+          onValuesChange={onValuesChange}
+        >
+          <Tabs items={[
+            // forceRender on every tab so all Form.Items register at modal
+            // open, not lazily on first visit. Without it, AntD's items API
+            // lazy-mounts inactive tabs — their fields don't register, so
+            // Form.useWatch on a parent path (e.g. 'sniffing') returns the
+            // partial-view {} until the user touches the tab and the
+            // inner Form.Item for `sniffing.enabled` registers.
+            { key: 'basic', label: t('pages.xray.basicTemplate'), children: basicTab, forceRender: true },
+            ...(([
+              Protocols.VLESS,
+              Protocols.SHADOWSOCKS,
+              Protocols.HTTP,
+              Protocols.MIXED,
+              Protocols.TUNNEL,
+              Protocols.TUN,
+              Protocols.WIREGUARD,
+            ] as string[]).includes(protocol) || isFallbackHost
+              ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab, forceRender: true }]
+              : []),
+            ...(streamEnabled
+              ? [
+                { key: 'stream', label: t('pages.inbounds.streamTab'), children: streamTab, forceRender: true },
+                { key: 'security', label: t('pages.inbounds.securityTab'), children: securityTab, forceRender: true },
+              ]
+              : []),
+            { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab, forceRender: true },
+            { key: 'advanced', label: t('pages.xray.advancedTemplate'), children: advancedTab, forceRender: true },
+          ]} />
+        </Form>
+      </Modal>
+    </>
+  );
+}

+ 67 - 0
frontend/src/pages/inbounds/form/SniffingTab.tsx

@@ -0,0 +1,67 @@
+import { useTranslation } from 'react-i18next';
+import { Checkbox, Form, Select, Switch } from 'antd';
+
+import { SNIFFING_OPTION } from '@/schemas/primitives';
+
+export default function SniffingTab({ sniffingEnabled }: { sniffingEnabled: boolean }) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
+        <Switch />
+      </Form.Item>
+
+      {sniffingEnabled && (
+        <>
+          <Form.Item name={['sniffing', 'destOverride']} wrapperCol={{ span: 24 }}>
+            <Checkbox.Group>
+              {Object.entries(SNIFFING_OPTION).map(([key, value]) => (
+                <Checkbox key={key} value={value}>{key}</Checkbox>
+              ))}
+            </Checkbox.Group>
+          </Form.Item>
+
+          <Form.Item
+            name={['sniffing', 'metadataOnly']}
+            label={t('pages.inbounds.sniffingMetadataOnly')}
+            valuePropName="checked"
+          >
+            <Switch />
+          </Form.Item>
+
+          <Form.Item
+            name={['sniffing', 'routeOnly']}
+            label={t('pages.inbounds.sniffingRouteOnly')}
+            valuePropName="checked"
+          >
+            <Switch />
+          </Form.Item>
+
+          <Form.Item
+            name={['sniffing', 'ipsExcluded']}
+            label={t('pages.inbounds.sniffingIpsExcluded')}
+          >
+            <Select
+              mode="tags"
+              tokenSeparators={[',']}
+              placeholder="IP/CIDR/geoip:*/ext:*"
+              style={{ width: '100%' }}
+            />
+          </Form.Item>
+
+          <Form.Item
+            name={['sniffing', 'domainsExcluded']}
+            label={t('pages.inbounds.sniffingDomainsExcluded')}
+          >
+            <Select
+              mode="tags"
+              tokenSeparators={[',']}
+              placeholder="domain:*/ext:*"
+              style={{ width: '100%' }}
+            />
+          </Form.Item>
+        </>
+      )}
+    </>
+  );
+}

+ 184 - 0
frontend/src/pages/inbounds/form/advanced-editors.tsx

@@ -0,0 +1,184 @@
+import { useEffect, useRef, useState } from 'react';
+import { Form, type FormInstance } from 'antd';
+import type { NamePath } from 'antd/es/form/interface';
+
+import { JsonEditor } from '@/components/form';
+import {
+  pruneEmpty,
+  normalizeSniffing,
+  normalizeClients,
+  dropLegacyOptionalEmpties,
+} from '@/lib/xray/inbound-form-adapter';
+import type { InboundFormValues } from '@/schemas/forms/inbound-form';
+
+// Sub-editor for one slice of the form (settings, streamSettings, sniffing).
+// Holds a local text buffer so the user can type freely; on every keystroke
+// we try to JSON.parse and forward the result to form state. Invalid JSON
+// is held in the buffer until the next valid moment — no panic on partial
+// input. The buffer seeds once on mount; the modal's destroyOnHidden makes
+// each open a fresh editor instance, so we don't need to re-sync on outer
+// form changes.
+export function AdvancedSliceEditor({
+  form,
+  path,
+  wrapKey,
+  minHeight,
+  maxHeight,
+}: {
+  form: FormInstance<InboundFormValues>;
+  path: NamePath;
+  // When set, the editor wraps the inner value with `{ [wrapKey]: ... }` so
+  // the JSON the user sees matches the wire shape's slice envelope (e.g.
+  // `{ "settings": { ... } }`). Edits unwrap the outer key before writing
+  // back to the form. Mirrors the legacy modal's wrappedConfigValue.
+  wrapKey?: string;
+  minHeight?: string;
+  maxHeight?: string;
+}) {
+  const serialize = (value: unknown): string => {
+    const inner = value ?? {};
+    return JSON.stringify(wrapKey ? { [wrapKey]: inner } : inner, null, 2);
+  };
+
+  // preserve: true so useWatch returns the full subtree from the form
+  // store — without it, useWatch goes through getFieldsValue() which
+  // filters out unregistered fields. Slices like `settings` would lose
+  // their `clients` / `fallbacks` sub-trees because those aren't bound
+  // to any Form.Item.
+  const watched = Form.useWatch(path, { form, preserve: true });
+  const lastEmitRef = useRef<string>('');
+  const [text, setText] = useState(() => {
+    const initial = serialize(form.getFieldValue(path));
+    lastEmitRef.current = initial;
+    return initial;
+  });
+
+  useEffect(() => {
+    const formStr = serialize(watched);
+    if (formStr === lastEmitRef.current) return;
+    setText(formStr);
+    lastEmitRef.current = formStr;
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [watched, wrapKey]);
+
+  return (
+    <JsonEditor
+      value={text}
+      minHeight={minHeight}
+      maxHeight={maxHeight}
+      onChange={(next) => {
+        setText(next);
+        try {
+          const parsed = JSON.parse(next);
+          const toWrite = wrapKey && parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+            ? (parsed as Record<string, unknown>)[wrapKey] ?? {}
+            : parsed;
+          form.setFieldValue(path, toWrite);
+          lastEmitRef.current = JSON.stringify(wrapKey ? { [wrapKey]: toWrite } : toWrite, null, 2);
+        } catch {
+          // invalid JSON; keep buffer, don't push to form
+        }
+      }}
+    />
+  );
+}
+
+// The "All" editor shows the full inbound JSON in one editor: top-level
+// connection fields plus the three nested sub-objects (settings,
+// streamSettings, sniffing). Edits round-trip back to the form's slices,
+// mirroring the legacy modal's setAdvancedAllValue behavior. Reactivity
+// works the same way as AdvancedSliceEditor: useWatch on the slices we
+// care about, lastEmitRef as the "we wrote this" guard.
+export function AdvancedAllEditor({
+  form,
+  streamEnabled,
+}: {
+  form: FormInstance<InboundFormValues>;
+  streamEnabled: boolean;
+}) {
+  // preserve: true — default useWatch returns only registered fields, so
+  // sub-trees we never bound (settings.clients/fallbacks, sniffing
+  // defaults, etc.) wouldn't show up. preserve switches the read to
+  // getFieldsValue(true) which returns the full form store.
+  const wListen = Form.useWatch('listen', { form, preserve: true });
+  const wPort = Form.useWatch('port', { form, preserve: true });
+  const wProtocol = Form.useWatch('protocol', { form, preserve: true });
+  const wTag = Form.useWatch('tag', { form, preserve: true });
+  const wSettings = Form.useWatch('settings', { form, preserve: true });
+  const wSniffing = Form.useWatch('sniffing', { form, preserve: true });
+  const wStream = Form.useWatch('streamSettings', { form, preserve: true });
+
+  const serialize = () => {
+    // Apply the same prune/normalize as the wire payload so the JSON
+    // shown here is what the panel actually POSTs (no empty defaults,
+    // disabled sniffing as { enabled: false }, finalmask dropped when
+    // there are no masks).
+    const settingsView = (pruneEmpty(wSettings ?? {}) ?? {}) as Record<string, unknown>;
+    if (typeof wProtocol === 'string' && Array.isArray(settingsView.clients)) {
+      settingsView.clients = normalizeClients(wProtocol, settingsView.clients);
+    }
+    const streamView = streamEnabled
+      ? ((pruneEmpty(wStream ?? {}) ?? {}) as Record<string, unknown>)
+      : undefined;
+    dropLegacyOptionalEmpties(settingsView, streamView);
+    const out: Record<string, unknown> = {
+      listen: wListen ?? '',
+      port: wPort ?? 0,
+      protocol: wProtocol ?? '',
+      tag: wTag ?? '',
+      settings: settingsView,
+      sniffing: normalizeSniffing(wSniffing as Parameters<typeof normalizeSniffing>[0]),
+    };
+    if (streamView) out.streamSettings = streamView;
+    return JSON.stringify(out, null, 2);
+  };
+
+  const lastEmitRef = useRef<string>('');
+  const [text, setText] = useState(() => {
+    const initial = serialize();
+    lastEmitRef.current = initial;
+    return initial;
+  });
+
+  useEffect(() => {
+    const formStr = serialize();
+    if (formStr === lastEmitRef.current) return;
+    setText(formStr);
+    lastEmitRef.current = formStr;
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [wListen, wPort, wProtocol, wTag, wSettings, wSniffing, wStream, streamEnabled]);
+
+  return (
+    <JsonEditor
+      value={text}
+      minHeight="340px"
+      maxHeight="560px"
+      onChange={(next) => {
+        setText(next);
+        let parsed: Record<string, unknown>;
+        try {
+          parsed = JSON.parse(next) as Record<string, unknown>;
+        } catch {
+          return;
+        }
+        if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return;
+        if (typeof parsed.listen === 'string') form.setFieldValue('listen', parsed.listen);
+        if (typeof parsed.port === 'number' && Number.isFinite(parsed.port)) {
+          form.setFieldValue('port', parsed.port);
+        }
+        if (typeof parsed.protocol === 'string') form.setFieldValue('protocol', parsed.protocol);
+        if (typeof parsed.tag === 'string') form.setFieldValue('tag', parsed.tag);
+        if (parsed.settings && typeof parsed.settings === 'object') {
+          form.setFieldValue('settings', parsed.settings);
+        }
+        if (parsed.sniffing && typeof parsed.sniffing === 'object') {
+          form.setFieldValue('sniffing', parsed.sniffing);
+        }
+        if (streamEnabled && parsed.streamSettings && typeof parsed.streamSettings === 'object') {
+          form.setFieldValue('streamSettings', parsed.streamSettings);
+        }
+        lastEmitRef.current = next;
+      }}
+    />
+  );
+}

+ 1 - 0
frontend/src/pages/inbounds/form/index.ts

@@ -0,0 +1 @@
+export { default as InboundFormModal } from './InboundFormModal';

+ 47 - 0
frontend/src/pages/inbounds/form/protocols/accounts-list.tsx

@@ -0,0 +1,47 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, Space } from 'antd';
+import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
+
+import { RandomUtil } from '@/utils';
+import { InputAddon } from '@/components/ui';
+
+export default function AccountsList() {
+  const { t } = useTranslation();
+  return (
+    <Form.List name={['settings', 'accounts']}>
+      {(fields, { add, remove }) => (
+        <>
+          <Form.Item label={t('pages.inbounds.form.accounts')}>
+            <Button
+              size="small"
+              onClick={() => add({
+                user: RandomUtil.randomLowerAndNum(8),
+                pass: RandomUtil.randomLowerAndNum(12),
+              })}
+            >
+              <PlusOutlined /> {t('add')}
+            </Button>
+          </Form.Item>
+          {fields.length > 0 && (
+            <Form.Item wrapperCol={{ span: 24 }}>
+              {fields.map((field, idx) => (
+                <Space.Compact key={field.key} className="mb-8" block>
+                  <InputAddon>{String(idx + 1)}</InputAddon>
+                  <Form.Item name={[field.name, 'user']} noStyle>
+                    <Input placeholder={t('username')} />
+                  </Form.Item>
+                  <Form.Item name={[field.name, 'pass']} noStyle>
+                    <Input placeholder={t('password')} />
+                  </Form.Item>
+                  <Button onClick={() => remove(field.name)}>
+                    <MinusOutlined />
+                  </Button>
+                </Space.Compact>
+              ))}
+            </Form.Item>
+          )}
+        </>
+      )}
+    </Form.List>
+  );
+}

+ 20 - 0
frontend/src/pages/inbounds/form/protocols/http.tsx

@@ -0,0 +1,20 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Switch } from 'antd';
+
+import AccountsList from './accounts-list';
+
+export default function HttpFields() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <AccountsList />
+      <Form.Item
+        name={['settings', 'allowTransparent']}
+        label={t('pages.inbounds.form.allowTransparent')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+    </>
+  );
+}

+ 16 - 8
frontend/src/components/HysteriaMasqueradeForm.tsx → frontend/src/pages/inbounds/form/protocols/hysteria.tsx

@@ -1,19 +1,27 @@
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { Form, Input, InputNumber, Select, Switch } from 'antd';
-import type { FormInstance } from 'antd';
+import { Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd';
 
 
-import HeaderMapEditor from '@/components/HeaderMapEditor';
+import { HeaderMapEditor } from '@/components/form';
 
 
 const MASQ_PATH = ['streamSettings', 'hysteriaSettings', 'masquerade'];
 const MASQ_PATH = ['streamSettings', 'hysteriaSettings', 'masquerade'];
 
 
-interface HysteriaMasqueradeFormProps {
-  form: FormInstance;
-}
-
-export default function HysteriaMasqueradeForm({ form }: HysteriaMasqueradeFormProps) {
+export default function HysteriaFields({ form }: { form: FormInstance }) {
   const { t } = useTranslation();
   const { t } = useTranslation();
   return (
   return (
     <>
     <>
+      <Form.Item
+        label={t('pages.inbounds.form.version')}
+        name={['streamSettings', 'hysteriaSettings', 'version']}
+      >
+        <InputNumber min={2} max={2} disabled />
+      </Form.Item>
+      <Form.Item
+        label={t('pages.inbounds.form.udpIdleTimeout')}
+        name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
+      >
+        <InputNumber min={1} style={{ width: '100%' }} />
+      </Form.Item>
+
       <Form.Item label={t('pages.inbounds.form.masquerade')}>
       <Form.Item label={t('pages.inbounds.form.masquerade')}>
         <Form.Item shouldUpdate noStyle>
         <Form.Item shouldUpdate noStyle>
           {() => {
           {() => {

+ 8 - 0
frontend/src/pages/inbounds/form/protocols/index.ts

@@ -0,0 +1,8 @@
+export { default as TunFields } from './tun';
+export { default as TunnelFields } from './tunnel';
+export { default as ShadowsocksFields } from './shadowsocks';
+export { default as WireguardFields } from './wireguard';
+export { default as HysteriaFields } from './hysteria';
+export { default as HttpFields } from './http';
+export { default as MixedFields } from './mixed';
+export { default as VlessFields } from './vless';

+ 33 - 0
frontend/src/pages/inbounds/form/protocols/mixed.tsx

@@ -0,0 +1,33 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, Select, Switch } from 'antd';
+
+import AccountsList from './accounts-list';
+
+export default function MixedFields({ mixedUdpOn }: { mixedUdpOn: boolean }) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <AccountsList />
+      <Form.Item name={['settings', 'auth']} label={t('pages.inbounds.info.auth')}>
+        <Select
+          options={[
+            { value: 'noauth', label: 'noauth' },
+            { value: 'password', label: 'password' },
+          ]}
+        />
+      </Form.Item>
+      <Form.Item
+        name={['settings', 'udp']}
+        label="UDP"
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      {mixedUdpOn && (
+        <Form.Item name={['settings', 'ip']} label="UDP IP">
+          <Input />
+        </Form.Item>
+      )}
+    </>
+  );
+}

+ 67 - 0
frontend/src/pages/inbounds/form/protocols/shadowsocks.tsx

@@ -0,0 +1,67 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, Select, Space, Switch, type FormInstance } from 'antd';
+import { ReloadOutlined } from '@ant-design/icons';
+
+import { RandomUtil } from '@/utils';
+import { SSMethodSchema } from '@/schemas/protocols/shared/shadowsocks';
+import type { InboundFormValues } from '@/schemas/forms/inbound-form';
+
+interface ShadowsocksFieldsProps {
+  form: FormInstance<InboundFormValues>;
+  isSSWith2022: boolean;
+}
+
+export default function ShadowsocksFields({ form, isSSWith2022 }: ShadowsocksFieldsProps) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['settings', 'method']} label={t('pages.inbounds.form.encryptionMethod')}>
+        <Select
+          onChange={(v) => {
+            form.setFieldValue(
+              ['settings', 'password'],
+              RandomUtil.randomShadowsocksPassword(v as string),
+            );
+          }}
+          options={SSMethodSchema.options.map((m) => ({ value: m, label: m }))}
+        />
+      </Form.Item>
+      {isSSWith2022 && (
+        <Form.Item label={t('password')}>
+          <Space.Compact block>
+            <Form.Item name={['settings', 'password']} noStyle>
+              <Input style={{ width: 'calc(100% - 32px)' }} />
+            </Form.Item>
+            <Button
+              icon={<ReloadOutlined />}
+              onClick={() => {
+                const method = form.getFieldValue(['settings', 'method']);
+                form.setFieldValue(
+                  ['settings', 'password'],
+                  RandomUtil.randomShadowsocksPassword(method as string),
+                );
+              }}
+            />
+          </Space.Compact>
+        </Form.Item>
+      )}
+      <Form.Item name={['settings', 'network']} label={t('pages.inbounds.network')}>
+        <Select
+          style={{ width: 120 }}
+          options={[
+            { value: 'tcp,udp', label: 'TCP, UDP' },
+            { value: 'tcp', label: 'TCP' },
+            { value: 'udp', label: 'UDP' },
+          ]}
+        />
+      </Form.Item>
+      <Form.Item
+        name={['settings', 'ivCheck']}
+        label="ivCheck"
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+    </>
+  );
+}

+ 93 - 0
frontend/src/pages/inbounds/form/protocols/tun.tsx

@@ -0,0 +1,93 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Space, Tooltip } from 'antd';
+import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
+
+export default function TunFields() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['settings', 'name']} label={t('pages.inbounds.info.interfaceName')}>
+        <Input placeholder="xray0" />
+      </Form.Item>
+      <Form.Item name={['settings', 'mtu']} label="MTU">
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.List name={['settings', 'gateway']}>
+        {(fields, { add, remove }) => (
+          <Form.Item label={t('pages.inbounds.info.gateway')}>
+            <Button size="small" onClick={() => add('')}>
+              <PlusOutlined />
+            </Button>
+            {fields.map((field, j) => (
+              <Space.Compact key={field.key} block className="mt-4">
+                <Form.Item name={field.name} noStyle>
+                  <Input placeholder={j === 0 ? '10.0.0.1/16' : 'fc00::1/64'} />
+                </Form.Item>
+                <Button size="small" onClick={() => remove(field.name)}>
+                  <MinusOutlined />
+                </Button>
+              </Space.Compact>
+            ))}
+          </Form.Item>
+        )}
+      </Form.List>
+      <Form.List name={['settings', 'dns']}>
+        {(fields, { add, remove }) => (
+          <Form.Item label="DNS">
+            <Button size="small" onClick={() => add('')}>
+              <PlusOutlined />
+            </Button>
+            {fields.map((field, j) => (
+              <Space.Compact key={field.key} block className="mt-4">
+                <Form.Item name={field.name} noStyle>
+                  <Input placeholder={j === 0 ? '1.1.1.1' : '8.8.8.8'} />
+                </Form.Item>
+                <Button size="small" onClick={() => remove(field.name)}>
+                  <MinusOutlined />
+                </Button>
+              </Space.Compact>
+            ))}
+          </Form.Item>
+        )}
+      </Form.List>
+      <Form.Item name={['settings', 'userLevel']} label={t('pages.xray.tun.userLevel')}>
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.List name={['settings', 'autoSystemRoutingTable']}>
+        {(fields, { add, remove }) => (
+          <Form.Item
+            label={
+              <Tooltip title={t('pages.inbounds.form.autoSystemRoutesTooltip')}>
+                {t('pages.inbounds.info.autoSystemRoutes')}
+              </Tooltip>
+            }
+          >
+            <Button size="small" onClick={() => add('')}>
+              <PlusOutlined />
+            </Button>
+            {fields.map((field, j) => (
+              <Space.Compact key={field.key} block className="mt-4">
+                <Form.Item name={field.name} noStyle>
+                  <Input placeholder={j === 0 ? '0.0.0.0/0' : '::/0'} />
+                </Form.Item>
+                <Button size="small" onClick={() => remove(field.name)}>
+                  <MinusOutlined />
+                </Button>
+              </Space.Compact>
+            ))}
+          </Form.Item>
+        )}
+      </Form.List>
+      <Form.Item
+        name={['settings', 'autoOutboundsInterface']}
+        label={
+          <Tooltip title={t('pages.inbounds.form.autoOutboundsInterfaceTooltip')}>
+            {t('pages.inbounds.form.autoOutboundsInterface')}
+          </Tooltip>
+        }
+      >
+        <Input placeholder="auto" />
+      </Form.Item>
+    </>
+  );
+}

+ 37 - 0
frontend/src/pages/inbounds/form/protocols/tunnel.tsx

@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, InputNumber, Select, Switch } from 'antd';
+
+import { HeaderMapEditor } from '@/components/form';
+
+export default function TunnelFields() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['settings', 'rewriteAddress']} label={t('pages.inbounds.form.rewriteAddress')}>
+        <Input />
+      </Form.Item>
+      <Form.Item name={['settings', 'rewritePort']} label={t('pages.inbounds.form.rewritePort')}>
+        <InputNumber min={0} max={65535} />
+      </Form.Item>
+      <Form.Item name={['settings', 'allowedNetwork']} label={t('pages.inbounds.form.allowedNetwork')}>
+        <Select
+          options={[
+            { value: 'tcp,udp', label: 'TCP, UDP' },
+            { value: 'tcp', label: 'TCP' },
+            { value: 'udp', label: 'UDP' },
+          ]}
+        />
+      </Form.Item>
+      <Form.Item label={t('pages.inbounds.portMap')} name={['settings', 'portMap']}>
+        <HeaderMapEditor mode="v1" />
+      </Form.Item>
+      <Form.Item
+        name={['settings', 'followRedirect']}
+        label={t('pages.inbounds.form.followRedirect')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+    </>
+  );
+}

+ 60 - 0
frontend/src/pages/inbounds/form/protocols/vless.tsx

@@ -0,0 +1,60 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Space, Typography } from 'antd';
+
+interface VlessFieldsProps {
+  saving: boolean;
+  selectedVlessAuth: string;
+  network: string;
+  security: string;
+  getNewVlessEnc: (kind: 'x25519' | 'mlkem768') => void;
+  clearVlessEnc: () => void;
+}
+
+export default function VlessFields({
+  saving,
+  selectedVlessAuth,
+  network,
+  security,
+  getNewVlessEnc,
+  clearVlessEnc,
+}: VlessFieldsProps) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['settings', 'decryption']} label={t('pages.inbounds.decryption')}>
+        <Input />
+      </Form.Item>
+      <Form.Item name={['settings', 'encryption']} label={t('pages.inbounds.encryption')}>
+        <Input />
+      </Form.Item>
+      <Form.Item label=" ">
+        <Space size={8} wrap>
+          <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('x25519')}>
+            {t('pages.inbounds.vlessAuthX25519')}
+          </Button>
+          <Button type="primary" loading={saving} onClick={() => getNewVlessEnc('mlkem768')}>
+            {t('pages.inbounds.vlessAuthMlkem768')}
+          </Button>
+          <Button danger onClick={clearVlessEnc}>{t('clear')}</Button>
+        </Space>
+        <Typography.Text type="secondary" className="vless-auth-state">
+          {t('pages.inbounds.vlessAuthSelected', { auth: selectedVlessAuth })}
+        </Typography.Text>
+      </Form.Item>
+      {network === 'tcp' && (security === 'tls' || security === 'reality') && (
+        <Form.Item
+          label={t('pages.inbounds.form.visionTestseed')}
+          extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
+        >
+          <Space.Compact block>
+            {[900, 500, 900, 256].map((def, i) => (
+              <Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
+                <InputNumber min={1} style={{ width: '25%' }} />
+              </Form.Item>
+            ))}
+          </Space.Compact>
+        </Form.Item>
+      )}
+    </>
+  );
+}

+ 120 - 0
frontend/src/pages/inbounds/form/protocols/wireguard.tsx

@@ -0,0 +1,120 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Divider, Form, Input, InputNumber, Space, Switch } from 'antd';
+import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
+
+import { Wireguard } from '@/utils';
+
+interface WireguardFieldsProps {
+  wgPubKey: string;
+  regenInboundWg: () => void;
+  regenWgPeerKeypair: (name: number) => void;
+}
+
+export default function WireguardFields({ wgPubKey, regenInboundWg, regenWgPeerKeypair }: WireguardFieldsProps) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item label={t('pages.xray.wireguard.secretKey')}>
+        <Space.Compact block>
+          <Form.Item name={['settings', 'secretKey']} noStyle>
+            <Input style={{ width: 'calc(100% - 32px)' }} />
+          </Form.Item>
+          <Button icon={<ReloadOutlined />} onClick={regenInboundWg} />
+        </Space.Compact>
+      </Form.Item>
+      <Form.Item label={t('pages.xray.wireguard.publicKey')}>
+        <Input value={wgPubKey} disabled />
+      </Form.Item>
+      <Form.Item name={['settings', 'mtu']} label="MTU">
+        <InputNumber />
+      </Form.Item>
+      <Form.Item
+        name={['settings', 'noKernelTun']}
+        label={t('pages.inbounds.info.noKernelTun')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.List name={['settings', 'peers']}>
+        {(fields, { add, remove }) => (
+          <>
+            <Form.Item label={t('pages.inbounds.form.peers')}>
+              <Button
+                size="small"
+                onClick={() => {
+                  const kp = Wireguard.generateKeypair();
+                  add({
+                    privateKey: kp.privateKey,
+                    publicKey: kp.publicKey,
+                    allowedIPs: ['10.0.0.2/32'],
+                    keepAlive: 0,
+                  });
+                }}
+              >
+                <PlusOutlined /> {t('pages.inbounds.form.addPeer')}
+              </Button>
+            </Form.Item>
+            {fields.map((field, idx) => (
+              <div key={field.key} className="wg-peer">
+                <Divider titlePlacement="center">
+                  <Space>
+                    <span>{t('pages.inbounds.info.peerNumber', { n: idx + 1 })}</span>
+                    {fields.length > 1 && (
+                      <Button
+                        size="small"
+                        danger
+                        icon={<MinusOutlined />}
+                        onClick={() => remove(field.name)}
+                      />
+                    )}
+                  </Space>
+                </Divider>
+                <Form.Item label={t('pages.xray.wireguard.secretKey')}>
+                  <Space.Compact block>
+                    <Form.Item name={[field.name, 'privateKey']} noStyle>
+                      <Input style={{ width: 'calc(100% - 32px)' }} />
+                    </Form.Item>
+                    <Button
+                      icon={<ReloadOutlined />}
+                      onClick={() => regenWgPeerKeypair(field.name)}
+                    />
+                  </Space.Compact>
+                </Form.Item>
+                <Form.Item name={[field.name, 'publicKey']} label={t('pages.xray.wireguard.publicKey')}>
+                  <Input />
+                </Form.Item>
+                <Form.Item name={[field.name, 'preSharedKey']} label="PSK">
+                  <Input />
+                </Form.Item>
+                <Form.List name={[field.name, 'allowedIPs']}>
+                  {(ipFields, { add: addIp, remove: removeIp }) => (
+                    <Form.Item label={t('pages.xray.wireguard.allowedIPs')}>
+                      <Button size="small" onClick={() => addIp('')}>
+                        <PlusOutlined />
+                      </Button>
+                      {ipFields.map((ipField) => (
+                        <Space.Compact key={ipField.key} block className="mt-4">
+                          <Form.Item name={ipField.name} noStyle>
+                            <Input />
+                          </Form.Item>
+                          {ipFields.length > 1 && (
+                            <Button size="small" onClick={() => removeIp(ipField.name)}>
+                              <MinusOutlined />
+                            </Button>
+                          )}
+                        </Space.Compact>
+                      ))}
+                    </Form.Item>
+                  )}
+                </Form.List>
+                <Form.Item name={[field.name, 'keepAlive']} label={t('pages.inbounds.form.keepAlive')}>
+                  <InputNumber min={0} />
+                </Form.Item>
+              </div>
+            ))}
+          </>
+        )}
+      </Form.List>
+    </>
+  );
+}

+ 2 - 0
frontend/src/pages/inbounds/form/security/index.ts

@@ -0,0 +1,2 @@
+export { default as TlsForm } from './tls';
+export { default as RealityForm } from './reality';

+ 143 - 0
frontend/src/pages/inbounds/form/security/reality.tsx

@@ -0,0 +1,143 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
+import { ReloadOutlined } from '@ant-design/icons';
+
+import { UTLS_FINGERPRINT } from '@/schemas/primitives';
+
+interface RealityFormProps {
+  saving: boolean;
+  randomizeRealityTarget: () => void;
+  randomizeShortIds: () => void;
+  genRealityKeypair: () => void;
+  clearRealityKeypair: () => void;
+  genMldsa65: () => void;
+  clearMldsa65: () => void;
+}
+
+export default function RealityForm({
+  saving,
+  randomizeRealityTarget,
+  randomizeShortIds,
+  genRealityKeypair,
+  clearRealityKeypair,
+  genMldsa65,
+  clearMldsa65,
+}: RealityFormProps) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'show']}
+        label={t('pages.inbounds.form.show')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'realitySettings', 'xver']} label={t('pages.inbounds.form.xver')}>
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
+        label="uTLS"
+      >
+        <Select
+          options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
+        />
+      </Form.Item>
+      <Form.Item label={t('pages.inbounds.form.target')}>
+        <Space.Compact block>
+          <Form.Item name={['streamSettings', 'realitySettings', 'target']} noStyle>
+            <Input style={{ width: 'calc(100% - 32px)' }} />
+          </Form.Item>
+          <Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
+        </Space.Compact>
+      </Form.Item>
+      <Form.Item label="SNI">
+        <Space.Compact block style={{ display: 'flex' }}>
+          <Form.Item
+            name={['streamSettings', 'realitySettings', 'serverNames']}
+            noStyle
+          >
+            <Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
+          </Form.Item>
+          <Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
+        </Space.Compact>
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'maxTimediff']}
+        label={t('pages.inbounds.form.maxTimeDiff')}
+      >
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'minClientVer']}
+        label={t('pages.inbounds.form.minClientVer')}
+      >
+        <Input placeholder="25.9.11" />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'maxClientVer']}
+        label={t('pages.inbounds.form.maxClientVer')}
+      >
+        <Input placeholder="25.9.11" />
+      </Form.Item>
+      <Form.Item label={t('pages.inbounds.form.shortIds')}>
+        <Space.Compact block style={{ display: 'flex' }}>
+          <Form.Item
+            name={['streamSettings', 'realitySettings', 'shortIds']}
+            noStyle
+          >
+            <Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
+          </Form.Item>
+          <Button icon={<ReloadOutlined />} onClick={randomizeShortIds} />
+        </Space.Compact>
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
+        label={t('pages.inbounds.form.spiderX')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
+        label={t('pages.inbounds.publicKey')}
+      >
+        <Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'privateKey']}
+        label={t('pages.inbounds.privatekey')}
+      >
+        <Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
+      </Form.Item>
+      <Form.Item label=" ">
+        <Space>
+          <Button type="primary" loading={saving} onClick={genRealityKeypair}>
+            {t('pages.inbounds.form.getNewCert')}
+          </Button>
+          <Button danger onClick={clearRealityKeypair}>{t('clear')}</Button>
+        </Space>
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
+        label={t('pages.inbounds.form.mldsa65Seed')}
+      >
+        <Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
+        label={t('pages.inbounds.form.mldsa65Verify')}
+      >
+        <Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
+      </Form.Item>
+      <Form.Item label=" ">
+        <Space>
+          <Button type="primary" loading={saving} onClick={genMldsa65}>
+            {t('pages.inbounds.form.getNewSeed')}
+          </Button>
+          <Button danger onClick={clearMldsa65}>{t('clear')}</Button>
+        </Space>
+      </Form.Item>
+    </>
+  );
+}

+ 309 - 0
frontend/src/pages/inbounds/form/security/tls.tsx

@@ -0,0 +1,309 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, Radio, Select, Space, Switch } from 'antd';
+import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
+
+import {
+  ALPN_OPTION,
+  TLS_CIPHER_OPTION,
+  TLS_VERSION_OPTION,
+  USAGE_OPTION,
+  UTLS_FINGERPRINT,
+} from '@/schemas/primitives';
+
+const { TextArea } = Input;
+
+interface TlsFormProps {
+  saving: boolean;
+  setCertFromPanel: (certName: number) => void;
+  clearCertFiles: (certName: number) => void;
+  generateRandomPinHash: () => void;
+  getNewEchCert: () => void;
+  clearEchCert: () => void;
+}
+
+export default function TlsForm({
+  saving,
+  setCertFromPanel,
+  clearCertFiles,
+  generateRandomPinHash,
+  getNewEchCert,
+  clearEchCert,
+}: TlsFormProps) {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['streamSettings', 'tlsSettings', 'serverName']} label="SNI">
+        <Input placeholder={t('pages.inbounds.form.serverNameIndication')} />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label={t('pages.inbounds.form.cipherSuites')}>
+        <Select
+          options={[
+            { value: '', label: t('pages.inbounds.form.autoOption') },
+            ...Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ({ value: v, label: k })),
+          ]}
+        />
+      </Form.Item>
+      <Form.Item label={t('pages.inbounds.form.minMaxVersion')}>
+        <Space.Compact block>
+          <Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
+            <Select
+              style={{ width: '50%' }}
+              options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
+            />
+          </Form.Item>
+          <Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
+            <Select
+              style={{ width: '50%' }}
+              options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
+            />
+          </Form.Item>
+        </Space.Compact>
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
+        label="uTLS"
+      >
+        <Select
+          options={[
+            { value: '', label: 'None' },
+            ...Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp })),
+          ]}
+        />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
+        <Select
+          mode="multiple"
+          tokenSeparators={[',']}
+          style={{ width: '100%' }}
+          options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
+        />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
+        label={t('pages.inbounds.form.rejectUnknownSni')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'disableSystemRoot']}
+        label={t('pages.inbounds.form.disableSystemRoot')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'enableSessionResumption']}
+        label={t('pages.inbounds.form.sessionResumption')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+
+      <Form.List name={['streamSettings', 'tlsSettings', 'certificates']}>
+        {(certFields, { add, remove }) => (
+          <>
+            <Form.Item label={t('certificate')}>
+              <Button
+                type="primary"
+                size="small"
+                onClick={() => add({
+                  useFile: true,
+                  certificateFile: '',
+                  keyFile: '',
+                  certificate: [],
+                  key: [],
+                  oneTimeLoading: false,
+                  usage: 'encipherment',
+                  buildChain: false,
+                })}
+              >
+                <PlusOutlined />
+              </Button>
+            </Form.Item>
+            {certFields.map((certField, idx) => (
+              <div key={certField.key}>
+                <Form.Item
+                  name={[certField.name, 'useFile']}
+                  label={`${t('certificate')} ${idx + 1}`}
+                >
+                  <Radio.Group buttonStyle="solid">
+                    <Radio.Button value={true}>
+                      {t('pages.inbounds.certificatePath')}
+                    </Radio.Button>
+                    <Radio.Button value={false}>
+                      {t('pages.inbounds.certificateContent')}
+                    </Radio.Button>
+                  </Radio.Group>
+                </Form.Item>
+                {certFields.length > 1 && (
+                  <Form.Item label=" ">
+                    <Button
+                      size="small"
+                      danger
+                      onClick={() => remove(certField.name)}
+                    >
+                      <MinusOutlined /> {t('remove')}
+                    </Button>
+                  </Form.Item>
+                )}
+                <Form.Item
+                  noStyle
+                  shouldUpdate={(prev, curr) =>
+                    prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
+                    !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
+                  }
+                >
+                  {({ getFieldValue }) => {
+                    const useFile = getFieldValue([
+                      'streamSettings', 'tlsSettings', 'certificates',
+                      certField.name, 'useFile',
+                    ]);
+                    return useFile ? (
+                      <>
+                        <Form.Item
+                          name={[certField.name, 'certificateFile']}
+                          label={t('pages.inbounds.publicKey')}
+                        >
+                          <Input />
+                        </Form.Item>
+                        <Form.Item
+                          name={[certField.name, 'keyFile']}
+                          label={t('pages.inbounds.privatekey')}
+                        >
+                          <Input />
+                        </Form.Item>
+                        <Form.Item label=" ">
+                          <Space>
+                            <Button
+                              type="primary"
+                              loading={saving}
+                              onClick={() => setCertFromPanel(certField.name)}
+                            >
+                              {t('pages.inbounds.setDefaultCert')}
+                            </Button>
+                            <Button danger onClick={() => clearCertFiles(certField.name)}>
+                              {t('clear')}
+                            </Button>
+                          </Space>
+                        </Form.Item>
+                      </>
+                    ) : (
+                      <>
+                        <Form.Item
+                          name={[certField.name, 'certificate']}
+                          label={t('pages.inbounds.publicKey')}
+                          normalize={(v) => typeof v === 'string'
+                            ? v.split('\n')
+                            : v}
+                          getValueProps={(v) => ({
+                            value: Array.isArray(v) ? v.join('\n') : v,
+                          })}
+                        >
+                          <TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
+                        </Form.Item>
+                        <Form.Item
+                          name={[certField.name, 'key']}
+                          label={t('pages.inbounds.privatekey')}
+                          normalize={(v) => typeof v === 'string'
+                            ? v.split('\n')
+                            : v}
+                          getValueProps={(v) => ({
+                            value: Array.isArray(v) ? v.join('\n') : v,
+                          })}
+                        >
+                          <TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
+                        </Form.Item>
+                      </>
+                    );
+                  }}
+                </Form.Item>
+                <Form.Item
+                  name={[certField.name, 'oneTimeLoading']}
+                  label={t('pages.inbounds.form.oneTimeLoading')}
+                  valuePropName="checked"
+                >
+                  <Switch />
+                </Form.Item>
+                <Form.Item
+                  name={[certField.name, 'usage']}
+                  label={t('pages.inbounds.form.usageOption')}
+                >
+                  <Select
+                    style={{ width: '50%' }}
+                    options={Object.values(USAGE_OPTION).map((u) => ({ value: u, label: u }))}
+                  />
+                </Form.Item>
+                <Form.Item
+                  noStyle
+                  shouldUpdate={(prev, curr) =>
+                    prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
+                    !== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
+                  }
+                >
+                  {({ getFieldValue }) => {
+                    const usage = getFieldValue([
+                      'streamSettings', 'tlsSettings', 'certificates',
+                      certField.name, 'usage',
+                    ]);
+                    if (usage !== 'issue') return null;
+                    return (
+                      <Form.Item
+                        name={[certField.name, 'buildChain']}
+                        label={t('pages.inbounds.form.buildChain')}
+                        valuePropName="checked"
+                      >
+                        <Switch />
+                      </Form.Item>
+                    );
+                  }}
+                </Form.Item>
+              </div>
+            ))}
+          </>
+        )}
+      </Form.List>
+
+      <Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label={t('pages.inbounds.form.echKey')}>
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
+        label={t('pages.inbounds.form.echConfig')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        label={t('pages.inbounds.form.pinnedPeerCertSha256')}
+        tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
+      >
+        <Space.Compact block>
+          <Form.Item
+            name={['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256']}
+            noStyle
+          >
+            <Select
+              mode="tags"
+              tokenSeparators={[',', ' ']}
+              placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
+              style={{ width: 'calc(100% - 32px)' }}
+            />
+          </Form.Item>
+          <Button
+            icon={<ReloadOutlined />}
+            onClick={generateRandomPinHash}
+            title={t('pages.inbounds.form.generateRandomPin')}
+          />
+        </Space.Compact>
+      </Form.Item>
+      <Form.Item label=" ">
+        <Space>
+          <Button type="primary" loading={saving} onClick={getNewEchCert}>
+            {t('pages.inbounds.form.getNewEchCert')}
+          </Button>
+          <Button danger onClick={clearEchCert}>{t('clear')}</Button>
+        </Space>
+      </Form.Item>
+    </>
+  );
+}

+ 136 - 0
frontend/src/pages/inbounds/form/transport/external-proxy.tsx

@@ -0,0 +1,136 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
+import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
+
+import { InputAddon } from '@/components/ui';
+import { ALPN_OPTION, UTLS_FINGERPRINT } from '@/schemas/primitives';
+
+export default function ExternalProxyForm({
+  toggleExternalProxy,
+}: {
+  toggleExternalProxy: (on: boolean) => void;
+}) {
+  const { t } = useTranslation();
+  return (
+    <Form.Item
+      noStyle
+      shouldUpdate={(prev, curr) => {
+        const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
+        const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
+        return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
+      }}
+    >
+      {({ getFieldValue }) => {
+        const arr = getFieldValue(['streamSettings', 'externalProxy']);
+        const on = Array.isArray(arr) && arr.length > 0;
+        return (
+          <>
+            <Form.Item label={t('pages.inbounds.form.externalProxy')}>
+              <Switch checked={on} onChange={toggleExternalProxy} />
+            </Form.Item>
+            {on && (
+              <Form.List name={['streamSettings', 'externalProxy']}>
+                {(fields, { add, remove }) => (
+                  <>
+                    <Form.Item label=" " colon={false}>
+                      <Button
+                        size="small"
+                        type="primary"
+                        onClick={() => add({
+                          forceTls: 'same',
+                          dest: '',
+                          port: 443,
+                          remark: '',
+                          sni: '',
+                          fingerprint: '',
+                          alpn: [],
+                        })}
+                      >
+                        <PlusOutlined />
+                      </Button>
+                    </Form.Item>
+                    <Form.Item wrapperCol={{ span: 24 }}>
+                      {fields.map((field) => (
+                        <div key={field.key} style={{ margin: '8px 0' }}>
+                          <Space.Compact block>
+                            <Form.Item name={[field.name, 'forceTls']} noStyle>
+                              <Select
+                                style={{ width: '20%' }}
+                                options={[
+                                  { value: 'same', label: t('pages.inbounds.same') },
+                                  { value: 'none', label: t('none') },
+                                  { value: 'tls', label: 'TLS' },
+                                ]}
+                              />
+                            </Form.Item>
+                            <Form.Item name={[field.name, 'dest']} noStyle>
+                              <Input style={{ width: '30%' }} placeholder={t('host')} />
+                            </Form.Item>
+                            <Form.Item name={[field.name, 'port']} noStyle>
+                              <InputNumber style={{ width: '15%' }} min={1} max={65535} />
+                            </Form.Item>
+                            <Form.Item name={[field.name, 'remark']} noStyle>
+                              <Input style={{ width: '25%' }} placeholder={t('pages.inbounds.remark')} />
+                            </Form.Item>
+                            <InputAddon onClick={() => remove(field.name)}>
+                              <MinusOutlined />
+                            </InputAddon>
+                          </Space.Compact>
+                          <Form.Item
+                            noStyle
+                            shouldUpdate={(prev, curr) =>
+                              prev.streamSettings?.externalProxy?.[field.name]?.forceTls
+                              !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
+                            }
+                          >
+                            {({ getFieldValue }) => {
+                              const ft = getFieldValue([
+                                'streamSettings', 'externalProxy', field.name, 'forceTls',
+                              ]);
+                              if (ft !== 'tls') return null;
+                              return (
+                                <Space.Compact style={{ marginTop: 6 }} block>
+                                  <Form.Item name={[field.name, 'sni']} noStyle>
+                                    <Input style={{ width: '30%' }} placeholder={t('pages.inbounds.form.sniPlaceholder')} />
+                                  </Form.Item>
+                                  <Form.Item name={[field.name, 'fingerprint']} noStyle>
+                                    <Select
+                                      style={{ width: '30%' }}
+                                      placeholder={t('pages.inbounds.form.fingerprint')}
+                                      options={[
+                                        { value: '', label: t('pages.inbounds.form.defaultOption') },
+                                        ...Object.values(UTLS_FINGERPRINT).map((fp) => ({
+                                          value: fp,
+                                          label: fp,
+                                        })),
+                                      ]}
+                                    />
+                                  </Form.Item>
+                                  <Form.Item name={[field.name, 'alpn']} noStyle>
+                                    <Select
+                                      mode="multiple"
+                                      style={{ width: '40%' }}
+                                      placeholder="ALPN"
+                                      options={Object.values(ALPN_OPTION).map((a) => ({
+                                        value: a,
+                                        label: a,
+                                      }))}
+                                    />
+                                  </Form.Item>
+                                </Space.Compact>
+                              );
+                            }}
+                          </Form.Item>
+                        </div>
+                      ))}
+                    </Form.Item>
+                  </>
+                )}
+              </Form.List>
+            )}
+          </>
+        );
+      }}
+    </Form.Item>
+  );
+}

+ 29 - 0
frontend/src/pages/inbounds/form/transport/grpc.tsx

@@ -0,0 +1,29 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, Switch } from 'antd';
+
+export default function GrpcForm() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item
+        name={['streamSettings', 'grpcSettings', 'serviceName']}
+        label={t('pages.inbounds.form.serviceName')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'grpcSettings', 'authority']}
+        label={t('pages.inbounds.form.authority')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'grpcSettings', 'multiMode']}
+        label={t('pages.inbounds.form.multiMode')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+    </>
+  );
+}

+ 37 - 0
frontend/src/pages/inbounds/form/transport/httpupgrade.tsx

@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, Switch } from 'antd';
+
+import { HeaderMapEditor } from '@/components/form';
+
+export default function HttpUpgradeForm() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item
+        name={['streamSettings', 'httpupgradeSettings', 'acceptProxyProtocol']}
+        label={t('pages.inbounds.form.proxyProtocol')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'httpupgradeSettings', 'host']}
+        label={t('host')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'httpupgradeSettings', 'path']}
+        label={t('path')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        label={t('pages.inbounds.form.headers')}
+        name={['streamSettings', 'httpupgradeSettings', 'headers']}
+      >
+        <HeaderMapEditor mode="v1" />
+      </Form.Item>
+    </>
+  );
+}

+ 8 - 0
frontend/src/pages/inbounds/form/transport/index.ts

@@ -0,0 +1,8 @@
+export { default as RawForm } from './raw';
+export { default as WsForm } from './ws';
+export { default as GrpcForm } from './grpc';
+export { default as XhttpForm } from './xhttp';
+export { default as HttpUpgradeForm } from './httpupgrade';
+export { default as KcpForm } from './kcp';
+export { default as ExternalProxyForm } from './external-proxy';
+export { default as SockoptForm } from './sockopt';

+ 34 - 0
frontend/src/pages/inbounds/form/transport/kcp.tsx

@@ -0,0 +1,34 @@
+import { useTranslation } from 'react-i18next';
+import { Form, InputNumber } from 'antd';
+
+export default function KcpForm() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item name={['streamSettings', 'kcpSettings', 'mtu']} label="MTU">
+        <InputNumber min={576} max={1460} />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'kcpSettings', 'tti']} label={t('pages.inbounds.form.ttiMs')}>
+        <InputNumber min={10} max={100} />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'kcpSettings', 'uplinkCapacity']} label={t('pages.inbounds.form.uplinkMbps')}>
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'kcpSettings', 'downlinkCapacity']} label={t('pages.inbounds.form.downlinkMbps')}>
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
+        label={t('pages.inbounds.form.cwndMultiplier')}
+      >
+        <InputNumber min={1} />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
+        label={t('pages.inbounds.form.maxSendingWindow')}
+      >
+        <InputNumber min={0} />
+      </Form.Item>
+    </>
+  );
+}

+ 164 - 0
frontend/src/pages/inbounds/form/transport/raw.tsx

@@ -0,0 +1,164 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, Switch } from 'antd';
+
+import { HeaderMapEditor } from '@/components/form';
+
+export default function RawForm() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item
+        name={['streamSettings', 'tcpSettings', 'acceptProxyProtocol']}
+        label={t('pages.inbounds.form.proxyProtocol')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.Item label={`HTTP ${t('camouflage')}`}>
+        <Form.Item
+          noStyle
+          shouldUpdate={(prev, curr) =>
+            prev.streamSettings?.tcpSettings?.header?.type
+            !== curr.streamSettings?.tcpSettings?.header?.type
+          }
+        >
+          {({ getFieldValue, setFieldValue }) => {
+            const headerType = getFieldValue(
+              ['streamSettings', 'tcpSettings', 'header', 'type'],
+            ) as string | undefined;
+            return (
+              <Switch
+                checked={headerType === 'http'}
+                onChange={(v) => {
+                  setFieldValue(
+                    ['streamSettings', 'tcpSettings', 'header'],
+                    v
+                      ? {
+                        type: 'http',
+                        request: {
+                          version: '1.1',
+                          method: 'GET',
+                          path: ['/'],
+                          headers: {},
+                        },
+                        response: {
+                          version: '1.1',
+                          status: '200',
+                          reason: 'OK',
+                          headers: {},
+                        },
+                      }
+                      : { type: 'none' },
+                  );
+                }}
+              />
+            );
+          }}
+        </Form.Item>
+      </Form.Item>
+      {/* Per Xray docs (transports/raw.html#httpheaderobject), the
+          `request` object is honored only by outbound proxies; the
+          inbound listener reads `response`. Showing Host / Path /
+          Method / Version / request-headers on the inbound side was
+          a regression from this modal's earlier iteration — those
+          inputs wrote to the wire but xray-core ignored them. The
+          inbound modal now only exposes the response side. */}
+      <Form.Item
+        noStyle
+        shouldUpdate={(prev, curr) =>
+          prev.streamSettings?.tcpSettings?.header?.type
+          !== curr.streamSettings?.tcpSettings?.header?.type
+        }
+      >
+        {({ getFieldValue }) => {
+          const headerType = getFieldValue(
+            ['streamSettings', 'tcpSettings', 'header', 'type'],
+          ) as string | undefined;
+          if (headerType !== 'http') return null;
+          return (
+            <>
+              <Form.Item
+                label={t('pages.inbounds.form.requestVersion')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'request', 'version',
+                ]}
+              >
+                <Input placeholder="1.1" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.requestMethod')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'request', 'method',
+                ]}
+              >
+                <Input placeholder="GET" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.requestPath')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'request', 'path',
+                ]}
+                getValueProps={(v) => ({ value: Array.isArray(v) ? v.join(',') : v })}
+                getValueFromEvent={(e) => {
+                  const raw = (e?.target?.value ?? '') as string;
+                  const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
+                  return parts.length > 0 ? parts : ['/'];
+                }}
+              >
+                <Input placeholder="/" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.requestHeaders')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'request', 'headers',
+                ]}
+              >
+                <HeaderMapEditor mode="v2" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.responseVersion')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'response', 'version',
+                ]}
+              >
+                <Input placeholder="1.1" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.responseStatus')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'response', 'status',
+                ]}
+              >
+                <Input placeholder="200" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.responseReason')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'response', 'reason',
+                ]}
+              >
+                <Input placeholder="OK" />
+              </Form.Item>
+              <Form.Item
+                label={t('pages.inbounds.form.responseHeaders')}
+                name={[
+                  'streamSettings', 'tcpSettings', 'header',
+                  'response', 'headers',
+                ]}
+              >
+                <HeaderMapEditor mode="v2" />
+              </Form.Item>
+            </>
+          );
+        }}
+      </Form.Item>
+    </>
+  );
+}

+ 270 - 0
frontend/src/pages/inbounds/form/transport/sockopt.tsx

@@ -0,0 +1,270 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
+
+import {
+  Address_Port_Strategy,
+  DOMAIN_STRATEGY_OPTION,
+  TCP_CONGESTION_OPTION,
+} from '@/schemas/primitives';
+import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
+
+export default function SockoptForm({
+  toggleSockopt,
+}: {
+  toggleSockopt: (on: boolean) => void;
+}) {
+  const { t } = useTranslation();
+  return (
+    <Form.Item
+      noStyle
+      shouldUpdate={(prev, curr) => {
+        const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
+        const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
+        return !!a !== !!b;
+      }}
+    >
+      {({ getFieldValue }) => {
+        const sock = getFieldValue(['streamSettings', 'sockopt']);
+        const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
+        return (
+          <>
+            <Form.Item label="Sockopt">
+              <Switch checked={on} onChange={toggleSockopt} />
+            </Form.Item>
+            {on && (
+              <>
+                <Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
+                  label={t('pages.inbounds.form.tcpKeepAliveInterval')}
+                >
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
+                  label={t('pages.inbounds.form.tcpKeepAliveIdle')}
+                >
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label={t('pages.inbounds.form.tcpMaxSeg')}>
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
+                  label={t('pages.inbounds.form.tcpUserTimeout')}
+                >
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
+                  label={t('pages.inbounds.form.tcpWindowClamp')}
+                >
+                  <InputNumber min={0} />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
+                  label={t('pages.inbounds.form.proxyProtocol')}
+                  valuePropName="checked"
+                >
+                  <Switch />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpFastOpen']}
+                  label={t('pages.inbounds.form.tcpFastOpen')}
+                  valuePropName="checked"
+                >
+                  <Switch />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpMptcp']}
+                  label={t('pages.inbounds.form.multipathTcp')}
+                  valuePropName="checked"
+                >
+                  <Switch />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'penetrate']}
+                  label={t('pages.inbounds.form.penetrate')}
+                  valuePropName="checked"
+                >
+                  <Switch />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'V6Only']}
+                  label={t('pages.inbounds.form.v6Only')}
+                  valuePropName="checked"
+                >
+                  <Switch />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'domainStrategy']}
+                  label={t('pages.xray.wireguard.domainStrategy')}
+                >
+                  <Select
+                    style={{ width: '50%' }}
+                    options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
+                  />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'tcpcongestion']}
+                  label={t('pages.inbounds.form.tcpCongestion')}
+                >
+                  <Select
+                    style={{ width: '50%' }}
+                    options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
+                  />
+                </Form.Item>
+                <Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
+                  <Select
+                    style={{ width: '50%' }}
+                    options={[
+                      { value: 'off', label: 'Off' },
+                      { value: 'redirect', label: 'Redirect' },
+                      { value: 'tproxy', label: 'TProxy' },
+                    ]}
+                  />
+                </Form.Item>
+                <Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label={t('pages.inbounds.form.dialerProxy')}>
+                  <Input />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'interfaceName']}
+                  label={t('pages.inbounds.info.interfaceName')}
+                >
+                  <Input />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
+                  label={t('pages.inbounds.form.trustedXForwardedFor')}
+                >
+                  <Select
+                    mode="tags"
+                    style={{ width: '100%' }}
+                    tokenSeparators={[',']}
+                    options={[
+                      { value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
+                      { value: 'X-Real-IP', label: 'X-Real-IP' },
+                      { value: 'True-Client-IP', label: 'True-Client-IP' },
+                      { value: 'X-Client-IP', label: 'X-Client-IP' },
+                    ]}
+                  />
+                </Form.Item>
+                <Form.Item
+                  name={['streamSettings', 'sockopt', 'addressPortStrategy']}
+                  label={t('pages.inbounds.form.addressPortStrategy')}
+                >
+                  <Select
+                    style={{ width: '50%' }}
+                    options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
+                  />
+                </Form.Item>
+                <Form.Item shouldUpdate noStyle>
+                  {({ getFieldValue, setFieldValue }) => {
+                    const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
+                    const hasHe = he != null;
+                    return (
+                      <>
+                        <Form.Item label="Happy Eyeballs">
+                          <Switch
+                            checked={hasHe}
+                            onChange={(v) => {
+                              setFieldValue(
+                                ['streamSettings', 'sockopt', 'happyEyeballs'],
+                                v ? HappyEyeballsSchema.parse({}) : undefined,
+                              );
+                            }}
+                          />
+                        </Form.Item>
+                        {hasHe && (
+                          <>
+                            <Form.Item
+                              name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
+                              label={t('pages.inbounds.form.tryDelayMs')}
+                            >
+                              <InputNumber min={0} placeholder="0 disabled — 250 recommended" />
+                            </Form.Item>
+                            <Form.Item
+                              name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
+                              label={t('pages.inbounds.form.prioritizeIPv6')}
+                              valuePropName="checked"
+                            >
+                              <Switch />
+                            </Form.Item>
+                            <Form.Item
+                              name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
+                              label={t('pages.inbounds.form.interleave')}
+                            >
+                              <InputNumber min={1} />
+                            </Form.Item>
+                            <Form.Item
+                              name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
+                              label={t('pages.inbounds.form.maxConcurrentTry')}
+                            >
+                              <InputNumber min={0} />
+                            </Form.Item>
+                          </>
+                        )}
+                      </>
+                    );
+                  }}
+                </Form.Item>
+                <Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
+                  {(fields, { add, remove }) => (
+                    <>
+                      <Form.Item label={t('pages.inbounds.form.customSockopt')}>
+                        <Button
+                          type="dashed"
+                          size="small"
+                          onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
+                        >
+                          + {t('pages.inbounds.form.addCustomOption')}
+                        </Button>
+                      </Form.Item>
+                      {fields.map((field) => (
+                        <Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
+                          <Form.Item name={[field.name, 'system']} noStyle>
+                            <Select
+                              placeholder="all"
+                              allowClear
+                              style={{ width: 100 }}
+                              options={[
+                                { value: 'linux', label: 'linux' },
+                                { value: 'windows', label: 'windows' },
+                                { value: 'darwin', label: 'darwin' },
+                              ]}
+                            />
+                          </Form.Item>
+                          <Form.Item name={[field.name, 'type']} noStyle>
+                            <Select
+                              style={{ width: 80 }}
+                              options={[
+                                { value: 'int', label: 'int' },
+                                { value: 'str', label: 'str' },
+                              ]}
+                            />
+                          </Form.Item>
+                          <Form.Item name={[field.name, 'level']} noStyle>
+                            <Input placeholder="level (6=TCP)" style={{ width: 100 }} />
+                          </Form.Item>
+                          <Form.Item name={[field.name, 'opt']} noStyle>
+                            <Input placeholder="opt" style={{ width: 120 }} />
+                          </Form.Item>
+                          <Form.Item name={[field.name, 'value']} noStyle>
+                            <Input placeholder="value" style={{ flex: 1 }} />
+                          </Form.Item>
+                          <Button danger onClick={() => remove(field.name)}>−</Button>
+                        </Space.Compact>
+                      ))}
+                    </>
+                  )}
+                </Form.List>
+              </>
+            )}
+          </>
+        );
+      }}
+    </Form.Item>
+  );
+}

+ 37 - 0
frontend/src/pages/inbounds/form/transport/ws.tsx

@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, InputNumber, Switch } from 'antd';
+
+import { HeaderMapEditor } from '@/components/form';
+
+export default function WsForm() {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Form.Item
+        name={['streamSettings', 'wsSettings', 'acceptProxyProtocol']}
+        label={t('pages.inbounds.form.proxyProtocol')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'wsSettings', 'host']} label={t('host')}>
+        <Input />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'wsSettings', 'path']} label={t('path')}>
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
+        label={t('pages.inbounds.form.heartbeatPeriod')}
+      >
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.Item
+        label={t('pages.inbounds.form.headers')}
+        name={['streamSettings', 'wsSettings', 'headers']}
+      >
+        <HeaderMapEditor mode="v1" />
+      </Form.Item>
+    </>
+  );
+}

+ 218 - 0
frontend/src/pages/inbounds/form/transport/xhttp.tsx

@@ -0,0 +1,218 @@
+import { useTranslation } from 'react-i18next';
+import { Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd';
+
+import { HeaderMapEditor } from '@/components/form';
+import type { InboundFormValues } from '@/schemas/forms/inbound-form';
+
+export default function XhttpForm({ form }: { form: FormInstance<InboundFormValues> }) {
+  const { t } = useTranslation();
+  const xhttpMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'mode'], form);
+  const xhttpObfsMode = Form.useWatch(['streamSettings', 'xhttpSettings', 'xPaddingObfsMode'], form) ?? false;
+  const xhttpSessionPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'sessionPlacement'], form);
+  const xhttpSeqPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'seqPlacement'], form);
+  const xhttpUplinkPlacement = Form.useWatch(['streamSettings', 'xhttpSettings', 'uplinkDataPlacement'], form);
+  return (
+    <>
+      <Form.Item name={['streamSettings', 'xhttpSettings', 'host']} label={t('host')}>
+        <Input />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'xhttpSettings', 'path']} label={t('path')}>
+        <Input />
+      </Form.Item>
+      <Form.Item name={['streamSettings', 'xhttpSettings', 'mode']} label={t('pages.inbounds.info.mode')}>
+        <Select
+          style={{ width: '50%' }}
+          options={(['auto', 'packet-up', 'stream-up', 'stream-one'] as const).map((m) => ({
+            value: m,
+            label: m,
+          }))}
+        />
+      </Form.Item>
+      {xhttpMode === 'packet-up' && (
+        <>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'scMaxBufferedPosts']}
+            label={t('pages.inbounds.form.maxBufferedUpload')}
+          >
+            <InputNumber />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
+            label={t('pages.inbounds.form.maxUploadSize')}
+          >
+            <Input />
+          </Form.Item>
+        </>
+      )}
+      {xhttpMode === 'stream-up' && (
+        <Form.Item
+          name={['streamSettings', 'xhttpSettings', 'scStreamUpServerSecs']}
+          label={t('pages.inbounds.form.streamUpServer')}
+        >
+          <Input />
+        </Form.Item>
+      )}
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'serverMaxHeaderBytes']}
+        label={t('pages.inbounds.form.serverMaxHeaderBytes')}
+      >
+        <InputNumber min={0} placeholder="0 (default)" />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
+        label={t('pages.inbounds.form.paddingBytes')}
+      >
+        <Input />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'headers']}
+        label={t('pages.inbounds.form.headers')}
+      >
+        <HeaderMapEditor mode="v1" />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
+        label={t('pages.inbounds.form.uplinkHttpMethod')}
+      >
+        <Select
+          options={[
+            { value: '', label: 'Default (POST)' },
+            { value: 'POST', label: 'POST' },
+            { value: 'PUT', label: 'PUT' },
+            {
+              value: 'GET',
+              label: 'GET (packet-up only)',
+              disabled: xhttpMode !== 'packet-up',
+            },
+          ]}
+        />
+      </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
+        label={t('pages.inbounds.form.paddingObfsMode')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+      {xhttpObfsMode && (
+        <>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
+            label={t('pages.inbounds.form.paddingKey')}
+          >
+            <Input placeholder="x_padding" />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
+            label={t('pages.inbounds.form.paddingHeader')}
+          >
+            <Input placeholder="X-Padding" />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
+            label={t('pages.inbounds.form.paddingPlacement')}
+          >
+            <Select
+              options={[
+                { value: '', label: 'Default (queryInHeader)' },
+                { value: 'queryInHeader', label: 'queryInHeader' },
+                { value: 'header', label: 'header' },
+                { value: 'cookie', label: 'cookie' },
+                { value: 'query', label: 'query' },
+              ]}
+            />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
+            label={t('pages.inbounds.form.paddingMethod')}
+          >
+            <Select
+              options={[
+                { value: '', label: 'Default (repeat-x)' },
+                { value: 'repeat-x', label: 'repeat-x' },
+                { value: 'tokenish', label: 'tokenish' },
+              ]}
+            />
+          </Form.Item>
+        </>
+      )}
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
+        label={t('pages.inbounds.form.sessionPlacement')}
+      >
+        <Select
+          options={[
+            { value: '', label: 'Default (path)' },
+            { value: 'path', label: 'path' },
+            { value: 'header', label: 'header' },
+            { value: 'cookie', label: 'cookie' },
+            { value: 'query', label: 'query' },
+          ]}
+        />
+      </Form.Item>
+      {xhttpSessionPlacement && xhttpSessionPlacement !== 'path' && (
+        <Form.Item
+          name={['streamSettings', 'xhttpSettings', 'sessionKey']}
+          label={t('pages.inbounds.form.sessionKey')}
+        >
+          <Input placeholder="x_session" />
+        </Form.Item>
+      )}
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
+        label={t('pages.inbounds.form.sequencePlacement')}
+      >
+        <Select
+          options={[
+            { value: '', label: 'Default (path)' },
+            { value: 'path', label: 'path' },
+            { value: 'header', label: 'header' },
+            { value: 'cookie', label: 'cookie' },
+            { value: 'query', label: 'query' },
+          ]}
+        />
+      </Form.Item>
+      {xhttpSeqPlacement && xhttpSeqPlacement !== 'path' && (
+        <Form.Item
+          name={['streamSettings', 'xhttpSettings', 'seqKey']}
+          label={t('pages.inbounds.form.sequenceKey')}
+        >
+          <Input placeholder="x_seq" />
+        </Form.Item>
+      )}
+      {xhttpMode === 'packet-up' && (
+        <>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
+            label={t('pages.inbounds.form.uplinkDataPlacement')}
+          >
+            <Select
+              options={[
+                { value: '', label: 'Default (body)' },
+                { value: 'body', label: 'body' },
+                { value: 'header', label: 'header' },
+                { value: 'cookie', label: 'cookie' },
+                { value: 'query', label: 'query' },
+              ]}
+            />
+          </Form.Item>
+          {xhttpUplinkPlacement && xhttpUplinkPlacement !== 'body' && (
+            <Form.Item
+              name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
+              label={t('pages.inbounds.form.uplinkDataKey')}
+            >
+              <Input placeholder="x_data" />
+            </Form.Item>
+          )}
+        </>
+      )}
+      <Form.Item
+        name={['streamSettings', 'xhttpSettings', 'noSSEHeader']}
+        label={t('pages.inbounds.form.noSseHeader')}
+        valuePropName="checked"
+      >
+        <Switch />
+      </Form.Item>
+    </>
+  );
+}

+ 187 - 0
frontend/src/pages/inbounds/form/useInboundFallbacks.ts

@@ -0,0 +1,187 @@
+import { useRef, useState } from 'react';
+
+import { HttpUtil } from '@/utils';
+import type { FallbackRow } from '@/schemas/forms/inbound-form';
+import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
+
+// Fallback rows for VLESS/Trojan TLS inbounds: state + the load/save/derive
+// and add/update/remove/move handlers, plus the eligible-child option list.
+// Lifted out of InboundFormModal so the modal body stays focused on layout.
+export function useInboundFallbacks(dbInbound: DBInbound | null, dbInbounds: DBInbound[]) {
+  const fallbackKeyRef = useRef(0);
+  const [fallbacks, setFallbacks] = useState<FallbackRow[]>([]);
+
+  const fallbackChildOptions = (dbInbounds || [])
+    .filter((ib) => ib.id !== dbInbound?.id)
+    .map((ib) => ({
+      label: `${ib.remark || `#${ib.id}`} · ${ib.protocol}:${ib.port}`,
+      value: ib.id,
+    }));
+
+  const loadFallbacks = async (masterId: number | null) => {
+    if (!masterId) {
+      setFallbacks([]);
+      return;
+    }
+    const msg = await HttpUtil.get(`/panel/api/inbounds/${masterId}/fallbacks`);
+    if (!msg?.success || !Array.isArray(msg.obj)) {
+      setFallbacks([]);
+      return;
+    }
+    setFallbacks(
+      (msg.obj as {
+        childId: number;
+        name?: string;
+        alpn?: string;
+        path?: string;
+        dest?: string;
+        xver?: number;
+      }[])
+        .map((r) => ({
+          rowKey: `fb-${++fallbackKeyRef.current}`,
+          childId: r.childId,
+          name: r.name || '',
+          alpn: r.alpn || '',
+          path: r.path || '',
+          dest: r.dest || '',
+          xver: r.xver || 0,
+        })),
+    );
+  };
+
+  const saveFallbacks = async (masterId: number) => {
+    if (!masterId) return true;
+    const payload = {
+      fallbacks: fallbacks.filter((c) => c.childId).map((c, i) => ({
+        childId: c.childId,
+        name: c.name,
+        alpn: c.alpn,
+        path: c.path,
+        dest: c.dest,
+        xver: Number(c.xver) || 0,
+        sortOrder: i,
+      })),
+    };
+    const msg = await HttpUtil.post(
+      `/panel/api/inbounds/${masterId}/fallbacks`,
+      payload,
+      { headers: { 'Content-Type': 'application/json' } },
+    );
+    return !!msg?.success;
+  };
+
+  // Derive a fallback row's SNI / ALPN / Path / xver from a child
+  // inbound's streamSettings — what the legacy panel auto-filled when an
+  // operator wired a fallback target. SNI/ALPN come straight off the
+  // child's TLS block; path depends on the child's transport (ws/grpc
+  // /httpupgrade carry an explicit path; tcp/kcp/xhttp have no path of
+  // their own). xver stays 0 unless the child explicitly opts in via
+  // PROXY-protocol sockopt.
+  const deriveFallbackDefaults = (childId: number): Partial<FallbackRow> => {
+    const child = (dbInbounds || []).find((ib) => ib.id === childId);
+    if (!child) return {};
+    const stream = coerceInboundJsonField(child.streamSettings);
+    const tls = (stream.tlsSettings as Record<string, unknown> | undefined) ?? {};
+    const network = typeof stream.network === 'string' ? stream.network : '';
+    const sni = typeof tls.serverName === 'string' ? tls.serverName : '';
+    const alpnArr = Array.isArray(tls.alpn) ? tls.alpn : [];
+    const alpn = alpnArr.filter((v) => typeof v === 'string').join(',');
+    let path = '';
+    if (network === 'ws') {
+      const ws = (stream.wsSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof ws.path === 'string') path = ws.path;
+    } else if (network === 'grpc') {
+      const grpc = (stream.grpcSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof grpc.serviceName === 'string') path = grpc.serviceName;
+    } else if (network === 'httpupgrade') {
+      const hu = (stream.httpupgradeSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof hu.path === 'string') path = hu.path;
+    } else if (network === 'xhttp') {
+      const xh = (stream.xhttpSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof xh.path === 'string') path = xh.path;
+    }
+    return { name: sni, alpn, path, xver: 0 };
+  };
+
+  const addFallback = () => {
+    setFallbacks((prev) => [...prev, {
+      rowKey: `fb-${++fallbackKeyRef.current}`,
+      childId: null,
+      name: '',
+      alpn: '',
+      path: '',
+      dest: '',
+      xver: 0,
+    }]);
+  };
+
+  const updateFallback = (rowKey: string, patch: Partial<FallbackRow>) => {
+    setFallbacks((prev) => prev.map((r) => {
+      if (r.rowKey !== rowKey) return r;
+      // When the picker selects a new child inbound and the row hasn't
+      // been hand-edited yet (sni/alpn/path/dest all blank, xver = 0),
+      // pull the SNI/ALPN/Path defaults off that child. Operators who
+      // intentionally typed values keep them — we only fill the empties.
+      if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
+        const isPristine = !r.name && !r.alpn && !r.path && !r.dest && r.xver === 0;
+        if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
+      }
+      return { ...r, ...patch };
+    }));
+  };
+
+  const removeFallback = (idx: number) => {
+    setFallbacks((prev) => prev.filter((_, i) => i !== idx));
+  };
+
+  // Move a fallback row up/down by swapping adjacent indices. The order
+  // is persisted via the fallback row's sortOrder (rebuilt by index on
+  // save), so reordering survives reloads.
+  const moveFallback = (idx: number, direction: -1 | 1) => {
+    setFallbacks((prev) => {
+      const target = idx + direction;
+      if (target < 0 || target >= prev.length) return prev;
+      const next = prev.slice();
+      [next[idx], next[target]] = [next[target], next[idx]];
+      return next;
+    });
+  };
+
+  // One-shot: add a fresh fallback row for every eligible inbound (i.e.
+  // every option in fallbackChildOptions) that is not already wired up.
+  // Convenient for operators who want catch-all routing to every host
+  // they manage on the panel.
+  const addAllFallbacks = () => {
+    setFallbacks((prev) => {
+      const alreadyHave = new Set(prev.map((r) => r.childId));
+      const additions = fallbackChildOptions
+        .filter((opt) => !alreadyHave.has(opt.value))
+        .map<FallbackRow>((opt) => {
+          const derived = deriveFallbackDefaults(opt.value);
+          return {
+            rowKey: `fb-${++fallbackKeyRef.current}`,
+            childId: opt.value,
+            name: derived.name ?? '',
+            alpn: derived.alpn ?? '',
+            path: derived.path ?? '',
+            dest: '',
+            xver: derived.xver ?? 0,
+          };
+        });
+      if (additions.length === 0) return prev;
+      return [...prev, ...additions];
+    });
+  };
+
+  return {
+    fallbacks,
+    fallbackChildOptions,
+    loadFallbacks,
+    saveFallbacks,
+    addFallback,
+    updateFallback,
+    removeFallback,
+    moveFallback,
+    addAllFallbacks,
+  };
+}

+ 205 - 0
frontend/src/pages/inbounds/form/useSecurityActions.ts

@@ -0,0 +1,205 @@
+import type { Dispatch, SetStateAction } from 'react';
+import { useTranslation } from 'react-i18next';
+import type { FormInstance } from 'antd';
+import type { MessageInstance } from 'antd/es/message/interface';
+
+import { HttpUtil, RandomUtil } from '@/utils';
+import { getRandomRealityTarget } from '@/models/reality-targets';
+import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
+import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
+import type { InboundFormValues } from '@/schemas/forms/inbound-form';
+
+interface UseSecurityActionsArgs {
+  form: FormInstance<InboundFormValues>;
+  setSaving: Dispatch<SetStateAction<boolean>>;
+  messageApi: MessageInstance;
+}
+
+// Server-side TLS / Reality key + certificate generation handlers for the
+// inbound modal's security tab. Each talks to a /panel server endpoint and
+// writes the result back into the form. Lifted out of InboundFormModal so
+// the modal body stays focused on orchestration.
+export function useSecurityActions({ form, setSaving, messageApi }: UseSecurityActionsArgs) {
+  const { t } = useTranslation();
+
+  const genRealityKeypair = async () => {
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
+      if (msg?.success) {
+        const obj = msg.obj as { privateKey: string; publicKey: string };
+        form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
+        form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
+      }
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const clearRealityKeypair = () => {
+    form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], '');
+    form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], '');
+  };
+
+  const genMldsa65 = async () => {
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
+      if (msg?.success) {
+        const obj = msg.obj as { seed: string; verify: string };
+        form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], obj.seed);
+        form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], obj.verify);
+      }
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const clearMldsa65 = () => {
+    form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], '');
+    form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], '');
+  };
+
+  const randomizeRealityTarget = () => {
+    const tgt = getRandomRealityTarget() as { target: string; sni: string };
+    form.setFieldValue(['streamSettings', 'realitySettings', 'target'], tgt.target);
+    form.setFieldValue(
+      ['streamSettings', 'realitySettings', 'serverNames'],
+      tgt.sni.split(',').map((s) => s.trim()).filter(Boolean),
+    );
+  };
+
+  const randomizeShortIds = () => {
+    form.setFieldValue(
+      ['streamSettings', 'realitySettings', 'shortIds'],
+      RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean),
+    );
+  };
+
+  const getNewEchCert = async () => {
+    const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']);
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni });
+      if (msg?.success) {
+        const obj = msg.obj as { echServerKeys: string; echConfigList: string };
+        form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], obj.echServerKeys);
+        form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], obj.echConfigList);
+      }
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const clearEchCert = () => {
+    form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], '');
+    form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
+  };
+
+  const generateRandomPinHash = () => {
+    const bytes = new Uint8Array(32);
+    crypto.getRandomValues(bytes);
+    let binary = '';
+    for (const b of bytes) binary += String.fromCharCode(b);
+    const hash = btoa(binary);
+    const current = (form.getFieldValue(
+      ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
+    ) as string[] | undefined) ?? [];
+    form.setFieldValue(
+      ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
+      [...current, hash],
+    );
+  };
+
+  const setCertFromPanel = async (certName: number) => {
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
+      if (msg?.success) {
+        const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
+        if (!obj.webCertFile && !obj.webKeyFile) {
+          messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
+          return;
+        }
+        form.setFieldValue(
+          ['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
+          obj.webCertFile ?? '',
+        );
+        form.setFieldValue(
+          ['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
+          obj.webKeyFile ?? '',
+        );
+      }
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const clearCertFiles = (certName: number) => {
+    form.setFieldValue(
+      ['streamSettings', 'tlsSettings', 'certificates', certName, 'certificateFile'],
+      '',
+    );
+    form.setFieldValue(
+      ['streamSettings', 'tlsSettings', 'certificates', certName, 'keyFile'],
+      '',
+    );
+  };
+
+  const onSecurityChange = async (next: string) => {
+    const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
+    const cleaned: Record<string, unknown> = { ...current, security: next };
+    delete cleaned.tlsSettings;
+    delete cleaned.realitySettings;
+    if (next === 'tls') {
+      const tls = TlsStreamSettingsSchema.parse({}) as Record<string, unknown>;
+      tls.certificates = [{
+        useFile: true,
+        certificateFile: '',
+        keyFile: '',
+        certificate: [],
+        key: [],
+        oneTimeLoading: false,
+        usage: 'encipherment',
+        buildChain: false,
+      }];
+      cleaned.tlsSettings = tls;
+    }
+    if (next === 'reality') {
+      const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
+      const tgt = getRandomRealityTarget() as { target: string; sni: string };
+      reality.target = tgt.target;
+      reality.serverNames = tgt.sni.split(',').map((s) => s.trim()).filter(Boolean);
+      reality.shortIds = RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean);
+      cleaned.realitySettings = reality;
+    }
+    form.setFieldValue('streamSettings', cleaned);
+    if (next === 'reality') {
+      try {
+        const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
+        if (msg?.success) {
+          const obj = msg.obj as { privateKey: string; publicKey: string };
+          form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
+          form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
+        }
+      } catch {
+        // best-effort: leave keypair fields empty if server call fails
+      }
+    }
+  };
+
+  return {
+    genRealityKeypair,
+    clearRealityKeypair,
+    genMldsa65,
+    clearMldsa65,
+    randomizeRealityTarget,
+    randomizeShortIds,
+    getNewEchCert,
+    clearEchCert,
+    generateRandomPinHash,
+    setCertFromPanel,
+    clearCertFiles,
+    onSecurityChange,
+  };
+}

+ 0 - 0
frontend/src/pages/inbounds/InboundInfoModal.css → frontend/src/pages/inbounds/info/InboundInfoModal.css


+ 12 - 262
frontend/src/pages/inbounds/InboundInfoModal.tsx → frontend/src/pages/inbounds/info/InboundInfoModal.tsx

@@ -1,279 +1,29 @@
 import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
 import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
 import { Button, Divider, Modal, Space, Tabs, Tag, Tooltip } from 'antd';
-import { getMessage } from '@/utils/messageBus';
 import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
 import { CopyOutlined, SyncOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
 
 
-import {
-  HttpUtil,
-  IntlUtil,
-  SizeFormatter,
-  ColorUtils,
-  ClipboardManager,
-  FileManager,
-} from '@/utils';
+import { HttpUtil, IntlUtil, SizeFormatter, ColorUtils } from '@/utils';
 import { Protocols } from '@/schemas/primitives';
 import { Protocols } from '@/schemas/primitives';
-import InfinityIcon from '@/components/InfinityIcon';
+import { InfinityIcon } from '@/components/ui';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import { useDatepicker } from '@/hooks/useDatepicker';
-import { coerceInboundJsonField } from '@/models/dbinbound';
-import {
-  canEnableTlsFlow,
-  isSS2022 as isSS2022Helper,
-  isSSMultiUser as isSSMultiUserHelper,
-} from '@/lib/xray/protocol-capabilities';
 import {
 import {
   genAllLinks,
   genAllLinks,
   genWireguardConfigs,
   genWireguardConfigs,
   genWireguardLinks,
   genWireguardLinks,
 } from '@/lib/xray/inbound-link';
 } from '@/lib/xray/inbound-link';
 import { inboundFromDb } from '@/lib/xray/inbound-from-db';
 import { inboundFromDb } from '@/lib/xray/inbound-from-db';
-import type { SubSettings } from './useInbounds';
-import './InboundInfoModal.css';
-
-const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
-  Protocols.VMESS,
-  Protocols.VLESS,
-  Protocols.TROJAN,
-  Protocols.SHADOWSOCKS,
-  Protocols.HYSTERIA,
-]);
-
-function hasShareLink(protocol: string): boolean {
-  return LINK_PROTOCOLS.has(protocol);
-}
-
-function readHeader(headers: unknown, name: string): string {
-  const needle = name.toLowerCase();
-  if (Array.isArray(headers)) {
-    for (const h of headers) {
-      if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
-        return String((h as { value?: unknown }).value ?? '');
-      }
-    }
-    return '';
-  }
-  if (headers && typeof headers === 'object') {
-    for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
-      if (k.toLowerCase() === needle) {
-        return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
-      }
-    }
-  }
-  return '';
-}
-
-function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
-  switch (network) {
-    case 'tcp': {
-      const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
-      return readHeader(tcp?.header?.request?.headers, 'host');
-    }
-    case 'ws': {
-      const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
-      return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
-    }
-    case 'httpupgrade': {
-      const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
-      return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
-    }
-    case 'xhttp': {
-      const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
-      return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
-    }
-    default:
-      return null;
-  }
-}
-
-function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
-  switch (network) {
-    case 'tcp': {
-      const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
-      return tcp?.header?.request?.path?.[0] ?? null;
-    }
-    case 'ws':
-      return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
-    case 'httpupgrade':
-      return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
-    case 'xhttp':
-      return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
-    default:
-      return null;
-  }
-}
 
 
-interface ClientStats {
-  email: string;
-  up: number;
-  down: number;
-  total: number;
-  expiryTime: number;
-  enable?: boolean;
-}
-
-interface ClientSetting {
-  email?: string;
-  id?: string;
-  security?: string;
-  password?: string;
-  flow?: string;
-  subId?: string;
-  totalGB?: number;
-  expiryTime?: number;
-  comment?: string;
-  tgId?: string;
-  enable?: boolean;
-  limitIp?: number;
-  created_at?: number;
-  updated_at?: number;
-}
-
-interface InboundInfo {
-  protocol: string;
-  clients: ClientSetting[];
-  settings: Record<string, unknown>;
-  isTcp: boolean;
-  isWs: boolean;
-  isHttpupgrade: boolean;
-  isXHTTP: boolean;
-  isGrpc: boolean;
-  isSSMultiUser: boolean;
-  isSS2022: boolean;
-  isVlessTlsFlow: boolean;
-  host: string | null;
-  path: string | null;
-  serviceName: string;
-  serverName: string;
-  stream: {
-    network: string;
-    security: string;
-    xhttp?: { mode?: string };
-    grpc?: { multiMode?: boolean };
-  };
-}
-
-interface DBInboundLike {
-  id: number;
-  address: string;
-  port: number;
-  listen: string;
-  protocol: string;
-  remark: string;
-  enable?: boolean;
-  isVMess?: boolean;
-  isVLess?: boolean;
-  isTrojan?: boolean;
-  isSS?: boolean;
-  isMixed?: boolean;
-  isHTTP?: boolean;
-  isWireguard?: boolean;
-  settings: unknown;
-  streamSettings: unknown;
-  sniffing: unknown;
-  clientStats?: ClientStats[];
-}
-
-function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
-  const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
-  const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
-  const network = (stream.network as string | undefined) ?? '';
-  const security = (stream.security as string | undefined) ?? 'none';
-  const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
-  const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
-  const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
-  let serverName = '';
-  if (security === 'tls') {
-    const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
-    serverName = tls?.sni ?? tls?.serverName ?? '';
-  } else if (security === 'reality') {
-    const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
-    if (Array.isArray(reality?.serverNames)) {
-      serverName = reality.serverNames.join(', ');
-    } else if (reality?.serverName) {
-      serverName = reality.serverName;
-    }
-  }
-  return {
-    protocol: dbInbound.protocol,
-    clients,
-    settings,
-    isTcp: network === 'tcp',
-    isWs: network === 'ws',
-    isHttpupgrade: network === 'httpupgrade',
-    isXHTTP: network === 'xhttp',
-    isGrpc: network === 'grpc',
-    isSSMultiUser: isSSMultiUserHelper({
-      protocol: dbInbound.protocol,
-      settings: settings as { method?: string },
-    }),
-    isSS2022: isSS2022Helper({
-      protocol: dbInbound.protocol,
-      settings: settings as { method?: string },
-    }),
-    isVlessTlsFlow: canEnableTlsFlow({
-      protocol: dbInbound.protocol,
-      streamSettings: { network, security },
-    }),
-    host: readNetworkHost(stream, network),
-    path: readNetworkPath(stream, network),
-    serviceName: grpcSettings?.serviceName ?? '',
-    serverName,
-    stream: {
-      network,
-      security,
-      xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
-      grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
-    },
-  };
-}
-
-interface InboundInfoModalProps {
-  open: boolean;
-  onClose: () => void;
-  dbInbound: DBInboundLike | null;
-  clientIndex?: number;
-  remarkModel?: string;
-  expireDiff?: number;
-  trafficDiff?: number;
-  ipLimitEnable?: boolean;
-  tgBotEnable?: boolean;
-  nodeAddress?: string;
-  subSettings?: SubSettings;
-  lastOnlineMap?: Record<string, number>;
-}
-
-function copyText(value: unknown, t: (k: string) => string) {
-  ClipboardManager.copyText(String(value ?? '')).then((ok) => {
-    if (ok) getMessage().success(t('copied'));
-  });
-}
-
-function downloadText(content: string, filename: string) {
-  FileManager.downloadTextFile(content, filename);
-}
-
-function statsColor(stats: ClientStats, trafficDiff: number) {
-  return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
-}
-
-function formatIpInfo(record: unknown) {
-  if (record == null) return '';
-  if (typeof record === 'string' || typeof record === 'number') return String(record);
-  const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
-  const ip = r.ip || r.IP || '';
-  const ts = r.timestamp || r.Timestamp || 0;
-  if (!ip) return String(record);
-  if (!ts) return String(ip);
-  const date = new Date(Number(ts) * 1000);
-  const timeStr = date
-    .toLocaleString('en-GB', {
-      year: 'numeric', month: '2-digit', day: '2-digit',
-      hour: '2-digit', minute: '2-digit', second: '2-digit',
-      hour12: false,
-    })
-    .replace(',', '');
-  return `${ip} (${timeStr})`;
-}
+import {
+  buildInboundInfo,
+  copyText,
+  downloadText,
+  formatIpInfo,
+  hasShareLink,
+  statsColor,
+} from './helpers';
+import type { ClientSetting, ClientStats, InboundInfo, InboundInfoModalProps } from './types';
+import './InboundInfoModal.css';
 
 
 export default function InboundInfoModal({
 export default function InboundInfoModal({
   open,
   open,

+ 170 - 0
frontend/src/pages/inbounds/info/helpers.ts

@@ -0,0 +1,170 @@
+import { getMessage } from '@/utils/messageBus';
+import { ColorUtils, ClipboardManager, FileManager } from '@/utils';
+import { Protocols } from '@/schemas/primitives';
+import { coerceInboundJsonField } from '@/models/dbinbound';
+import {
+  canEnableTlsFlow,
+  isSS2022 as isSS2022Helper,
+  isSSMultiUser as isSSMultiUserHelper,
+} from '@/lib/xray/protocol-capabilities';
+
+import type { ClientSetting, ClientStats, DBInboundLike, InboundInfo } from './types';
+
+const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
+  Protocols.VMESS,
+  Protocols.VLESS,
+  Protocols.TROJAN,
+  Protocols.SHADOWSOCKS,
+  Protocols.HYSTERIA,
+]);
+
+export function hasShareLink(protocol: string): boolean {
+  return LINK_PROTOCOLS.has(protocol);
+}
+
+function readHeader(headers: unknown, name: string): string {
+  const needle = name.toLowerCase();
+  if (Array.isArray(headers)) {
+    for (const h of headers) {
+      if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
+        return String((h as { value?: unknown }).value ?? '');
+      }
+    }
+    return '';
+  }
+  if (headers && typeof headers === 'object') {
+    for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
+      if (k.toLowerCase() === needle) {
+        return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
+      }
+    }
+  }
+  return '';
+}
+
+function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
+  switch (network) {
+    case 'tcp': {
+      const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
+      return readHeader(tcp?.header?.request?.headers, 'host');
+    }
+    case 'ws': {
+      const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
+      return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
+    }
+    case 'httpupgrade': {
+      const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
+      return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
+    }
+    case 'xhttp': {
+      const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
+      return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
+    }
+    default:
+      return null;
+  }
+}
+
+function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
+  switch (network) {
+    case 'tcp': {
+      const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
+      return tcp?.header?.request?.path?.[0] ?? null;
+    }
+    case 'ws':
+      return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
+    case 'httpupgrade':
+      return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
+    case 'xhttp':
+      return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
+    default:
+      return null;
+  }
+}
+
+export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
+  const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
+  const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
+  const network = (stream.network as string | undefined) ?? '';
+  const security = (stream.security as string | undefined) ?? 'none';
+  const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
+  const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
+  const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
+  let serverName = '';
+  if (security === 'tls') {
+    const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
+    serverName = tls?.sni ?? tls?.serverName ?? '';
+  } else if (security === 'reality') {
+    const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
+    if (Array.isArray(reality?.serverNames)) {
+      serverName = reality.serverNames.join(', ');
+    } else if (reality?.serverName) {
+      serverName = reality.serverName;
+    }
+  }
+  return {
+    protocol: dbInbound.protocol,
+    clients,
+    settings,
+    isTcp: network === 'tcp',
+    isWs: network === 'ws',
+    isHttpupgrade: network === 'httpupgrade',
+    isXHTTP: network === 'xhttp',
+    isGrpc: network === 'grpc',
+    isSSMultiUser: isSSMultiUserHelper({
+      protocol: dbInbound.protocol,
+      settings: settings as { method?: string },
+    }),
+    isSS2022: isSS2022Helper({
+      protocol: dbInbound.protocol,
+      settings: settings as { method?: string },
+    }),
+    isVlessTlsFlow: canEnableTlsFlow({
+      protocol: dbInbound.protocol,
+      streamSettings: { network, security },
+    }),
+    host: readNetworkHost(stream, network),
+    path: readNetworkPath(stream, network),
+    serviceName: grpcSettings?.serviceName ?? '',
+    serverName,
+    stream: {
+      network,
+      security,
+      xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
+      grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
+    },
+  };
+}
+
+export function copyText(value: unknown, t: (k: string) => string) {
+  ClipboardManager.copyText(String(value ?? '')).then((ok) => {
+    if (ok) getMessage().success(t('copied'));
+  });
+}
+
+export function downloadText(content: string, filename: string) {
+  FileManager.downloadTextFile(content, filename);
+}
+
+export function statsColor(stats: ClientStats, trafficDiff: number) {
+  return ColorUtils.usageColor(stats.up + stats.down, trafficDiff, stats.total);
+}
+
+export function formatIpInfo(record: unknown) {
+  if (record == null) return '';
+  if (typeof record === 'string' || typeof record === 'number') return String(record);
+  const r = record as { ip?: string; IP?: string; timestamp?: number | string; Timestamp?: number | string };
+  const ip = r.ip || r.IP || '';
+  const ts = r.timestamp || r.Timestamp || 0;
+  if (!ip) return String(record);
+  if (!ts) return String(ip);
+  const date = new Date(Number(ts) * 1000);
+  const timeStr = date
+    .toLocaleString('en-GB', {
+      year: 'numeric', month: '2-digit', day: '2-digit',
+      hour: '2-digit', minute: '2-digit', second: '2-digit',
+      hour12: false,
+    })
+    .replace(',', '');
+  return `${ip} (${timeStr})`;
+}

+ 1 - 0
frontend/src/pages/inbounds/info/index.ts

@@ -0,0 +1 @@
+export { default as InboundInfoModal } from './InboundInfoModal';

+ 87 - 0
frontend/src/pages/inbounds/info/types.ts

@@ -0,0 +1,87 @@
+import type { SubSettings } from '../useInbounds';
+
+export interface ClientStats {
+  email: string;
+  up: number;
+  down: number;
+  total: number;
+  expiryTime: number;
+  enable?: boolean;
+}
+
+export interface ClientSetting {
+  email?: string;
+  id?: string;
+  security?: string;
+  password?: string;
+  flow?: string;
+  subId?: string;
+  totalGB?: number;
+  expiryTime?: number;
+  comment?: string;
+  tgId?: string;
+  enable?: boolean;
+  limitIp?: number;
+  created_at?: number;
+  updated_at?: number;
+}
+
+export interface InboundInfo {
+  protocol: string;
+  clients: ClientSetting[];
+  settings: Record<string, unknown>;
+  isTcp: boolean;
+  isWs: boolean;
+  isHttpupgrade: boolean;
+  isXHTTP: boolean;
+  isGrpc: boolean;
+  isSSMultiUser: boolean;
+  isSS2022: boolean;
+  isVlessTlsFlow: boolean;
+  host: string | null;
+  path: string | null;
+  serviceName: string;
+  serverName: string;
+  stream: {
+    network: string;
+    security: string;
+    xhttp?: { mode?: string };
+    grpc?: { multiMode?: boolean };
+  };
+}
+
+export interface DBInboundLike {
+  id: number;
+  address: string;
+  port: number;
+  listen: string;
+  protocol: string;
+  remark: string;
+  enable?: boolean;
+  isVMess?: boolean;
+  isVLess?: boolean;
+  isTrojan?: boolean;
+  isSS?: boolean;
+  isMixed?: boolean;
+  isHTTP?: boolean;
+  isWireguard?: boolean;
+  settings: unknown;
+  streamSettings: unknown;
+  sniffing: unknown;
+  clientStats?: ClientStats[];
+}
+
+export interface InboundInfoModalProps {
+  open: boolean;
+  onClose: () => void;
+  dbInbound: DBInboundLike | null;
+  clientIndex?: number;
+  remarkModel?: string;
+  expireDiff?: number;
+  trafficDiff?: number;
+  ipLimitEnable?: boolean;
+  tgBotEnable?: boolean;
+  nodeAddress?: string;
+  subSettings?: SubSettings;
+  lastOnlineMap?: Record<string, number>;
+}

+ 0 - 0
frontend/src/pages/inbounds/InboundList.css → frontend/src/pages/inbounds/list/InboundList.css


+ 203 - 0
frontend/src/pages/inbounds/list/InboundList.tsx

@@ -0,0 +1,203 @@
+import { useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+  Button,
+  Card,
+  Dropdown,
+  Space,
+  Switch,
+  Table,
+  Tooltip,
+  type MenuProps,
+} from 'antd';
+import {
+  PlusOutlined,
+  MenuOutlined,
+  MoreOutlined,
+  ExportOutlined,
+  ImportOutlined,
+  ReloadOutlined,
+  InfoCircleOutlined,
+} from '@ant-design/icons';
+
+import { HttpUtil } from '@/utils';
+
+import { SORT_FNS } from './helpers';
+import { buildRowActionsMenu } from './RowActions';
+import { useInboundColumns } from './useInboundColumns';
+import InboundStatsModal from './InboundStatsModal';
+import type { DBInboundRecord, GeneralAction, InboundListProps, RowAction, SortKey, SortOrder } from './types';
+import './InboundList.css';
+
+export default function InboundList({
+  dbInbounds,
+  clientCount,
+  lastOnlineMap: _lastOnlineMap,
+  expireDiff,
+  trafficDiff,
+  pageSize,
+  isMobile,
+  subEnable,
+  nodesById,
+  hasActiveNode,
+  onAddInbound,
+  onGeneralAction,
+  onRowAction,
+}: InboundListProps) {
+  const { t } = useTranslation();
+  const [sortKey, setSortKey] = useState<SortKey | null>(null);
+  const [sortOrder, setSortOrder] = useState<SortOrder>(null);
+  const [statsRecord, setStatsRecord] = useState<DBInboundRecord | null>(null);
+
+  const onSwitchEnable = useCallback(async (dbInbound: DBInboundRecord, next: boolean) => {
+    const previous = dbInbound.enable;
+    dbInbound.enable = next;
+    try {
+      const formData = new FormData();
+      formData.append('enable', String(next));
+      const msg = await HttpUtil.post(`/panel/api/inbounds/setEnable/${dbInbound.id}`, formData);
+      if (!msg?.success) dbInbound.enable = previous;
+    } catch {
+      dbInbound.enable = previous;
+    }
+  }, []);
+
+  const sortedInbounds = useMemo(() => {
+    if (!sortKey || !sortOrder) return dbInbounds;
+    const fn = SORT_FNS[sortKey];
+    if (!fn) return dbInbounds;
+    const sorted = [...dbInbounds].sort((a, b) => fn(a, b, { nodesById, clientCount }));
+    return sortOrder === 'descend' ? sorted.reverse() : sorted;
+  }, [dbInbounds, sortKey, sortOrder, nodesById, clientCount]);
+
+  const hasAnyRemark = useMemo(
+    () => dbInbounds.some((i) => typeof i.remark === 'string' && i.remark.trim() !== ''),
+    [dbInbounds],
+  );
+
+  const columns = useInboundColumns({
+    hasAnyRemark,
+    hasActiveNode,
+    nodesById,
+    clientCount,
+    subEnable,
+    expireDiff,
+    trafficDiff,
+    sortKey,
+    sortOrder,
+    onRowAction,
+    onSwitchEnable,
+  });
+
+  const paginationFor = (rows: DBInboundRecord[]) => {
+    const size = pageSize > 0 ? pageSize : rows.length || 1;
+    return { pageSize: size, showSizeChanger: false, hideOnSinglePage: true };
+  };
+
+  const generalActionsMenu: MenuProps = {
+    items: [
+      { key: 'import', icon: <ImportOutlined />, label: t('pages.inbounds.importInbound') },
+      { key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') },
+      ...(subEnable
+        ? [{ key: 'subs', icon: <ExportOutlined />, label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}` }]
+        : []),
+      { key: 'resetInbounds', icon: <ReloadOutlined />, label: t('pages.inbounds.resetAllTraffic') },
+    ],
+    onClick: ({ key }) => onGeneralAction(key as GeneralAction),
+  };
+
+  return (
+    <Card
+      hoverable
+      title={(
+        <Space>
+          <Button type="primary" onClick={onAddInbound} icon={<PlusOutlined />}>
+            {!isMobile && t('pages.inbounds.addInbound')}
+          </Button>
+          <Dropdown trigger={['click']} menu={generalActionsMenu}>
+            <Button type="primary" icon={<MenuOutlined />}>
+              {!isMobile && t('pages.inbounds.generalActions')}
+            </Button>
+          </Dropdown>
+        </Space>
+      )}
+    >
+      <Space orientation="vertical" style={{ width: '100%' }}>
+        {isMobile ? (
+          <div className="inbound-cards">
+            {sortedInbounds.length === 0 ? (
+              <div className="card-empty">
+                <ImportOutlined style={{ fontSize: 28, opacity: 0.5 }} />
+                <div>{t('noData')}</div>
+              </div>
+            ) : (
+              sortedInbounds.map((record) => (
+                <div key={record.id} className="inbound-card">
+                  <div className="card-head">
+                    <span className="card-id">#{record.id}</span>
+                    <span className="tag-name">{record.remark}</span>
+                    <div className="card-actions" onClick={(e) => e.stopPropagation()}>
+                      <Tooltip title={t('pages.inbounds.inboundInfo')}>
+                        <InfoCircleOutlined className="row-action-trigger" onClick={() => setStatsRecord(record)} />
+                      </Tooltip>
+                      <Switch
+                        checked={record.enable}
+                        size="small"
+                        onChange={(next) => onSwitchEnable(record, next)}
+                      />
+                      <Dropdown
+                        trigger={['click']}
+                        placement="bottomRight"
+                        menu={{
+                          items: buildRowActionsMenu({ record, subEnable, t, isMobile: true, hasClients: (clientCount[record.id]?.clients || 0) > 0 }),
+                          onClick: ({ key }) => onRowAction({ key: key as RowAction, dbInbound: record }),
+                        }}
+                      >
+                        <MoreOutlined className="row-action-trigger" onClick={(e) => e.preventDefault()} />
+                      </Dropdown>
+                    </div>
+                  </div>
+                </div>
+              ))
+            )}
+          </div>
+        ) : (
+          <Table
+            columns={columns}
+            dataSource={sortedInbounds}
+            rowKey={(r) => r.id}
+            pagination={paginationFor(sortedInbounds)}
+            scroll={{ x: 1000 }}
+            style={{ marginTop: 10 }}
+            size="small"
+            locale={{
+              emptyText: (
+                <div className="card-empty">
+                  <ImportOutlined style={{ fontSize: 32, marginBottom: 8 }} />
+                  <div>{t('noData')}</div>
+                </div>
+              ),
+            }}
+            onChange={(_p, _f, sorter) => {
+              const single = Array.isArray(sorter) ? sorter[0] : sorter;
+              const colKey = (single?.columnKey || single?.field) as SortKey | undefined;
+              setSortKey(colKey || null);
+              setSortOrder((single?.order as SortOrder) || null);
+            }}
+          />
+        )}
+      </Space>
+
+      <InboundStatsModal
+        open={isMobile && !!statsRecord}
+        record={statsRecord}
+        hasActiveNode={hasActiveNode}
+        nodesById={nodesById}
+        clientCount={clientCount}
+        trafficDiff={trafficDiff}
+        expireDiff={expireDiff}
+        onClose={() => setStatsRecord(null)}
+      />
+    </Card>
+  );
+}

+ 141 - 0
frontend/src/pages/inbounds/list/InboundStatsModal.tsx

@@ -0,0 +1,141 @@
+import { useTranslation } from 'react-i18next';
+import { Modal, Tag } from 'antd';
+
+import { SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
+import { InfinityIcon } from '@/components/ui';
+import type { NodeRecord } from '@/api/queries/useNodesQuery';
+
+import {
+  readStreamHints,
+  networkLabel,
+  networkL4,
+  shadowsocksNetworkLabel,
+  tunnelNetworkLabel,
+  mixedNetworkLabel,
+} from './helpers';
+import type { ClientCountEntry, DBInboundRecord } from './types';
+
+interface InboundStatsModalProps {
+  open: boolean;
+  record: DBInboundRecord | null;
+  hasActiveNode: boolean;
+  nodesById: Map<number, NodeRecord>;
+  clientCount: Record<number, ClientCountEntry>;
+  trafficDiff: number;
+  expireDiff: number;
+  onClose: () => void;
+}
+
+export default function InboundStatsModal({
+  open,
+  record,
+  hasActiveNode,
+  nodesById,
+  clientCount,
+  trafficDiff,
+  expireDiff,
+  onClose,
+}: InboundStatsModalProps) {
+  const { t } = useTranslation();
+  return (
+    <Modal
+      open={open}
+      footer={null}
+      width={360}
+      centered
+      title={record ? `#${record.id} ${record.remark || ''}`.trim() : ''}
+      onCancel={onClose}
+      destroyOnHidden
+    >
+      {record && (
+        <div className="card-stats">
+          <div className="stat-row">
+            <span className="stat-label">{t('pages.inbounds.protocol')}</span>
+            <Tag color="purple">{record.protocol}</Tag>
+            {(record.isWireguard || record.isHysteria) && (
+              <Tag color="green">UDP</Tag>
+            )}
+            {record.isSS && (() => {
+              const stream = readStreamHints(record.streamSettings);
+              return (
+                <>
+                  <Tag color="green">{shadowsocksNetworkLabel(record.settings)}</Tag>
+                  {stream.isTls && <Tag color="blue">TLS</Tag>}
+                </>
+              );
+            })()}
+            {record.isTunnel && (
+              <Tag color="green">{tunnelNetworkLabel(record.settings)}</Tag>
+            )}
+            {record.isMixed && (
+              <Tag color="green">{mixedNetworkLabel(record.settings)}</Tag>
+            )}
+            {(record.isVMess || record.isVLess || record.isTrojan) && (() => {
+              const stream = readStreamHints(record.streamSettings);
+              const l4 = networkL4(stream.network);
+              return (
+                <>
+                  <Tag color="green">{networkLabel(stream.network)}</Tag>
+                  {l4 && <Tag color="green">{l4}</Tag>}
+                  {stream.isTls && <Tag color="blue">TLS</Tag>}
+                  {stream.isReality && <Tag color="blue">Reality</Tag>}
+                </>
+              );
+            })()}
+          </div>
+          <div className="stat-row">
+            <span className="stat-label">{t('pages.inbounds.port')}</span>
+            <Tag>{record.port}</Tag>
+          </div>
+          {hasActiveNode && (
+            <div className="stat-row">
+              <span className="stat-label">{t('pages.inbounds.node')}</span>
+              {record.nodeId == null ? (
+                <Tag color="default">{t('pages.inbounds.localPanel')}</Tag>
+              ) : nodesById.get(record.nodeId) ? (
+                <Tag color={nodesById.get(record.nodeId)!.status === 'online' ? 'blue' : 'red'}>
+                  {nodesById.get(record.nodeId)!.name}
+                </Tag>
+              ) : (
+                <Tag color="orange">#{record.nodeId}</Tag>
+              )}
+            </div>
+          )}
+          <div className="stat-row">
+            <span className="stat-label">{t('pages.inbounds.traffic')}</span>
+            <Tag color={ColorUtils.usageColor(record.up + record.down, trafficDiff, record.total)}>
+              {SizeFormatter.sizeFormat(record.up + record.down)} /
+              {' '}
+              {record.total > 0 ? SizeFormatter.sizeFormat(record.total) : <InfinityIcon />}
+            </Tag>
+          </div>
+          {clientCount[record.id] && (
+            <div className="stat-row">
+              <span className="stat-label">{t('clients')}</span>
+              <Tag color="green" className="client-count-tag">{clientCount[record.id].clients}</Tag>
+              {clientCount[record.id].online.length > 0 && (
+                <Tag color="blue">{clientCount[record.id].online.length} {t('online')}</Tag>
+              )}
+              {clientCount[record.id].depleted.length > 0 && (
+                <Tag color="red">{clientCount[record.id].depleted.length} {t('depleted')}</Tag>
+              )}
+              {clientCount[record.id].expiring.length > 0 && (
+                <Tag color="orange">{clientCount[record.id].expiring.length} {t('depletingSoon')}</Tag>
+              )}
+            </div>
+          )}
+          <div className="stat-row">
+            <span className="stat-label">{t('pages.inbounds.expireDate')}</span>
+            {record.expiryTime > 0 ? (
+              <Tag color={ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)}>
+                {IntlUtil.formatRelativeTime(record.expiryTime)}
+              </Tag>
+            ) : (
+              <Tag color="purple"><InfinityIcon /></Tag>
+            )}
+          </div>
+        </div>
+      )}
+    </Modal>
+  );
+}

+ 81 - 0
frontend/src/pages/inbounds/list/RowActions.tsx

@@ -0,0 +1,81 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Dropdown, type MenuProps } from 'antd';
+import {
+  MoreOutlined,
+  EditOutlined,
+  QrcodeOutlined,
+  CopyOutlined,
+  ExportOutlined,
+  RetweetOutlined,
+  BlockOutlined,
+  DeleteOutlined,
+  InfoCircleOutlined,
+  TagsOutlined,
+  UsergroupAddOutlined,
+  UsergroupDeleteOutlined,
+} from '@ant-design/icons';
+
+import { isInboundMultiUser, showQrCodeMenu } from './helpers';
+import type { DBInboundRecord, RowAction } from './types';
+
+interface RowActionsMenuProps {
+  record: DBInboundRecord;
+  subEnable: boolean;
+  hasClients: boolean;
+  onClick: (key: RowAction) => void;
+  isMobile?: boolean;
+}
+
+export function buildRowActionsMenu({ record, subEnable, t, isMobile, hasClients }: { record: DBInboundRecord; subEnable: boolean; t: (k: string) => string; isMobile?: boolean; hasClients?: boolean }): MenuProps['items'] {
+  const items: MenuProps['items'] = [];
+  if (isMobile) {
+    items.push({ key: 'edit', icon: <EditOutlined />, label: t('edit') });
+  }
+  if (showQrCodeMenu(record)) {
+    items.push({ key: 'qrcode', icon: <QrcodeOutlined />, label: t('qrCode') });
+  }
+  if (isInboundMultiUser(record)) {
+    items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
+    if (subEnable) {
+      items.push({
+        key: 'subs',
+        icon: <ExportOutlined />,
+        label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
+      });
+    }
+  } else {
+    items.push({ key: 'showInfo', icon: <InfoCircleOutlined />, label: t('pages.inbounds.inboundInfo') });
+  }
+  items.push({ key: 'clipboard', icon: <CopyOutlined />, label: t('pages.inbounds.exportInbound') });
+  items.push({ key: 'resetTraffic', icon: <RetweetOutlined />, label: t('pages.inbounds.resetTraffic') });
+  items.push({ key: 'clone', icon: <BlockOutlined />, label: t('pages.inbounds.clone') });
+  if (isInboundMultiUser(record) && hasClients) {
+    items.push({ key: 'attachClients', icon: <UsergroupAddOutlined />, label: t('pages.inbounds.attachClients') });
+    items.push({ key: 'detachClients', icon: <UsergroupDeleteOutlined />, label: t('pages.inbounds.detachClients') });
+    items.push({ key: 'addToGroup', icon: <TagsOutlined />, label: t('pages.inbounds.addClientsToGroup') });
+    items.push({ type: 'divider' });
+    items.push({ key: 'delAllClients', icon: <UsergroupDeleteOutlined />, danger: true, label: t('pages.inbounds.delAllClients') });
+  } else {
+    items.push({ type: 'divider' });
+  }
+  items.push({ key: 'delete', icon: <DeleteOutlined />, danger: true, label: t('delete') });
+  return items;
+}
+
+export function RowActionsCell({ record, subEnable, hasClients, onClick }: RowActionsMenuProps) {
+  const { t } = useTranslation();
+  return (
+    <div className="action-buttons">
+      <Button type="text" size="small" icon={<EditOutlined />} onClick={() => onClick('edit')} />
+      <Dropdown
+        trigger={['click']}
+        menu={{
+          items: buildRowActionsMenu({ record, subEnable, t, hasClients }),
+          onClick: ({ key }) => onClick(key as RowAction),
+        }}
+      >
+        <Button type="text" size="small" icon={<MoreOutlined />} />
+      </Dropdown>
+    </div>
+  );
+}

+ 106 - 0
frontend/src/pages/inbounds/list/helpers.ts

@@ -0,0 +1,106 @@
+import type { NodeRecord } from '@/api/queries/useNodesQuery';
+import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
+import { coerceInboundJsonField } from '@/models/dbinbound';
+
+import type { ClientCountEntry, DBInboundRecord, SortKey, StreamHints } from './types';
+
+export function readStreamHints(streamSettings: unknown): StreamHints {
+  const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
+  return {
+    network: stream.network ?? '',
+    isTls: stream.security === 'tls',
+    isReality: stream.security === 'reality',
+  };
+}
+
+// Display label for a network value. All known transports render in
+// upper-case for visual consistency with the TCP/UDP/TLS/Reality tags
+// already shown alongside; compound names (`httpupgrade`, `splithttp`,
+// `xhttp`) get a tiny touch of casing so they don't read as one word.
+export function networkLabel(network: string): string {
+  const n = (network || '').toLowerCase();
+  if (!n) return 'TCP';
+  switch (n) {
+    case 'httpupgrade': return 'HTTPUpgrade';
+    case 'splithttp': return 'SplitHTTP';
+    case 'xhttp': return 'XHTTP';
+  }
+  return n.toUpperCase();
+}
+
+// Returns the underlying L4 protocol for transports whose name isn't
+// already TCP/UDP. `kcp` and `quic` both ride on UDP; everything else
+// (`ws`, `grpc`, `http`, `httpupgrade`, `xhttp`) is TCP-based and gets
+// no extra tag (the transport name implies TCP).
+export function networkL4(network: string): 'UDP' | '' {
+  const n = (network || '').toLowerCase();
+  if (n === 'kcp' || n === 'quic') return 'UDP';
+  return '';
+}
+
+// Shadowsocks settings.network ("tcp" / "udp" / "tcp,udp") and Tunnel
+// settings.allowedNetwork (same shape, different field name) both carry
+// the L4 transport list independent of streamSettings. Returns a
+// comma-separated label.
+export function commaNetworkLabel(raw: string): string {
+  const parts = (raw || 'tcp').toLowerCase().split(',').map((p) => p.trim()).filter(Boolean);
+  if (parts.length === 0) return 'TCP';
+  return parts.map(networkLabel).join(',');
+}
+
+export function shadowsocksNetworkLabel(settings: unknown): string {
+  return commaNetworkLabel(readSettings(settings).network || '');
+}
+
+export function tunnelNetworkLabel(settings: unknown): string {
+  return commaNetworkLabel(readSettings(settings).allowedNetwork || '');
+}
+
+// Mixed (socks+http combo) is always TCP at L4; settings.udp=true adds
+// UDP-associate support on the same port (SOCKS5 UDP).
+export function mixedNetworkLabel(settings: unknown): string {
+  const st = coerceInboundJsonField(settings) as { udp?: boolean };
+  return st.udp ? 'TCP,UDP' : 'TCP';
+}
+
+export function readSettings(settings: unknown): { method?: string; network?: string; allowedNetwork?: string } {
+  return coerceInboundJsonField(settings) as { method?: string; network?: string; allowedNetwork?: string };
+}
+
+export function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
+  switch (record.protocol) {
+    case 'vmess':
+    case 'vless':
+    case 'trojan':
+    case 'hysteria':
+      return true;
+    case 'shadowsocks':
+      return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
+    default:
+      return false;
+  }
+}
+
+export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
+  if (dbInbound.isWireguard) return true;
+  if (dbInbound.isSS) {
+    return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
+  }
+  return false;
+}
+
+export const SORT_FNS: Record<SortKey, (a: DBInboundRecord, b: DBInboundRecord, ctx: { nodesById: Map<number, NodeRecord>; clientCount: Record<number, ClientCountEntry> }) => number> = {
+  id: (a, b) => a.id - b.id,
+  enable: (a, b) => Number(a.enable) - Number(b.enable),
+  remark: (a, b) => (a.remark || '').localeCompare(b.remark || ''),
+  port: (a, b) => a.port - b.port,
+  protocol: (a, b) => a.protocol.localeCompare(b.protocol),
+  traffic: (a, b) => (a.up + a.down) - (b.up + b.down),
+  expiryTime: (a, b) => (a.expiryTime || Infinity) - (b.expiryTime || Infinity),
+  node: (a, b, ctx) => {
+    const nameA = ctx.nodesById.get(a.nodeId ?? -1)?.name ?? (a.nodeId == null ? '￿' : `node #${a.nodeId}`);
+    const nameB = ctx.nodesById.get(b.nodeId ?? -1)?.name ?? (b.nodeId == null ? '￿' : `node #${b.nodeId}`);
+    return nameA.localeCompare(nameB);
+  },
+  clients: (a, b, ctx) => (ctx.clientCount[a.id]?.clients || 0) - (ctx.clientCount[b.id]?.clients || 0),
+};

+ 2 - 0
frontend/src/pages/inbounds/list/index.ts

@@ -0,0 +1,2 @@
+export { default as InboundList } from './InboundList';
+export { isInboundMultiUser } from './helpers';

+ 88 - 0
frontend/src/pages/inbounds/list/types.ts

@@ -0,0 +1,88 @@
+import type { NodeRecord } from '@/api/queries/useNodesQuery';
+
+export interface StreamHints {
+  network: string;
+  isTls: boolean;
+  isReality: boolean;
+}
+
+export type ProtocolFlags = {
+  isVMess?: boolean;
+  isVLess?: boolean;
+  isTrojan?: boolean;
+  isSS?: boolean;
+  isHysteria?: boolean;
+  isMixed?: boolean;
+  isHTTP?: boolean;
+  isWireguard?: boolean;
+  isTunnel?: boolean;
+};
+
+export interface DBInboundRecord extends ProtocolFlags {
+  id: number;
+  enable: boolean;
+  remark: string;
+  port: number;
+  protocol: string;
+  up: number;
+  down: number;
+  total: number;
+  expiryTime: number;
+  _expiryTime: { valueOf(): number } | null;
+  nodeId?: number | null;
+  settings: unknown;
+  streamSettings: unknown;
+}
+
+export interface ClientCountEntry {
+  clients: number;
+  active: string[];
+  deactive: string[];
+  depleted: string[];
+  expiring: string[];
+  online: string[];
+}
+
+export type RowAction =
+  | 'edit'
+  | 'showInfo'
+  | 'qrcode'
+  | 'export'
+  | 'subs'
+  | 'clipboard'
+  | 'delete'
+  | 'resetTraffic'
+  | 'delAllClients'
+  | 'clone';
+
+export type GeneralAction = 'import' | 'export' | 'subs' | 'resetInbounds';
+
+export interface InboundListProps {
+  dbInbounds: DBInboundRecord[];
+  clientCount: Record<number, ClientCountEntry>;
+  onlineClients: string[];
+  lastOnlineMap: Record<string, number>;
+  expireDiff: number;
+  trafficDiff: number;
+  pageSize: number;
+  isMobile: boolean;
+  subEnable: boolean;
+  nodesById: Map<number, NodeRecord>;
+  hasActiveNode: boolean;
+  onAddInbound: () => void;
+  onGeneralAction: (key: GeneralAction) => void;
+  onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void;
+}
+
+export type SortKey =
+  | 'id'
+  | 'enable'
+  | 'remark'
+  | 'port'
+  | 'protocol'
+  | 'traffic'
+  | 'expiryTime'
+  | 'node'
+  | 'clients';
+
+export type SortOrder = 'ascend' | 'descend' | null;

+ 290 - 0
frontend/src/pages/inbounds/list/useInboundColumns.tsx

@@ -0,0 +1,290 @@
+import { useCallback, useMemo, type ReactElement } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Popover, Switch, Tag, type TableColumnType } from 'antd';
+
+import { SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
+import { InfinityIcon } from '@/components/ui';
+import { useDatepicker } from '@/hooks/useDatepicker';
+import type { NodeRecord } from '@/api/queries/useNodesQuery';
+
+import { RowActionsCell } from './RowActions';
+import {
+  readStreamHints,
+  networkLabel,
+  networkL4,
+  shadowsocksNetworkLabel,
+  tunnelNetworkLabel,
+  mixedNetworkLabel,
+} from './helpers';
+import type { ClientCountEntry, DBInboundRecord, RowAction, SortKey, SortOrder } from './types';
+
+interface UseInboundColumnsParams {
+  hasAnyRemark: boolean;
+  hasActiveNode: boolean;
+  nodesById: Map<number, NodeRecord>;
+  clientCount: Record<number, ClientCountEntry>;
+  subEnable: boolean;
+  expireDiff: number;
+  trafficDiff: number;
+  sortKey: SortKey | null;
+  sortOrder: SortOrder;
+  onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void;
+  onSwitchEnable: (dbInbound: DBInboundRecord, next: boolean) => void;
+}
+
+export function useInboundColumns({
+  hasAnyRemark,
+  hasActiveNode,
+  nodesById,
+  clientCount,
+  subEnable,
+  expireDiff,
+  trafficDiff,
+  sortKey,
+  sortOrder,
+  onRowAction,
+  onSwitchEnable,
+}: UseInboundColumnsParams): TableColumnType<DBInboundRecord>[] {
+  const { t } = useTranslation();
+  const { datepicker } = useDatepicker();
+
+  const sorterFor = useCallback((key: SortKey) => ({
+    sorter: true as const,
+    showSorterTooltip: false,
+    sortOrder: sortKey === key ? sortOrder : null,
+    sortDirections: ['ascend' as const, 'descend' as const],
+  }), [sortKey, sortOrder]);
+
+  return useMemo(() => {
+    const cols: TableColumnType<DBInboundRecord>[] = [
+      {
+        title: 'ID',
+        dataIndex: 'id',
+        key: 'id',
+        align: 'right',
+        width: 30,
+        ...sorterFor('id'),
+      },
+      {
+        title: t('pages.inbounds.operate'),
+        key: 'action',
+        align: 'center',
+        width: 60,
+        render: (_, record) => (
+          <RowActionsCell
+            record={record}
+            subEnable={subEnable}
+            hasClients={(clientCount[record.id]?.clients || 0) > 0}
+            onClick={(key) => onRowAction({ key, dbInbound: record })}
+          />
+        ),
+      },
+      {
+        title: t('pages.inbounds.enable'),
+        key: 'enable',
+        align: 'center',
+        width: 35,
+        ...sorterFor('enable'),
+        render: (_, record) => (
+          <Switch
+            checked={record.enable}
+            onChange={(next) => onSwitchEnable(record, next)}
+          />
+        ),
+      },
+    ];
+
+    if (hasAnyRemark) {
+      cols.push({
+        title: t('pages.inbounds.remark'),
+        dataIndex: 'remark',
+        key: 'remark',
+        align: 'center',
+        width: 60,
+        ...sorterFor('remark'),
+      });
+    }
+
+    if (hasActiveNode) {
+      cols.push({
+        title: t('pages.inbounds.node'),
+        key: 'node',
+        align: 'center',
+        width: 60,
+        ...sorterFor('node'),
+        render: (_, record) => {
+          if (record.nodeId == null) {
+            return <Tag color="default">{t('pages.inbounds.localPanel')}</Tag>;
+          }
+          const node = nodesById.get(record.nodeId);
+          if (!node) {
+            return <Tag color="orange">node #{record.nodeId}</Tag>;
+          }
+          return (
+            <Tag color={node.status === 'online' ? 'blue' : 'red'}>{node.name}</Tag>
+          );
+        },
+      });
+    }
+
+    cols.push(
+      {
+        title: t('pages.inbounds.port'),
+        dataIndex: 'port',
+        key: 'port',
+        align: 'center',
+        width: 40,
+        ...sorterFor('port'),
+      },
+      {
+        title: t('pages.inbounds.protocol'),
+        key: 'protocol',
+        align: 'left',
+        width: 130,
+        ...sorterFor('protocol'),
+        render: (_, record) => {
+          const tags: ReactElement[] = [<Tag key="p" color="purple">{record.protocol}</Tag>];
+          if (record.isWireguard || record.isHysteria) {
+            tags.push(<Tag key="n" color="green">UDP</Tag>);
+          } else if (record.isSS) {
+            const stream = readStreamHints(record.streamSettings);
+            tags.push(<Tag key="n" color="green">{shadowsocksNetworkLabel(record.settings)}</Tag>);
+            if (stream.isTls) tags.push(<Tag key="tls" color="blue">TLS</Tag>);
+          } else if (record.isTunnel) {
+            tags.push(<Tag key="n" color="green">{tunnelNetworkLabel(record.settings)}</Tag>);
+          } else if (record.isMixed) {
+            tags.push(<Tag key="n" color="green">{mixedNetworkLabel(record.settings)}</Tag>);
+          } else if (record.isVMess || record.isVLess || record.isTrojan) {
+            const stream = readStreamHints(record.streamSettings);
+            tags.push(<Tag key="n" color="green">{networkLabel(stream.network)}</Tag>);
+            const l4 = networkL4(stream.network);
+            if (l4) tags.push(<Tag key="l4" color="green">{l4}</Tag>);
+            if (stream.isTls) tags.push(<Tag key="tls" color="blue">TLS</Tag>);
+            if (stream.isReality) tags.push(<Tag key="reality" color="blue">Reality</Tag>);
+          }
+          return <div className="protocol-tags">{tags}</div>;
+        },
+      },
+      {
+        title: t('clients'),
+        key: 'clients',
+        align: 'left',
+        width: 50,
+        ...sorterFor('clients'),
+        render: (_, record) => {
+          const cc = clientCount[record.id];
+          if (!cc) return null;
+          return (
+            <>
+              <Tag color="green" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>
+                {cc.clients}
+              </Tag>
+              {cc.deactive.length > 0 && (
+                <Popover
+                  title={t('disabled')}
+                  content={(
+                    <div className="client-email-list">
+                      {cc.deactive.map((e) => <div key={e}>{e}</div>)}
+                    </div>
+                  )}
+                >
+                  <Tag className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.deactive.length}</Tag>
+                </Popover>
+              )}
+              {cc.depleted.length > 0 && (
+                <Popover
+                  title={t('depleted')}
+                  content={(
+                    <div className="client-email-list">
+                      {cc.depleted.map((e) => <div key={e}>{e}</div>)}
+                    </div>
+                  )}
+                >
+                  <Tag color="red" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.depleted.length}</Tag>
+                </Popover>
+              )}
+              {cc.expiring.length > 0 && (
+                <Popover
+                  title={t('depletingSoon')}
+                  content={(
+                    <div className="client-email-list">
+                      {cc.expiring.map((e) => <div key={e}>{e}</div>)}
+                    </div>
+                  )}
+                >
+                  <Tag color="orange" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.expiring.length}</Tag>
+                </Popover>
+              )}
+              {cc.online.length > 0 && (
+                <Popover
+                  title={t('online')}
+                  content={(
+                    <div className="client-email-list">
+                      {cc.online.map((e) => <div key={e}>{e}</div>)}
+                    </div>
+                  )}
+                >
+                  <Tag color="blue" className="client-count-tag" style={{ margin: 0, padding: '0 2px' }}>{cc.online.length}</Tag>
+                </Popover>
+              )}
+            </>
+          );
+        },
+      },
+      {
+        title: t('pages.inbounds.traffic'),
+        key: 'traffic',
+        align: 'center',
+        width: 90,
+        ...sorterFor('traffic'),
+        render: (_, record) => (
+          <Popover
+            content={(
+              <table cellPadding={2}>
+                <tbody>
+                  <tr>
+                    <td>↑ {SizeFormatter.sizeFormat(record.up)}</td>
+                    <td>↓ {SizeFormatter.sizeFormat(record.down)}</td>
+                  </tr>
+                  {record.total > 0 && record.up + record.down < record.total && (
+                    <tr>
+                      <td>{t('remained')}</td>
+                      <td>{SizeFormatter.sizeFormat(record.total - record.up - record.down)}</td>
+                    </tr>
+                  )}
+                </tbody>
+              </table>
+            )}
+          >
+            <Tag color={ColorUtils.usageColor(record.up + record.down, trafficDiff, record.total)}>
+              {SizeFormatter.sizeFormat(record.up + record.down)} /
+              {' '}
+              {record.total > 0 ? SizeFormatter.sizeFormat(record.total) : <InfinityIcon />}
+            </Tag>
+          </Popover>
+        ),
+      },
+      {
+        title: t('pages.inbounds.expireDate'),
+        key: 'expiryTime',
+        align: 'center',
+        width: 40,
+        ...sorterFor('expiryTime'),
+        render: (_, record) => {
+          if (record.expiryTime > 0) {
+            return (
+              <Popover content={IntlUtil.formatDate(record.expiryTime, datepicker)}>
+                <Tag color={ColorUtils.usageColor(Date.now(), expireDiff, record._expiryTime)} style={{ minWidth: 50 }}>
+                  {IntlUtil.formatRelativeTime(record.expiryTime)}
+                </Tag>
+              </Popover>
+            );
+          }
+          return <Tag color="purple"><InfinityIcon /></Tag>;
+        },
+      },
+    );
+
+    return cols;
+  }, [t, hasAnyRemark, hasActiveNode, nodesById, clientCount, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable, sorterFor]);
+}

+ 1 - 1
frontend/src/pages/inbounds/QrCodeModal.tsx → frontend/src/pages/inbounds/qr/QrCodeModal.tsx

@@ -12,7 +12,7 @@ import {
 } from '@/lib/xray/inbound-link';
 } from '@/lib/xray/inbound-link';
 import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
 import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
 import QrPanel from './QrPanel';
 import QrPanel from './QrPanel';
-import type { SubSettings } from './useInbounds';
+import type { SubSettings } from '../useInbounds';
 
 
 interface ClientSetting {
 interface ClientSetting {
   email?: string;
   email?: string;

+ 0 - 0
frontend/src/pages/inbounds/QrPanel.css → frontend/src/pages/inbounds/qr/QrPanel.css


+ 0 - 0
frontend/src/pages/inbounds/QrPanel.tsx → frontend/src/pages/inbounds/qr/QrPanel.tsx


+ 2 - 0
frontend/src/pages/inbounds/qr/index.ts

@@ -0,0 +1,2 @@
+export { default as QrPanel } from './QrPanel';
+export { default as QrCodeModal } from './QrCodeModal';

+ 3 - 3
frontend/src/pages/index/IndexPage.tsx

@@ -39,13 +39,13 @@ import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager }
 import { useTheme } from '@/hooks/useTheme';
 import { useTheme } from '@/hooks/useTheme';
 import { useStatusQuery } from '@/api/queries/useStatusQuery';
 import { useStatusQuery } from '@/api/queries/useStatusQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
-import AppSidebar from '@/components/AppSidebar';
-import LazyMount from '@/components/LazyMount';
+import AppSidebar from '@/layouts/AppSidebar';
+import { LazyMount } from '@/components/utility';
 import { setMessageInstance } from '@/utils/messageBus';
 import { setMessageInstance } from '@/utils/messageBus';
 import StatusCard from './StatusCard';
 import StatusCard from './StatusCard';
 import XrayStatusCard from './XrayStatusCard';
 import XrayStatusCard from './XrayStatusCard';
 import type { PanelUpdateInfo } from './PanelUpdateModal';
 import type { PanelUpdateInfo } from './PanelUpdateModal';
-const JsonEditor = lazy(() => import('@/components/JsonEditor'));
+const JsonEditor = lazy(() => import('@/components/form/JsonEditor'));
 const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
 const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
 const LogModal = lazy(() => import('./LogModal'));
 const LogModal = lazy(() => import('./LogModal'));
 const BackupModal = lazy(() => import('./BackupModal'));
 const BackupModal = lazy(() => import('./BackupModal'));

+ 1 - 1
frontend/src/pages/index/SystemHistoryModal.tsx

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 import { Modal, Select, Tabs } from 'antd';
 import { Modal, Select, Tabs } from 'antd';
 
 
 import { HttpUtil, SizeFormatter } from '@/utils';
 import { HttpUtil, SizeFormatter } from '@/utils';
-import Sparkline from '@/components/Sparkline';
+import { Sparkline } from '@/components/viz';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import type { Status } from '@/models/status';
 import type { Status } from '@/models/status';
 import './SystemHistoryModal.css';
 import './SystemHistoryModal.css';

+ 1 - 1
frontend/src/pages/index/XrayMetricsModal.tsx

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 import { Alert, Modal, Select, Tabs, Tag } from 'antd';
 import { Alert, Modal, Select, Tabs, Tag } from 'antd';
 
 
 import { HttpUtil, Msg, SizeFormatter } from '@/utils';
 import { HttpUtil, Msg, SizeFormatter } from '@/utils';
-import Sparkline from '@/components/Sparkline';
+import { Sparkline } from '@/components/viz';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import './XrayMetricsModal.css';
 import './XrayMetricsModal.css';
 
 

+ 1 - 1
frontend/src/pages/nodes/NodeHistoryPanel.tsx

@@ -1,7 +1,7 @@
 import { useEffect, useRef, useState } from 'react';
 import { useEffect, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { HttpUtil } from '@/utils';
 import { HttpUtil } from '@/utils';
-import Sparkline from '@/components/Sparkline';
+import { Sparkline } from '@/components/viz';
 import './NodeHistoryPanel.css';
 import './NodeHistoryPanel.css';
 
 
 interface NodeRef {
 interface NodeRef {

+ 1 - 1
frontend/src/pages/nodes/NodesPage.tsx

@@ -13,7 +13,7 @@ import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useNodesQuery } from '@/api/queries/useNodesQuery';
 import { useNodesQuery } from '@/api/queries/useNodesQuery';
 import type { NodeRecord } from '@/api/queries/useNodesQuery';
 import type { NodeRecord } from '@/api/queries/useNodesQuery';
 import { useNodeMutations } from '@/api/queries/useNodeMutations';
 import { useNodeMutations } from '@/api/queries/useNodeMutations';
-import AppSidebar from '@/components/AppSidebar';
+import AppSidebar from '@/layouts/AppSidebar';
 import NodeList from './NodeList';
 import NodeList from './NodeList';
 import NodeFormModal from './NodeFormModal';
 import NodeFormModal from './NodeFormModal';
 import { setMessageInstance } from '@/utils/messageBus';
 import { setMessageInstance } from '@/utils/messageBus';

+ 1 - 1
frontend/src/pages/settings/GeneralTab.tsx

@@ -10,7 +10,7 @@ import {
 } from 'antd';
 } from 'antd';
 import type { AllSetting } from '@/models/setting';
 import type { AllSetting } from '@/models/setting';
 import { HttpUtil, LanguageManager } from '@/utils';
 import { HttpUtil, LanguageManager } from '@/utils';
-import SettingListItem from '@/components/SettingListItem';
+import { SettingListItem } from '@/components/ui';
 
 
 interface ApiMsg<T = unknown> {
 interface ApiMsg<T = unknown> {
   success?: boolean;
   success?: boolean;

+ 1 - 1
frontend/src/pages/settings/SecurityTab.tsx

@@ -14,7 +14,7 @@ import {
 } from 'antd';
 } from 'antd';
 import { ClipboardManager, HttpUtil, RandomUtil } from '@/utils';
 import { ClipboardManager, HttpUtil, RandomUtil } from '@/utils';
 import type { AllSetting } from '@/models/setting';
 import type { AllSetting } from '@/models/setting';
-import SettingListItem from '@/components/SettingListItem';
+import { SettingListItem } from '@/components/ui';
 import TwoFactorModal from './TwoFactorModal';
 import TwoFactorModal from './TwoFactorModal';
 import './SecurityTab.css';
 import './SecurityTab.css';
 
 

+ 1 - 1
frontend/src/pages/settings/SettingsPage.tsx

@@ -30,7 +30,7 @@ import { useTheme } from '@/hooks/useTheme';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
 import { useAllSettings } from '@/api/queries/useAllSettings';
 import { useAllSettings } from '@/api/queries/useAllSettings';
 import { AllSettingSchema } from '@/schemas/setting';
 import { AllSettingSchema } from '@/schemas/setting';
-import AppSidebar from '@/components/AppSidebar';
+import AppSidebar from '@/layouts/AppSidebar';
 import GeneralTab from './GeneralTab';
 import GeneralTab from './GeneralTab';
 import SecurityTab from './SecurityTab';
 import SecurityTab from './SecurityTab';
 import TelegramTab from './TelegramTab';
 import TelegramTab from './TelegramTab';

+ 1 - 1
frontend/src/pages/settings/SubscriptionFormatsTab.tsx

@@ -10,7 +10,7 @@ import {
   Switch,
   Switch,
 } from 'antd';
 } from 'antd';
 import type { AllSetting } from '@/models/setting';
 import type { AllSetting } from '@/models/setting';
-import SettingListItem from '@/components/SettingListItem';
+import { SettingListItem } from '@/components/ui';
 import './SubscriptionFormatsTab.css';
 import './SubscriptionFormatsTab.css';
 
 
 interface SubscriptionFormatsTabProps {
 interface SubscriptionFormatsTabProps {

+ 1 - 1
frontend/src/pages/settings/SubscriptionGeneralTab.tsx

@@ -1,7 +1,7 @@
 import { Collapse, Divider, Input, InputNumber, Switch } from 'antd';
 import { Collapse, Divider, Input, InputNumber, Switch } from 'antd';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import type { AllSetting } from '@/models/setting';
 import type { AllSetting } from '@/models/setting';
-import SettingListItem from '@/components/SettingListItem';
+import { SettingListItem } from '@/components/ui';
 
 
 interface SubscriptionGeneralTabProps {
 interface SubscriptionGeneralTabProps {
   allSetting: AllSetting;
   allSetting: AllSetting;

+ 1 - 1
frontend/src/pages/settings/TelegramTab.tsx

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 import { Collapse, Input, InputNumber, Select, Switch } from 'antd';
 import { Collapse, Input, InputNumber, Select, Switch } from 'antd';
 import { LanguageManager } from '@/utils';
 import { LanguageManager } from '@/utils';
 import type { AllSetting } from '@/models/setting';
 import type { AllSetting } from '@/models/setting';
-import SettingListItem from '@/components/SettingListItem';
+import { SettingListItem } from '@/components/ui';
 
 
 interface TelegramTabProps {
 interface TelegramTabProps {
   allSetting: AllSetting;
   allSetting: AllSetting;

+ 0 - 2238
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -1,2238 +0,0 @@
-import { useEffect, useMemo, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import {
-  Button,
-  Form,
-  Input,
-  InputNumber,
-  Modal,
-  Radio,
-  Select,
-  Space,
-  Switch,
-  Tabs,
-  message,
-} from 'antd';
-import { DeleteOutlined, MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
-
-import FinalMaskForm from '@/components/FinalMaskForm';
-import HeaderMapEditor from '@/components/HeaderMapEditor';
-import HysteriaMasqueradeForm from '@/components/HysteriaMasqueradeForm';
-import InputAddon from '@/components/InputAddon';
-import JsonEditor from '@/components/JsonEditor';
-import { Wireguard } from '@/utils';
-import {
-  XMUX_DEFAULTS,
-  formValuesToWirePayload,
-  rawOutboundToFormValues,
-} from '@/lib/xray/outbound-form-adapter';
-import { parseOutboundLink } from '@/lib/xray/outbound-link-parser';
-import {
-  OutboundFormBaseSchema,
-  ShadowsocksOutboundFormSettingsSchema,
-  TrojanOutboundFormSettingsSchema,
-  VlessOutboundFormSettingsSchema,
-  VmessOutboundFormSettingsSchema,
-  type OutboundFormValues,
-} from '@/schemas/forms/outbound-form';
-import {
-  ALPN_OPTION,
-  Address_Port_Strategy,
-  DNSRuleActions,
-  DOMAIN_STRATEGY_OPTION,
-  MODE_OPTION,
-  OutboundDomainStrategies,
-  OutboundProtocols as Protocols,
-  SNIFFING_OPTION,
-  TCP_CONGESTION_OPTION,
-  TLS_FLOW_CONTROL,
-  USERS_SECURITY,
-  UTLS_FINGERPRINT,
-  WireguardDomainStrategy,
-} from '@/schemas/primitives';
-import {
-  HappyEyeballsSchema,
-  SockoptStreamSettingsSchema,
-} from '@/schemas/protocols/stream/sockopt';
-import {
-  canEnableReality,
-  canEnableStream,
-  canEnableTls,
-  canEnableTlsFlow,
-} from '@/lib/xray/protocol-capabilities';
-import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
-import { antdRule } from '@/utils/zodForm';
-import './OutboundFormModal.css';
-
-// Pattern A rewrite of OutboundFormModal. Built as a sibling `.new.tsx`
-// file so the build stays green section-by-section. The atomic swap at
-// the end of the rewrite replaces the legacy file in one commit
-// (per Core Decision 7 in the migration spec).
-
-interface OutboundFormModalProps {
-  open: boolean;
-  outbound: Record<string, unknown> | null;
-  existingTags: string[];
-  onClose: () => void;
-  onConfirm: (outbound: Record<string, unknown>) => void;
-}
-
-const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
-const SECURITY_OPTIONS = Object.values(USERS_SECURITY).map((v) => ({ value: v, label: v }));
-const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v }));
-const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v }));
-const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v }));
-const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT).map((v) => ({ value: v, label: v }));
-const ALPN_OPTIONS = Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v }));
-const ADDRESS_PORT_STRATEGY_OPTIONS = Object.values(Address_Port_Strategy).map((v) => ({
-  value: v,
-  label: v,
-}));
-
-// canEnableMux mirrors the adapter's helper but lives here so the modal
-// can show/hide the Mux section without going through the adapter.
-const MUX_PROTOCOLS = new Set<string>(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
-function isMuxAllowed(protocol: string, flow: string, network: string): boolean {
-  if (!MUX_PROTOCOLS.has(protocol)) return false;
-  if (protocol === 'vless' && flow) return false;
-  if (network === 'xhttp') return false;
-  return true;
-}
-
-const NETWORK_OPTIONS: { value: string; label: string }[] = [
-  { value: 'tcp', label: 'RAW' },
-  { value: 'kcp', label: 'mKCP' },
-  { value: 'ws', label: 'WebSocket' },
-  { value: 'grpc', label: 'gRPC' },
-  { value: 'httpupgrade', label: 'HTTPUpgrade' },
-  { value: 'xhttp', label: 'XHTTP' },
-];
-
-// The hysteria protocol is locked to its own QUIC transport: the selector
-// shows only this option when the parent protocol is hysteria.
-const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' };
-
-// Per-network bootstrap. Mirrors the legacy class constructors so the
-// initial state for each transport matches what xray-core expects.
-function newStreamSlice(network: string): Record<string, unknown> {
-  switch (network) {
-    case 'tcp':
-      return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
-    case 'kcp':
-      return {
-        network: 'kcp',
-        kcpSettings: {
-          mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20,
-          cwndMultiplier: 1, maxSendingWindow: 2097152,
-        },
-      };
-    case 'ws':
-      return {
-        network: 'ws',
-        wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 },
-      };
-    case 'grpc':
-      return {
-        network: 'grpc',
-        grpcSettings: { serviceName: '', authority: '', multiMode: false },
-      };
-    case 'httpupgrade':
-      return {
-        network: 'httpupgrade',
-        httpupgradeSettings: { path: '/', host: '', headers: {} },
-      };
-    case 'xhttp':
-      return {
-        network: 'xhttp',
-        xhttpSettings: {
-          path: '/', host: '', mode: '', headers: [],
-          xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
-        },
-      };
-    case 'hysteria':
-      return {
-        network: 'hysteria',
-        hysteriaSettings: {
-          version: 2,
-          auth: '',
-          udpIdleTimeout: 60,
-        },
-      };
-    default:
-      return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
-  }
-}
-
-// Hysteria2 always rides its own QUIC transport with TLS — the panel never
-// offers another transport or 'none' security for it.
-function hysteriaStreamSlice(): Record<string, unknown> {
-  return {
-    ...newStreamSlice('hysteria'),
-    security: 'tls',
-    tlsSettings: {
-      serverName: '', alpn: ['h3'], fingerprint: '',
-      echConfigList: '', verifyPeerCertByName: '', pinnedPeerCertSha256: '',
-    },
-  };
-}
-
-// Protocols whose form schema carries a flat connect target — these all
-// get the shared "server" sub-block (address + port) at the top of the
-// protocol section. Wireguard has an address but no port. DNS/freedom/
-// blackhole/loopback have no connect target.
-const SERVER_PROTOCOLS = new Set<string>([
-  'vmess', 'vless', 'trojan', 'shadowsocks', 'socks', 'http', 'hysteria',
-]);
-
-function buildAddModeValues(): OutboundFormValues {
-  return rawOutboundToFormValues({});
-}
-
-export default function OutboundFormModal({
-  open,
-  outbound: outboundProp,
-  existingTags,
-  onClose,
-  onConfirm,
-}: OutboundFormModalProps) {
-  const { t } = useTranslation();
-  const [messageApi, messageContextHolder] = message.useMessage();
-  const [form] = Form.useForm<OutboundFormValues>();
-  const [activeKey, setActiveKey] = useState('1');
-  const [jsonText, setJsonText] = useState('');
-  const [jsonDirty, setJsonDirty] = useState(false);
-  const [linkInput, setLinkInput] = useState('');
-
-  // Parse a share link (vmess:// / vless:// / trojan:// / ss:// /
-  // hysteria2:// / wireguard://) and replace form state with the result.
-  // The current tag is preserved when the parsed link doesn't carry one.
-  function importLink() {
-    const link = linkInput.trim();
-    if (!link) return;
-    const parsed = parseOutboundLink(link);
-    if (!parsed) {
-      messageApi.error('Wrong Link!');
-      return;
-    }
-    const currentTag = form.getFieldValue('tag') as string | undefined;
-    if (!parsed.tag && currentTag) parsed.tag = currentTag;
-    const next = rawOutboundToFormValues(parsed);
-    form.resetFields();
-    form.setFieldsValue(next);
-    setJsonText(JSON.stringify(formValuesToWirePayload(next), null, 2));
-    setJsonDirty(false);
-    setLinkInput('');
-    messageApi.success('Link imported successfully');
-    switchTab('1');
-  }
-
-  const isEdit = outboundProp != null;
-  const title = isEdit
-    ? `${t('edit')} ${t('pages.xray.Outbounds')}`
-    : `+ ${t('pages.xray.Outbounds')}`;
-  const okText = isEdit ? t('pages.clients.submitEdit') : t('create');
-
-  useEffect(() => {
-    if (!open) return;
-    const initial = outboundProp
-      ? rawOutboundToFormValues(outboundProp)
-      : buildAddModeValues();
-    form.resetFields();
-    form.setFieldsValue(initial);
-    setActiveKey('1');
-    setJsonText(JSON.stringify(formValuesToWirePayload(initial), null, 2));
-    setJsonDirty(false);
-  }, [open, outboundProp, form]);
-
-  const tag = Form.useWatch('tag', form) ?? '';
-  const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
-  const network = (Form.useWatch(['streamSettings', 'network'], { form, preserve: true }) ?? '') as string;
-  const security = (Form.useWatch(['streamSettings', 'security'], { form, preserve: true }) ?? 'none') as string;
-  const streamAllowed = canEnableStream({ protocol });
-  const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
-  const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
-  const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
-
-  useEffect(() => {
-    if (!streamAllowed) return;
-    if (network) return;
-    form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [streamAllowed, network]);
-
-  useEffect(() => {
-    if (protocol !== 'hysteria') return;
-    if (network === 'hysteria' && security === 'tls') return;
-    const existing = (form.getFieldValue('streamSettings') ?? {}) as Record<string, unknown>;
-    const slice = hysteriaStreamSlice();
-    if (existing.hysteriaSettings) slice.hysteriaSettings = existing.hysteriaSettings;
-    if (existing.tlsSettings) slice.tlsSettings = existing.tlsSettings;
-    form.setFieldValue('streamSettings', slice);
-  }, [protocol, network, security]);
-
-  const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined;
-  useEffect(() => {
-    if (protocol !== 'wireguard') return;
-    const sk = (wgSecretKey ?? '').trim();
-    if (!sk) {
-      form.setFieldValue(['settings', 'pubKey'], '');
-      return;
-    }
-    try {
-      const { publicKey } = Wireguard.generateKeypair(sk);
-      form.setFieldValue(['settings', 'pubKey'], publicKey);
-    } catch {
-      form.setFieldValue(['settings', 'pubKey'], '');
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [protocol, wgSecretKey]);
-
-  function onValuesChange(changed: Partial<OutboundFormValues>) {
-    if ('protocol' in changed && changed.protocol) {
-      const next = rawOutboundToFormValues({ protocol: changed.protocol });
-      form.setFieldValue('settings', next.settings);
-      if (changed.protocol === 'hysteria') {
-        form.setFieldValue('streamSettings', hysteriaStreamSlice());
-      } else if ((form.getFieldValue(['streamSettings', 'network']) ?? '') === 'hysteria') {
-        form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
-      }
-    }
-  }
-
-  function onSecurityChange(next: string) {
-    const stream = form.getFieldValue('streamSettings') ?? {};
-    const cleaned = { ...stream } as Record<string, unknown>;
-    delete cleaned.tlsSettings;
-    delete cleaned.realitySettings;
-    if (next === 'tls') {
-      cleaned.tlsSettings = {
-        serverName: '',
-        alpn: [],
-        fingerprint: '',
-        echConfigList: '',
-        verifyPeerCertByName: '',
-        pinnedPeerCertSha256: '',
-      };
-    } else if (next === 'reality') {
-      cleaned.realitySettings = {
-        publicKey: '',
-        fingerprint: 'chrome',
-        serverName: '',
-        shortId: '',
-        spiderX: '',
-        mldsa65Verify: '',
-      };
-    }
-    cleaned.security = next;
-    form.setFieldValue('streamSettings', cleaned);
-  }
-
-  // Network change cascade: swap the per-network sub-key (tcpSettings,
-  // wsSettings, etc.) so the DU branch matches. Preserve security if
-  // the new network supports it, otherwise force back to 'none'.
-  function onNetworkChange(next: string) {
-    if (next === 'hysteria') {
-      form.setFieldValue('streamSettings', hysteriaStreamSlice());
-      return;
-    }
-    const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none';
-    const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
-    const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
-    const newSecurity =
-      currentSecurity === 'tls' && !stillAllowed
-        ? 'none'
-        : currentSecurity === 'reality' && !stillReality
-          ? 'none'
-          : currentSecurity;
-    form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity });
-  }
-
-  function onXmuxToggle(checked: boolean) {
-    if (!checked) return;
-    const existing = form.getFieldValue(['streamSettings', 'xhttpSettings', 'xmux']);
-    const hasValues = existing && typeof existing === 'object' && Object.keys(existing).length > 0;
-    if (hasValues) return;
-    form.setFieldValue(['streamSettings', 'xhttpSettings', 'xmux'], { ...XMUX_DEFAULTS });
-  }
-
-  const duplicateTag = useMemo(() => {
-    const myTag = tag.trim();
-    if (!myTag) return false;
-    if (isEdit && (outboundProp?.tag as string | undefined) === myTag) return false;
-    return (existingTags || []).includes(myTag);
-  }, [tag, existingTags, isEdit, outboundProp]);
-
-  // Bridge form ↔ JSON tab: when leaving the JSON tab back to Basic, push
-  // any edits into form state. When entering JSON tab, snapshot current
-  // form values so the user sees the live shape.
-  function applyJsonToForm(): boolean {
-    if (!jsonDirty) return true;
-    const raw = jsonText.trim();
-    if (!raw) return true;
-    let parsed: Record<string, unknown>;
-    try {
-      parsed = JSON.parse(raw) as Record<string, unknown>;
-    } catch (e) {
-      messageApi.error(`JSON: ${(e as Error).message}`);
-      return false;
-    }
-    const next = rawOutboundToFormValues(parsed);
-    form.resetFields();
-    form.setFieldsValue(next);
-    setJsonDirty(false);
-    return true;
-  }
-
-  function switchTab(key: string) {
-    if (typeof document !== 'undefined') {
-      (document.activeElement as HTMLElement | null)?.blur?.();
-    }
-    setActiveKey(key);
-  }
-
-  function onTabChange(key: string) {
-    if (key === '2') {
-      const values = form.getFieldsValue(true) as OutboundFormValues;
-      setJsonText(JSON.stringify(formValuesToWirePayload(values), null, 2));
-      setJsonDirty(false);
-      switchTab(key);
-      return;
-    }
-    if (key === '1' && activeKey === '2') {
-      if (!applyJsonToForm()) return;
-    }
-    switchTab(key);
-  }
-
-  async function onOk() {
-    let values: OutboundFormValues;
-    if (activeKey === '2') {
-      const raw = jsonText.trim();
-      if (!raw) return;
-      let parsed: Record<string, unknown>;
-      try {
-        parsed = JSON.parse(raw) as Record<string, unknown>;
-      } catch (e) {
-        messageApi.error(`JSON: ${(e as Error).message}`);
-        return;
-      }
-      values = rawOutboundToFormValues(parsed);
-      form.resetFields();
-      form.setFieldsValue(values);
-      setJsonDirty(false);
-    } else {
-      try {
-        await form.validateFields();
-      } catch {
-        return;
-      }
-      values = form.getFieldsValue(true) as OutboundFormValues;
-    }
-    const tagValue = (values.tag ?? '').trim();
-    if (!tagValue) {
-      messageApi.error(t('pages.xray.outboundForm.tagRequired'));
-      return;
-    }
-    const isDuplicateTag = (existingTags || []).includes(tagValue)
-      && !(isEdit && (outboundProp?.tag as string | undefined) === tagValue);
-    if (isDuplicateTag) {
-      messageApi.error('Tag already used by another outbound');
-      return;
-    }
-    onConfirm(formValuesToWirePayload(values));
-  }
-
-  return (
-    <>
-      {messageContextHolder}
-      <Modal
-        open={open}
-        title={title}
-        okText={okText}
-        cancelText={t('close')}
-        mask={{ closable: false }}
-        width={780}
-        onOk={onOk}
-        onCancel={onClose}
-        destroyOnHidden
-      >
-        <Form
-          form={form}
-          colon={false}
-          labelCol={{ md: { span: 8 } }}
-          wrapperCol={{ md: { span: 14 } }}
-          onValuesChange={onValuesChange}
-        >
-          <Tabs
-            activeKey={activeKey}
-            onChange={onTabChange}
-            items={[
-              {
-                key: '1',
-                label: t('pages.xray.basicTemplate'),
-                children: (
-                  <>
-                    <Form.Item
-                      label={t('protocol')}
-                      name="protocol"
-                      rules={[antdRule(OutboundFormBaseSchema.shape.tag, t)]}
-                    >
-                      <Select options={PROTOCOL_OPTIONS} />
-                    </Form.Item>
-
-                    <Form.Item
-                      label={t('pages.xray.outbound.tag')}
-                      name="tag"
-                      validateStatus={duplicateTag ? 'warning' : undefined}
-                      help={duplicateTag ? t('pages.xray.outboundForm.tagDuplicate') : undefined}
-                      rules={[
-                        { required: true, message: t('pages.xray.outboundForm.tagRequired') },
-                      ]}
-                    >
-                      <Input placeholder={t('pages.xray.outboundForm.tagPlaceholder')} />
-                    </Form.Item>
-
-                    <Form.Item label={t('pages.xray.outbound.sendThrough')} name="sendThrough">
-                      <Input placeholder={t('pages.xray.outboundForm.localIpPlaceholder')} />
-                    </Form.Item>
-
-                    {/* Shared connect target (address + port) for protocols
-                        whose form schema carries them flat at settings root.
-                        Hidden for freedom/blackhole/dns/loopback/wireguard. */}
-                    {SERVER_PROTOCOLS.has(protocol) && (
-                      <>
-                        <Form.Item
-                          label={t('pages.inbounds.address')}
-                          name={['settings', 'address']}
-                          rules={[{ required: true, message: t('pages.xray.outboundForm.addressRequired') }]}
-                        >
-                          <Input />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.inbounds.port')}
-                          name={['settings', 'port']}
-                          rules={[{ required: true, message: t('pages.xray.outboundForm.portRequired') }]}
-                        >
-                          <InputNumber min={1} max={65535} style={{ width: '100%' }} />
-                        </Form.Item>
-                      </>
-                    )}
-
-                    {(protocol === 'vmess' || protocol === 'vless') && (
-                      <Form.Item
-                        label="ID"
-                        name={['settings', 'id']}
-                        rules={[antdRule(VmessOutboundFormSettingsSchema.shape.id, t)]}
-                      >
-                        <Input placeholder="UUID" />
-                      </Form.Item>
-                    )}
-                    {protocol === 'vmess' && (
-                      <Form.Item
-                        label={t('security')}
-                        name={['settings', 'security']}
-                        rules={[antdRule(VmessOutboundFormSettingsSchema.shape.security, t)]}
-                      >
-                        <Select options={SECURITY_OPTIONS} />
-                      </Form.Item>
-                    )}
-                    {protocol === 'vless' && (
-                      <>
-                        <Form.Item
-                          label={t('encryption')}
-                          name={['settings', 'encryption']}
-                          rules={[antdRule(VlessOutboundFormSettingsSchema.shape.encryption, t)]}
-                        >
-                          <Input />
-                        </Form.Item>
-                        <Form.Item label={t('pages.clients.reverseTag')} name={['settings', 'reverseTag']}>
-                          <Input placeholder={t('pages.xray.outboundForm.optional')} />
-                        </Form.Item>
-                      </>
-                    )}
-
-                    {(protocol === 'trojan' || protocol === 'shadowsocks') && (
-                      <Form.Item
-                        label={t('password')}
-                        name={['settings', 'password']}
-                        rules={[
-                          antdRule(
-                            protocol === 'trojan'
-                              ? TrojanOutboundFormSettingsSchema.shape.password
-                              : ShadowsocksOutboundFormSettingsSchema.shape.password,
-                            t,
-                          ),
-                        ]}
-                      >
-                        <Input />
-                      </Form.Item>
-                    )}
-
-                    {protocol === 'shadowsocks' && (
-                      <>
-                        <Form.Item
-                          label={t('encryption')}
-                          name={['settings', 'method']}
-                          rules={[antdRule(SSMethodSchema, t)]}
-                        >
-                          <Select options={SS_METHOD_OPTIONS} />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.xray.outboundForm.udpOverTcp')}
-                          name={['settings', 'uot']}
-                          valuePropName="checked"
-                        >
-                          <Switch />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.outboundForm.uotVersion')} name={['settings', 'UoTVersion']}>
-                          <InputNumber min={1} max={2} />
-                        </Form.Item>
-                      </>
-                    )}
-
-                    {(protocol === 'socks' || protocol === 'http') && (
-                      <>
-                        <Form.Item label={t('username')} name={['settings', 'user']}>
-                          <Input />
-                        </Form.Item>
-                        <Form.Item label={t('password')} name={['settings', 'pass']}>
-                          <Input />
-                        </Form.Item>
-                      </>
-                    )}
-
-                    {protocol === 'loopback' && (
-                      <Form.Item label={t('pages.xray.outboundForm.inboundTag')} name={['settings', 'inboundTag']}>
-                        <Input placeholder={t('pages.xray.outboundForm.inboundTagPlaceholder')} />
-                      </Form.Item>
-                    )}
-
-                    {protocol === 'blackhole' && (
-                      <Form.Item label={t('pages.xray.outboundForm.responseType')} name={['settings', 'type']}>
-                        <Select
-                          options={[
-                            { value: '', label: '(empty)' },
-                            { value: 'none', label: 'none' },
-                            { value: 'http', label: 'http' },
-                          ]}
-                        />
-                      </Form.Item>
-                    )}
-
-                    {protocol === 'dns' && (
-                      <>
-                        <Form.Item label={t('pages.xray.outboundForm.rewriteNetwork')} name={['settings', 'rewriteNetwork']}>
-                          <Select
-                            allowClear
-                            placeholder={t('pages.xray.outboundForm.unchanged')}
-                            options={[
-                              { value: 'udp', label: 'udp' },
-                              { value: 'tcp', label: 'tcp' },
-                            ]}
-                          />
-                        </Form.Item>
-                        <Form.Item label={t('pages.inbounds.form.rewriteAddress')} name={['settings', 'rewriteAddress']}>
-                          <Input placeholder={t('pages.xray.outboundForm.unchangedAddress')} />
-                        </Form.Item>
-                        <Form.Item label={t('pages.inbounds.form.rewritePort')} name={['settings', 'rewritePort']}>
-                          <InputNumber min={0} max={65535} style={{ width: '100%' }} />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.tun.userLevel')} name={['settings', 'userLevel']}>
-                          <InputNumber min={0} style={{ width: '100%' }} />
-                        </Form.Item>
-                        <Form.List name={['settings', 'rules']}>
-                          {(fields, { add, remove }) => (
-                            <>
-                              <Form.Item label={t('pages.xray.outboundForm.rules')}>
-                                <Button
-                                  size="small"
-                                  type="primary"
-                                  icon={<PlusOutlined />}
-                                  onClick={() => add({ action: 'direct', qtype: '', domain: '' })}
-                                />
-                              </Form.Item>
-                              {fields.map((field, index) => (
-                                <div key={field.key}>
-                                  <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
-                                    <div className="item-heading">
-                                      <span>{t('pages.xray.outboundForm.ruleN', { n: index + 1 })}</span>
-                                      <DeleteOutlined
-                                        className="danger-icon"
-                                        onClick={() => remove(field.name)}
-                                      />
-                                    </div>
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.xray.outboundForm.action')} name={[field.name, 'action']}>
-                                    <Select
-                                      options={DNSRuleActions.map((a) => ({ value: a, label: a }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item label="QType" name={[field.name, 'qtype']}>
-                                    <Input placeholder="1,3,23-24" />
-                                  </Form.Item>
-                                  <Form.Item label={t('domainName')} name={[field.name, 'domain']}>
-                                    <Input placeholder="domain:example.com" />
-                                  </Form.Item>
-                                </div>
-                              ))}
-                            </>
-                          )}
-                        </Form.List>
-                      </>
-                    )}
-
-                    {protocol === 'freedom' && (
-                      <>
-                        <Form.Item label={t('pages.xray.balancer.balancerStrategy')} name={['settings', 'domainStrategy']}>
-                          <Select
-                            options={[
-                              { value: '', label: `(${t('none')})` },
-                              ...OutboundDomainStrategies.map((s) => ({ value: s, label: s })),
-                            ]}
-                          />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.outboundForm.redirect')} name={['settings', 'redirect']}>
-                          <Input />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.outboundForm.proxyProtocol')} name={['settings', 'proxyProtocol']}>
-                          <Select
-                            options={[
-                              { value: 0, label: `(${t('none')})` },
-                              { value: 1, label: 'v1' },
-                              { value: 2, label: 'v2' },
-                            ]}
-                          />
-                        </Form.Item>
-
-                        <Form.Item label={t('pages.xray.outboundForm.fragment')} shouldUpdate noStyle>
-                          {() => {
-                            const fragment = (form.getFieldValue(['settings', 'fragment']) ?? {}) as {
-                              packets?: string;
-                              length?: string;
-                              interval?: string;
-                              maxSplit?: string;
-                            };
-                            const enabled = !!(fragment.length || fragment.interval || fragment.maxSplit);
-                            return (
-                              <>
-                                <Form.Item label="Fragment">
-                                  <Switch
-                                    checked={enabled}
-                                    onChange={(checked) => {
-                                      form.setFieldValue(
-                                        ['settings', 'fragment'],
-                                        checked
-                                          ? {
-                                            packets: 'tlshello',
-                                            length: '100-200',
-                                            interval: '10-20',
-                                            maxSplit: '300-400',
-                                          }
-                                          : { packets: '', length: '', interval: '', maxSplit: '' },
-                                      );
-                                    }}
-                                  />
-                                </Form.Item>
-                                {enabled && (
-                                  <>
-                                    <Form.Item
-                                      label={t('pages.settings.subFormats.packets')}
-                                      name={['settings', 'fragment', 'packets']}
-                                    >
-                                      <Select
-                                        options={[
-                                          { value: '1-3', label: '1-3' },
-                                          { value: 'tlshello', label: 'tlshello' },
-                                        ]}
-                                      />
-                                    </Form.Item>
-                                    <Form.Item label={t('pages.settings.subFormats.length')} name={['settings', 'fragment', 'length']}>
-                                      <Input />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.settings.subFormats.interval')}
-                                      name={['settings', 'fragment', 'interval']}
-                                    >
-                                      <Input />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.settings.subFormats.maxSplit')}
-                                      name={['settings', 'fragment', 'maxSplit']}
-                                    >
-                                      <Input />
-                                    </Form.Item>
-                                  </>
-                                )}
-                              </>
-                            );
-                          }}
-                        </Form.Item>
-
-                        <Form.List name={['settings', 'noises']}>
-                          {(fields, { add, remove }) => (
-                            <>
-                              <Form.Item label={t('pages.settings.subFormats.noises')}>
-                                <Switch
-                                  checked={fields.length > 0}
-                                  onChange={(checked) => {
-                                    if (checked) {
-                                      add({
-                                        type: 'rand',
-                                        packet: '10-20',
-                                        delay: '10-16',
-                                        applyTo: 'ip',
-                                      });
-                                    } else {
-                                      // remove() with no arg is not supported;
-                                      // walk fields in reverse and drop each.
-                                      for (let i = fields.length - 1; i >= 0; i--) {
-                                        remove(fields[i].name);
-                                      }
-                                    }
-                                  }}
-                                />
-                                {fields.length > 0 && (
-                                  <Button
-                                    size="small"
-                                    type="primary"
-                                    className="ml-8"
-                                    icon={<PlusOutlined />}
-                                    onClick={() =>
-                                      add({
-                                        type: 'rand',
-                                        packet: '10-20',
-                                        delay: '10-16',
-                                        applyTo: 'ip',
-                                      })
-                                    }
-                                  />
-                                )}
-                              </Form.Item>
-                              {fields.map((field, index) => (
-                                <div key={field.key}>
-                                  <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
-                                    <div className="item-heading">
-                                      <span>{t('pages.settings.subFormats.noiseItem', { n: index + 1 })}</span>
-                                      {fields.length > 1 && (
-                                        <DeleteOutlined
-                                          className="danger-icon"
-                                          onClick={() => remove(field.name)}
-                                        />
-                                      )}
-                                    </div>
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.settings.subFormats.type')} name={[field.name, 'type']}>
-                                    <Select
-                                      options={['rand', 'base64', 'str', 'hex'].map((v) => ({
-                                        value: v,
-                                        label: v,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.settings.subFormats.packet')} name={[field.name, 'packet']}>
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.settings.subFormats.delayMs')} name={[field.name, 'delay']}>
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.settings.subFormats.applyTo')} name={[field.name, 'applyTo']}>
-                                    <Select
-                                      options={['ip', 'ipv4', 'ipv6'].map((v) => ({
-                                        value: v,
-                                        label: v,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                </div>
-                              ))}
-                            </>
-                          )}
-                        </Form.List>
-
-                        <Form.List name={['settings', 'finalRules']}>
-                          {(fields, { add, remove }) => (
-                            <>
-                              <Form.Item label={t('pages.xray.outboundForm.finalRules')}>
-                                <Button
-                                  size="small"
-                                  type="primary"
-                                  icon={<PlusOutlined />}
-                                  onClick={() =>
-                                    add({
-                                      action: 'allow',
-                                      network: '',
-                                      port: '',
-                                      ip: [],
-                                      blockDelay: '',
-                                    })
-                                  }
-                                />
-                                <span className="ml-8" style={{ opacity: 0.6 }}>
-                                  {t('pages.xray.outboundForm.overrideXrayPrivateIp')}
-                                </span>
-                              </Form.Item>
-                              {fields.map((field, index) => (
-                                <div key={field.key}>
-                                  <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
-                                    <div className="item-heading">
-                                      <span>{t('pages.xray.outboundForm.ruleN', { n: index + 1 })}</span>
-                                      <DeleteOutlined
-                                        className="danger-icon"
-                                        onClick={() => remove(field.name)}
-                                      />
-                                    </div>
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.xray.outboundForm.action')} name={[field.name, 'action']}>
-                                    <Select
-                                      options={['allow', 'block'].map((v) => ({
-                                        value: v,
-                                        label: v,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.inbounds.network')} name={[field.name, 'network']}>
-                                    <Select
-                                      allowClear
-                                      placeholder="(any)"
-                                      options={['tcp', 'udp', 'tcp,udp'].map((v) => ({
-                                        value: v,
-                                        label: v,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.inbounds.port')} name={[field.name, 'port']}>
-                                    <Input placeholder="e.g. 80,443 or 1000-2000" />
-                                  </Form.Item>
-                                  <Form.Item label="IP / CIDR / geoip" name={[field.name, 'ip']}>
-                                    <Select
-                                      mode="tags"
-                                      tokenSeparators={[',', ' ']}
-                                      placeholder="e.g. 10.0.0.0/8, geoip:private"
-                                    />
-                                  </Form.Item>
-                                  <Form.Item shouldUpdate noStyle>
-                                    {() => {
-                                      const ruleAction = form.getFieldValue([
-                                        'settings',
-                                        'finalRules',
-                                        field.name,
-                                        'action',
-                                      ]);
-                                      if (ruleAction !== 'block') return null;
-                                      return (
-                                        <Form.Item
-                                          label={t('pages.xray.outboundForm.blockDelay')}
-                                          name={[field.name, 'blockDelay']}
-                                        >
-                                          <Input placeholder="optional: 5000-10000" />
-                                        </Form.Item>
-                                      );
-                                    }}
-                                  </Form.Item>
-                                </div>
-                              ))}
-                            </>
-                          )}
-                        </Form.List>
-                      </>
-                    )}
-
-                    {protocol === 'vless' && (
-                      <Form.Item shouldUpdate noStyle>
-                        {() => {
-                          const reverseTag = form.getFieldValue(['settings', 'reverseTag']);
-                          if (!reverseTag) return null;
-                          const sniff = (form.getFieldValue(['settings', 'reverseSniffing']) ?? {}) as {
-                            enabled?: boolean;
-                          };
-                          return (
-                            <>
-                              <Form.Item
-                                label={t('pages.xray.outboundForm.reverseSniffing')}
-                                name={['settings', 'reverseSniffing', 'enabled']}
-                                valuePropName="checked"
-                              >
-                                <Switch />
-                              </Form.Item>
-                              {sniff.enabled && (
-                                <>
-                                  <Form.Item
-                                    wrapperCol={{ md: { span: 14, offset: 8 } }}
-                                    name={['settings', 'reverseSniffing', 'destOverride']}
-                                  >
-                                    <Select
-                                      mode="multiple"
-                                      className="sniffing-options"
-                                      options={Object.entries(SNIFFING_OPTION).map(([k, v]) => ({
-                                        value: v,
-                                        label: k,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.sniffingMetadataOnly')}
-                                    name={['settings', 'reverseSniffing', 'metadataOnly']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.sniffingRouteOnly')}
-                                    name={['settings', 'reverseSniffing', 'routeOnly']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.sniffingIpsExcluded')}
-                                    name={['settings', 'reverseSniffing', 'ipsExcluded']}
-                                  >
-                                    <Select
-                                      mode="tags"
-                                      tokenSeparators={[',']}
-                                      placeholder="IP/CIDR/geoip:*"
-                                    />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.sniffingDomainsExcluded')}
-                                    name={['settings', 'reverseSniffing', 'domainsExcluded']}
-                                  >
-                                    <Select
-                                      mode="tags"
-                                      tokenSeparators={[',']}
-                                      placeholder="domain:*"
-                                    />
-                                  </Form.Item>
-                                </>
-                              )}
-                            </>
-                          );
-                        }}
-                      </Form.Item>
-                    )}
-
-                    {protocol === 'wireguard' && (
-                      <>
-                        <Form.Item label={t('pages.inbounds.address')} name={['settings', 'address']}>
-                          <Input placeholder="comma-separated, e.g. 10.0.0.1,fd00::1" />
-                        </Form.Item>
-                        <Form.Item label={t('pages.inbounds.privatekey')}>
-                          <Space.Compact block>
-                            <Form.Item name={['settings', 'secretKey']} noStyle>
-                              <Input style={{ width: 'calc(100% - 32px)' }} />
-                            </Form.Item>
-                            <Button
-                              icon={<ReloadOutlined />}
-                              onClick={() => {
-                                const pair = Wireguard.generateKeypair();
-                                form.setFieldValue(['settings', 'secretKey'], pair.privateKey);
-                                form.setFieldValue(['settings', 'pubKey'], pair.publicKey);
-                              }}
-                            />
-                          </Space.Compact>
-                        </Form.Item>
-                        <Form.Item label={t('pages.inbounds.publicKey')} name={['settings', 'pubKey']}>
-                          <Input disabled />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.wireguard.domainStrategy')} name={['settings', 'domainStrategy']}>
-                          <Select
-                            options={[
-                              { value: '', label: `(${t('none')})` },
-                              ...WireguardDomainStrategy.map((s) => ({ value: s, label: s })),
-                            ]}
-                          />
-                        </Form.Item>
-                        <Form.Item label="MTU" name={['settings', 'mtu']}>
-                          <InputNumber min={0} />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.outboundForm.workers')} name={['settings', 'workers']}>
-                          <InputNumber min={0} />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.inbounds.info.noKernelTun')}
-                          name={['settings', 'noKernelTun']}
-                          valuePropName="checked"
-                        >
-                          <Switch />
-                        </Form.Item>
-                        <Form.Item label={t('pages.xray.outboundForm.reserved')} name={['settings', 'reserved']}>
-                          <Input placeholder="comma-separated bytes, e.g. 1,2,3" />
-                        </Form.Item>
-                        <Form.List name={['settings', 'peers']}>
-                          {(fields, { add, remove }) => (
-                            <>
-                              <Form.Item label={t('pages.inbounds.form.peers')}>
-                                <Button
-                                  size="small"
-                                  type="primary"
-                                  icon={<PlusOutlined />}
-                                  onClick={() =>
-                                    add({
-                                      publicKey: '',
-                                      psk: '',
-                                      allowedIPs: ['0.0.0.0/0', '::/0'],
-                                      endpoint: '',
-                                      keepAlive: 0,
-                                    })
-                                  }
-                                />
-                              </Form.Item>
-                              {fields.map((field, index) => (
-                                <div key={field.key}>
-                                  <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
-                                    <div className="item-heading">
-                                      <span>{t('pages.inbounds.info.peerNumber', { n: index + 1 })}</span>
-                                      {fields.length > 1 && (
-                                        <DeleteOutlined
-                                          className="danger-icon"
-                                          onClick={() => remove(field.name)}
-                                        />
-                                      )}
-                                    </div>
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.xray.wireguard.endpoint')} name={[field.name, 'endpoint']}>
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.publicKey')}
-                                    name={[field.name, 'publicKey']}
-                                  >
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item label="PSK" name={[field.name, 'psk']}>
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.xray.wireguard.allowedIPs')}>
-                                    <Form.List name={[field.name, 'allowedIPs']}>
-                                      {(ipFields, { add: addIp, remove: removeIp }) => (
-                                        <>
-                                          {ipFields.map((ipField, ipIdx) => (
-                                            <Space.Compact
-                                              key={ipField.key}
-                                              block
-                                              style={{ marginBottom: 4 }}
-                                            >
-                                              <Form.Item noStyle name={ipField.name}>
-                                                <Input />
-                                              </Form.Item>
-                                              {ipFields.length > 1 && (
-                                                <InputAddon onClick={() => removeIp(ipIdx)}>
-                                                  <MinusOutlined />
-                                                </InputAddon>
-                                              )}
-                                            </Space.Compact>
-                                          ))}
-                                          <Button
-                                            size="small"
-                                            icon={<PlusOutlined />}
-                                            onClick={() => addIp('')}
-                                          />
-                                        </>
-                                      )}
-                                    </Form.List>
-                                  </Form.Item>
-                                  <Form.Item label={t('pages.inbounds.info.keepAlive')} name={[field.name, 'keepAlive']}>
-                                    <InputNumber min={0} />
-                                  </Form.Item>
-                                </div>
-                              ))}
-                            </>
-                          )}
-                        </Form.List>
-                      </>
-                    )}
-
-                    {streamAllowed && network && (
-                      <>
-                        <Form.Item
-                          label={t('transmission')}
-                          name={['streamSettings', 'network']}
-                        >
-                          <Select
-                            value={network}
-                            onChange={onNetworkChange}
-                            options={
-                              protocol === 'hysteria'
-                                ? [HYSTERIA_NETWORK_OPTION]
-                                : NETWORK_OPTIONS
-                            }
-                          />
-                        </Form.Item>
-
-                        {network === 'tcp' && (
-                          <Form.Item shouldUpdate noStyle>
-                            {() => {
-                              const type =
-                                form.getFieldValue([
-                                  'streamSettings',
-                                  'tcpSettings',
-                                  'header',
-                                  'type',
-                                ]) ?? 'none';
-                              return (
-                                <>
-                                  <Form.Item label={`HTTP ${t('camouflage')}`}>
-                                    <Switch
-                                      checked={type === 'http'}
-                                      onChange={(checked) =>
-                                        form.setFieldValue(
-                                          ['streamSettings', 'tcpSettings', 'header'],
-                                          checked
-                                            ? {
-                                              type: 'http',
-                                              request: {
-                                                version: '1.1',
-                                                method: 'GET',
-                                                path: ['/'],
-                                                headers: {},
-                                              },
-                                              response: {
-                                                version: '1.1',
-                                                status: '200',
-                                                reason: 'OK',
-                                                headers: {},
-                                              },
-                                            }
-                                            : { type: 'none' },
-                                        )
-                                      }
-                                    />
-                                  </Form.Item>
-                                  {type === 'http' && (
-                                    <>
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.requestMethod')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'request', 'method',
-                                        ]}
-                                      >
-                                        <Input placeholder="GET" />
-                                      </Form.Item>
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.requestVersion')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'request', 'version',
-                                        ]}
-                                      >
-                                        <Input placeholder="1.1" />
-                                      </Form.Item>
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.requestHeaders')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'request', 'headers',
-                                        ]}
-                                      >
-                                        <HeaderMapEditor mode="v2" />
-                                      </Form.Item>
-
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.responseVersion')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'response', 'version',
-                                        ]}
-                                      >
-                                        <Input placeholder="1.1" />
-                                      </Form.Item>
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.responseStatus')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'response', 'status',
-                                        ]}
-                                      >
-                                        <Input placeholder="200" />
-                                      </Form.Item>
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.responseReason')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'response', 'reason',
-                                        ]}
-                                      >
-                                        <Input placeholder="OK" />
-                                      </Form.Item>
-                                      <Form.Item
-                                        label={t('pages.inbounds.form.responseHeaders')}
-                                        name={[
-                                          'streamSettings', 'tcpSettings', 'header',
-                                          'response', 'headers',
-                                        ]}
-                                      >
-                                        <HeaderMapEditor mode="v2" />
-                                      </Form.Item>
-                                    </>
-                                  )}
-                                </>
-                              );
-                            }}
-                          </Form.Item>
-                        )}
-
-                        {network === 'kcp' && (
-                          <>
-                            <Form.Item label="MTU" name={['streamSettings', 'kcpSettings', 'mtu']}>
-                              <InputNumber min={0} />
-                            </Form.Item>
-                            <Form.Item label={t('pages.inbounds.form.ttiMs')} name={['streamSettings', 'kcpSettings', 'tti']}>
-                              <InputNumber min={0} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.uplinkMbps')}
-                              name={['streamSettings', 'kcpSettings', 'uplinkCapacity']}
-                            >
-                              <InputNumber min={0} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.downlinkMbps')}
-                              name={['streamSettings', 'kcpSettings', 'downlinkCapacity']}
-                            >
-                              <InputNumber min={0} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.cwndMultiplier')}
-                              name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
-                            >
-                              <InputNumber min={1} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.maxSendingWindow')}
-                              name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
-                            >
-                              <InputNumber min={0} />
-                            </Form.Item>
-                          </>
-                        )}
-
-                        {network === 'ws' && (
-                          <>
-                            <Form.Item label={t('host')} name={['streamSettings', 'wsSettings', 'host']}>
-                              <Input />
-                            </Form.Item>
-                            <Form.Item label={t('path')} name={['streamSettings', 'wsSettings', 'path']}>
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.heartbeatPeriod')}
-                              name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
-                            >
-                              <InputNumber min={0} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.headers')}
-                              name={['streamSettings', 'wsSettings', 'headers']}
-                            >
-                              <HeaderMapEditor mode="v1" />
-                            </Form.Item>
-                          </>
-                        )}
-
-                        {network === 'grpc' && (
-                          <>
-                            <Form.Item
-                              label={t('pages.inbounds.form.serviceName')}
-                              name={['streamSettings', 'grpcSettings', 'serviceName']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.authority')}
-                              name={['streamSettings', 'grpcSettings', 'authority']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.multiMode')}
-                              name={['streamSettings', 'grpcSettings', 'multiMode']}
-                              valuePropName="checked"
-                            >
-                              <Switch />
-                            </Form.Item>
-                          </>
-                        )}
-
-                        {network === 'httpupgrade' && (
-                          <>
-                            <Form.Item
-                              label={t('host')}
-                              name={['streamSettings', 'httpupgradeSettings', 'host']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('path')}
-                              name={['streamSettings', 'httpupgradeSettings', 'path']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.headers')}
-                              name={['streamSettings', 'httpupgradeSettings', 'headers']}
-                            >
-                              <HeaderMapEditor mode="v1" />
-                            </Form.Item>
-                          </>
-                        )}
-
-                        {network === 'xhttp' && (
-                          <>
-                            <Form.Item
-                              label={t('host')}
-                              name={['streamSettings', 'xhttpSettings', 'host']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('path')}
-                              name={['streamSettings', 'xhttpSettings', 'path']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.info.mode')}
-                              name={['streamSettings', 'xhttpSettings', 'mode']}
-                            >
-                              <Select options={MODE_OPTIONS} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.paddingBytes')}
-                              name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.headers')}
-                              name={['streamSettings', 'xhttpSettings', 'headers']}
-                            >
-                              <HeaderMapEditor mode="v1" />
-                            </Form.Item>
-
-                            {/* Padding obfs sub-section: gated by a Switch.
-                                When on, four extra knobs (key/header/placement/
-                                method) tune how Xray injects random padding to
-                                disguise the post body shape. */}
-                            <Form.Item
-                              label={t('pages.inbounds.form.paddingObfsMode')}
-                              name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
-                              valuePropName="checked"
-                            >
-                              <Switch />
-                            </Form.Item>
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                const obfs = !!form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
-                                ]);
-                                if (!obfs) return null;
-                                return (
-                                  <>
-                                    <Form.Item
-                                      label={t('pages.inbounds.form.paddingKey')}
-                                      name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
-                                    >
-                                      <Input placeholder="x_padding" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.inbounds.form.paddingHeader')}
-                                      name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
-                                    >
-                                      <Input placeholder="X-Padding" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.inbounds.form.paddingPlacement')}
-                                      name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
-                                    >
-                                      <Select
-                                        options={[
-                                          { value: '', label: 'Default (queryInHeader)' },
-                                          { value: 'queryInHeader', label: 'queryInHeader' },
-                                          { value: 'header', label: 'header' },
-                                          { value: 'cookie', label: 'cookie' },
-                                          { value: 'query', label: 'query' },
-                                        ]}
-                                      />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.inbounds.form.paddingMethod')}
-                                      name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
-                                    >
-                                      <Select
-                                        options={[
-                                          { value: '', label: 'Default (repeat-x)' },
-                                          { value: 'repeat-x', label: 'repeat-x' },
-                                          { value: 'tokenish', label: 'tokenish' },
-                                        ]}
-                                      />
-                                    </Form.Item>
-                                  </>
-                                );
-                              }}
-                            </Form.Item>
-
-                            <Form.Item
-                              noStyle
-                              shouldUpdate={(prev, curr) =>
-                                prev?.streamSettings?.xhttpSettings?.mode !==
-                                curr?.streamSettings?.xhttpSettings?.mode
-                              }
-                            >
-                              {() => {
-                                const mode = form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'mode',
-                                ]);
-                                return (
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.uplinkHttpMethod')}
-                                    name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
-                                  >
-                                    <Select
-                                      placeholder="Default (POST)"
-                                      options={[
-                                        { value: '', label: 'Default (POST)' },
-                                        { value: 'POST', label: 'POST' },
-                                        { value: 'PUT', label: 'PUT' },
-                                        { value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
-                                      ]}
-                                    />
-                                  </Form.Item>
-                                );
-                              }}
-                            </Form.Item>
-
-                            {/* Session + sequence + uplinkData placements:
-                                three orthogonal slots Xray uses to thread
-                                request metadata through the transport
-                                (path / header / cookie / query). Key field
-                                only matters when placement is not 'path'. */}
-                            <Form.Item
-                              label={t('pages.inbounds.form.sessionPlacement')}
-                              name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
-                            >
-                              <Select
-                                placeholder="Default (path)"
-                                options={[
-                                  { value: '', label: 'Default (path)' },
-                                  { value: 'path', label: 'path' },
-                                  { value: 'header', label: 'header' },
-                                  { value: 'cookie', label: 'cookie' },
-                                  { value: 'query', label: 'query' },
-                                ]}
-                              />
-                            </Form.Item>
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                const placement = form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'sessionPlacement',
-                                ]);
-                                if (!placement || placement === 'path') return null;
-                                return (
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.sessionKey')}
-                                    name={['streamSettings', 'xhttpSettings', 'sessionKey']}
-                                  >
-                                    <Input placeholder="x_session" />
-                                  </Form.Item>
-                                );
-                              }}
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.sequencePlacement')}
-                              name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
-                            >
-                              <Select
-                                placeholder="Default (path)"
-                                options={[
-                                  { value: '', label: 'Default (path)' },
-                                  { value: 'path', label: 'path' },
-                                  { value: 'header', label: 'header' },
-                                  { value: 'cookie', label: 'cookie' },
-                                  { value: 'query', label: 'query' },
-                                ]}
-                              />
-                            </Form.Item>
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                const placement = form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'seqPlacement',
-                                ]);
-                                if (!placement || placement === 'path') return null;
-                                return (
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.sequenceKey')}
-                                    name={['streamSettings', 'xhttpSettings', 'seqKey']}
-                                  >
-                                    <Input placeholder="x_seq" />
-                                  </Form.Item>
-                                );
-                              }}
-                            </Form.Item>
-
-                            {/* Mode-conditional sub-sections. */}
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                const mode = form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'mode',
-                                ]);
-                                if (mode !== 'packet-up') return null;
-                                return (
-                                  <>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.minUploadInterval')}
-                                      name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
-                                    >
-                                      <Input placeholder="30" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.maxUploadSizeBytes')}
-                                      name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
-                                    >
-                                      <Input placeholder="1000000" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.inbounds.form.uplinkDataPlacement')}
-                                      name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
-                                    >
-                                      <Select
-                                        options={[
-                                          { value: '', label: 'Default (body)' },
-                                          { value: 'body', label: 'body' },
-                                          { value: 'header', label: 'header' },
-                                          { value: 'cookie', label: 'cookie' },
-                                          { value: 'query', label: 'query' },
-                                        ]}
-                                      />
-                                    </Form.Item>
-                                    <Form.Item shouldUpdate noStyle>
-                                      {() => {
-                                        const place = form.getFieldValue([
-                                          'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
-                                        ]);
-                                        if (!place || place === 'body') return null;
-                                        return (
-                                          <>
-                                            <Form.Item
-                                              label={t('pages.inbounds.form.uplinkDataKey')}
-                                              name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
-                                            >
-                                              <Input placeholder="x_data" />
-                                            </Form.Item>
-                                            <Form.Item
-                                              label={t('pages.xray.outboundForm.uplinkChunkSize')}
-                                              name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
-                                            >
-                                              <InputNumber
-                                                min={0}
-                                                placeholder="0 (unlimited)"
-                                                style={{ width: '100%' }}
-                                              />
-                                            </Form.Item>
-                                          </>
-                                        );
-                                      }}
-                                    </Form.Item>
-                                  </>
-                                );
-                              }}
-                            </Form.Item>
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                const mode = form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'mode',
-                                ]);
-                                if (mode !== 'stream-up' && mode !== 'stream-one') return null;
-                                return (
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.noGrpcHeader')}
-                                    name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                );
-                              }}
-                            </Form.Item>
-
-                            {/* XMUX is the connection-multiplexing layer
-                                xHTTP uses to fan out parallel requests over
-                                a small pool of upstream connections. UI-only
-                                toggle (enableXmux) hides the 6 nested knobs
-                                when off. */}
-                            <Form.Item
-                              label="XMUX"
-                              name={['streamSettings', 'xhttpSettings', 'enableXmux']}
-                              valuePropName="checked"
-                            >
-                              <Switch onChange={onXmuxToggle} />
-                            </Form.Item>
-                            <Form.Item shouldUpdate noStyle>
-                              {() => {
-                                if (!form.getFieldValue([
-                                  'streamSettings', 'xhttpSettings', 'enableXmux',
-                                ])) return null;
-                                return (
-                                  <>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.maxConcurrency')}
-                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
-                                    >
-                                      <Input placeholder="16-32" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.maxConnections')}
-                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
-                                    >
-                                      <Input placeholder="0" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.maxReuseTimes')}
-                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
-                                    >
-                                      <Input />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.maxRequestTimes')}
-                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
-                                    >
-                                      <Input placeholder="600-900" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.maxReusableSecs')}
-                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
-                                    >
-                                      <Input placeholder="1800-3000" />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.xray.outboundForm.keepAlivePeriod')}
-                                      name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
-                                    >
-                                      <InputNumber min={0} style={{ width: '100%' }} />
-                                    </Form.Item>
-                                  </>
-                                );
-                              }}
-                            </Form.Item>
-                          </>
-                        )}
-
-                        {network === 'hysteria' && (
-                          <>
-                            <Form.Item
-                              label={t('pages.inbounds.form.version')}
-                              name={['streamSettings', 'hysteriaSettings', 'version']}
-                            >
-                              <InputNumber min={2} max={2} disabled style={{ width: '100%' }} />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.xray.outboundForm.authPassword')}
-                              name={['streamSettings', 'hysteriaSettings', 'auth']}
-                            >
-                              <Input />
-                            </Form.Item>
-                            <Form.Item
-                              label={t('pages.inbounds.form.udpIdleTimeout')}
-                              name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
-                            >
-                              <InputNumber min={1} style={{ width: '100%' }} />
-                            </Form.Item>
-                            <HysteriaMasqueradeForm form={form} />
-                          </>
-                        )}
-                      </>
-                    )}
-
-                    {tlsFlowAllowed && (
-                      <Form.Item label={t('pages.clients.flow')} name={['settings', 'flow']}>
-                        <Select
-                          allowClear
-                          placeholder={t('none')}
-                          options={[{ value: '', label: t('none') }, ...FLOW_OPTIONS]}
-                        />
-                      </Form.Item>
-                    )}
-
-                    {/* Vision seed knobs only meaningful for the exact
-                        xtls-rprx-vision flow, on TCP+(tls|reality). The
-                        legacy class gated this on `canEnableVisionSeed()`
-                        — same condition encoded inline here. */}
-                    <Form.Item shouldUpdate noStyle>
-                      {() => {
-                        const flow =
-                          (form.getFieldValue(['settings', 'flow']) ?? '') as string;
-                        if (!(tlsFlowAllowed && flow === 'xtls-rprx-vision')) return null;
-                        return (
-                          <>
-                            <Form.Item label={t('pages.xray.outboundForm.visionTestpre')} name={['settings', 'testpre']}>
-                              <InputNumber min={0} style={{ width: '100%' }} />
-                            </Form.Item>
-                            <Form.Item label={t('pages.inbounds.form.visionTestseed')}>
-                              <Space.Compact block>
-                                {[900, 500, 900, 256].map((def, i) => (
-                                  <Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
-                                    <InputNumber min={1} style={{ width: '25%' }} />
-                                  </Form.Item>
-                                ))}
-                              </Space.Compact>
-                            </Form.Item>
-                          </>
-                        );
-                      }}
-                    </Form.Item>
-
-                    {streamAllowed && network && (
-                      <Form.Item label={t('security')}>
-                        <Radio.Group
-                          value={security}
-                          buttonStyle="solid"
-                          onChange={(e) => onSecurityChange(e.target.value as string)}
-                        >
-                          {network !== 'hysteria' && <Radio.Button value="none">{t('none')}</Radio.Button>}
-                          {tlsAllowed && <Radio.Button value="tls">TLS</Radio.Button>}
-                          {realityAllowed && <Radio.Button value="reality">Reality</Radio.Button>}
-                        </Radio.Group>
-                      </Form.Item>
-                    )}
-
-                    {security === 'tls' && tlsAllowed && (
-                      <>
-                        <Form.Item
-                          label="SNI"
-                          name={['streamSettings', 'tlsSettings', 'serverName']}
-                        >
-                          <Input placeholder={t('pages.xray.outboundForm.serverNamePlaceholder')} />
-                        </Form.Item>
-                        <Form.Item
-                          label="uTLS"
-                          name={['streamSettings', 'tlsSettings', 'fingerprint']}
-                        >
-                          <Select
-                            allowClear
-                            placeholder={t('none')}
-                            options={UTLS_OPTIONS}
-                          />
-                        </Form.Item>
-                        <Form.Item
-                          label="ALPN"
-                          name={['streamSettings', 'tlsSettings', 'alpn']}
-                        >
-                          <Select mode="multiple" options={ALPN_OPTIONS} />
-                        </Form.Item>
-                        <Form.Item
-                          label="ECH"
-                          name={['streamSettings', 'tlsSettings', 'echConfigList']}
-                        >
-                          <Input />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.xray.outboundForm.verifyPeerName')}
-                          name={['streamSettings', 'tlsSettings', 'verifyPeerCertByName']}
-                        >
-                          <Input placeholder="cloudflare-dns.com" />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.xray.outboundForm.pinnedSha256')}
-                          name={['streamSettings', 'tlsSettings', 'pinnedPeerCertSha256']}
-                        >
-                          <Input placeholder="base64 SHA256" />
-                        </Form.Item>
-                      </>
-                    )}
-
-                    {security === 'reality' && realityAllowed && (
-                      <>
-                        <Form.Item
-                          label="SNI"
-                          name={['streamSettings', 'realitySettings', 'serverName']}
-                        >
-                          <Input />
-                        </Form.Item>
-                        <Form.Item
-                          label="uTLS"
-                          name={['streamSettings', 'realitySettings', 'fingerprint']}
-                        >
-                          <Select options={UTLS_OPTIONS} />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.xray.outboundForm.shortId')}
-                          name={['streamSettings', 'realitySettings', 'shortId']}
-                        >
-                          <Input />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.inbounds.form.spiderX')}
-                          name={['streamSettings', 'realitySettings', 'spiderX']}
-                        >
-                          <Input />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.inbounds.publicKey')}
-                          name={['streamSettings', 'realitySettings', 'publicKey']}
-                        >
-                          <Input.TextArea autoSize={{ minRows: 2 }} />
-                        </Form.Item>
-                        <Form.Item
-                          label={t('pages.inbounds.form.mldsa65Verify')}
-                          name={['streamSettings', 'realitySettings', 'mldsa65Verify']}
-                        >
-                          <Input.TextArea autoSize={{ minRows: 2 }} />
-                        </Form.Item>
-                      </>
-                    )}
-
-                    {((streamAllowed && network) || !streamAllowed) && (
-                      <Form.Item shouldUpdate noStyle>
-                        {() => {
-                          const hasSockopt = !!form.getFieldValue([
-                            'streamSettings',
-                            'sockopt',
-                          ]);
-                          return (
-                            <>
-                              <Form.Item label={t('pages.xray.outboundForm.sockopts')}>
-                                <Switch
-                                  checked={hasSockopt}
-                                  onChange={(checked) => {
-                                    form.setFieldValue(
-                                      ['streamSettings', 'sockopt'],
-                                      checked ? SockoptStreamSettingsSchema.parse({}) : undefined,
-                                    );
-                                  }}
-                                />
-                              </Form.Item>
-                              {hasSockopt && (
-                                <>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.dialerProxy')}
-                                    name={['streamSettings', 'sockopt', 'dialerProxy']}
-                                  >
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.wireguard.domainStrategy')}
-                                    name={['streamSettings', 'sockopt', 'domainStrategy']}
-                                  >
-                                    <Select
-                                      options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({
-                                        value: v,
-                                        label: v,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.addressPortStrategy')}
-                                    name={['streamSettings', 'sockopt', 'addressPortStrategy']}
-                                  >
-                                    <Select options={ADDRESS_PORT_STRATEGY_OPTIONS} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.keepAliveInterval')}
-                                    name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
-                                  >
-                                    <InputNumber min={0} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.tcpFastOpen')}
-                                    name={['streamSettings', 'sockopt', 'tcpFastOpen']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.multipathTcp')}
-                                    name={['streamSettings', 'sockopt', 'tcpMptcp']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.penetrate')}
-                                    name={['streamSettings', 'sockopt', 'penetrate']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.markFwmark')}
-                                    name={['streamSettings', 'sockopt', 'mark']}
-                                  >
-                                    <InputNumber min={0} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.interface')}
-                                    name={['streamSettings', 'sockopt', 'interfaceName']}
-                                  >
-                                    <Input />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label="TProxy"
-                                    name={['streamSettings', 'sockopt', 'tproxy']}
-                                  >
-                                    <Select
-                                      options={[
-                                        { value: 'off', label: 'off' },
-                                        { value: 'redirect', label: 'redirect' },
-                                        { value: 'tproxy', label: 'tproxy' },
-                                      ]}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.tcpCongestion')}
-                                    name={['streamSettings', 'sockopt', 'tcpcongestion']}
-                                  >
-                                    <Select
-                                      options={Object.values(TCP_CONGESTION_OPTION).map((v) => ({
-                                        value: v,
-                                        label: v,
-                                      }))}
-                                    />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.ipv6Only')}
-                                    name={['streamSettings', 'sockopt', 'V6Only']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.acceptProxyProtocol')}
-                                    name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
-                                    valuePropName="checked"
-                                  >
-                                    <Switch />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.tcpUserTimeoutMs')}
-                                    name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
-                                  >
-                                    <InputNumber min={0} style={{ width: '100%' }} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.xray.outboundForm.tcpKeepAliveIdleS')}
-                                    name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
-                                  >
-                                    <InputNumber min={0} style={{ width: '100%' }} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.tcpMaxSeg')}
-                                    name={['streamSettings', 'sockopt', 'tcpMaxSeg']}
-                                  >
-                                    <InputNumber min={0} style={{ width: '100%' }} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.tcpWindowClamp')}
-                                    name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
-                                  >
-                                    <InputNumber min={0} style={{ width: '100%' }} />
-                                  </Form.Item>
-                                  <Form.Item
-                                    label={t('pages.inbounds.form.trustedXForwardedFor')}
-                                    name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
-                                  >
-                                    <Select
-                                      mode="tags"
-                                      tokenSeparators={[',', ' ']}
-                                      placeholder="trusted-proxy.example,10.0.0.0/8"
-                                    />
-                                  </Form.Item>
-                                  <Form.Item shouldUpdate noStyle>
-                                    {() => {
-                                      const he = form.getFieldValue([
-                                        'streamSettings', 'sockopt', 'happyEyeballs',
-                                      ]);
-                                      const hasHe = he != null;
-                                      return (
-                                        <>
-                                          <Form.Item label="Happy Eyeballs">
-                                            <Switch
-                                              checked={hasHe}
-                                              onChange={(v) => {
-                                                form.setFieldValue(
-                                                  ['streamSettings', 'sockopt', 'happyEyeballs'],
-                                                  v ? HappyEyeballsSchema.parse({}) : undefined,
-                                                );
-                                              }}
-                                            />
-                                          </Form.Item>
-                                          {hasHe && (
-                                            <>
-                                              <Form.Item
-                                                label={t('pages.inbounds.form.tryDelayMs')}
-                                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
-                                              >
-                                                <InputNumber min={0} style={{ width: '100%' }} placeholder="0 (disabled) — 250 recommended" />
-                                              </Form.Item>
-                                              <Form.Item
-                                                label={t('pages.inbounds.form.prioritizeIPv6')}
-                                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
-                                                valuePropName="checked"
-                                              >
-                                                <Switch />
-                                              </Form.Item>
-                                              <Form.Item
-                                                label={t('pages.inbounds.form.interleave')}
-                                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
-                                              >
-                                                <InputNumber min={1} style={{ width: '100%' }} />
-                                              </Form.Item>
-                                              <Form.Item
-                                                label={t('pages.inbounds.form.maxConcurrentTry')}
-                                                name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
-                                              >
-                                                <InputNumber min={0} style={{ width: '100%' }} />
-                                              </Form.Item>
-                                            </>
-                                          )}
-                                        </>
-                                      );
-                                    }}
-                                  </Form.Item>
-                                  <Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
-                                    {(fields, { add, remove }) => (
-                                      <>
-                                        <Form.Item label={t('pages.inbounds.form.customSockopt')}>
-                                          <Button
-                                            type="dashed"
-                                            size="small"
-                                            onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
-                                          >
-                                            + {t('pages.inbounds.form.addCustomOption')}
-                                          </Button>
-                                        </Form.Item>
-                                        {fields.map((field) => (
-                                          <Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
-                                            <Form.Item name={[field.name, 'system']} noStyle>
-                                              <Select
-                                                placeholder="all"
-                                                allowClear
-                                                style={{ width: 100 }}
-                                                options={[
-                                                  { value: 'linux', label: 'linux' },
-                                                  { value: 'windows', label: 'windows' },
-                                                  { value: 'darwin', label: 'darwin' },
-                                                ]}
-                                              />
-                                            </Form.Item>
-                                            <Form.Item name={[field.name, 'type']} noStyle>
-                                              <Select
-                                                style={{ width: 80 }}
-                                                options={[
-                                                  { value: 'int', label: 'int' },
-                                                  { value: 'str', label: 'str' },
-                                                ]}
-                                              />
-                                            </Form.Item>
-                                            <Form.Item name={[field.name, 'level']} noStyle>
-                                              <Input placeholder="level (6=TCP)" style={{ width: 100 }} />
-                                            </Form.Item>
-                                            <Form.Item name={[field.name, 'opt']} noStyle>
-                                              <Input placeholder="opt (decimal)" style={{ width: 120 }} />
-                                            </Form.Item>
-                                            <Form.Item name={[field.name, 'value']} noStyle>
-                                              <Input placeholder="value" style={{ flex: 1 }} />
-                                            </Form.Item>
-                                            <Button danger onClick={() => remove(field.name)}>−</Button>
-                                          </Space.Compact>
-                                        ))}
-                                      </>
-                                    )}
-                                  </Form.List>
-                                </>
-                              )}
-                            </>
-                          );
-                        }}
-                      </Form.Item>
-                    )}
-
-                    <FinalMaskForm
-                      name={['streamSettings', 'finalmask']}
-                      network={network}
-                      protocol={protocol}
-                      form={form}
-                    />
-
-                    {(() => {
-                      const flow = (form.getFieldValue(['settings', 'flow']) ?? '') as string;
-                      if (!isMuxAllowed(protocol, flow, network)) return null;
-                      return (
-                        <Form.Item shouldUpdate noStyle>
-                          {() => {
-                            const muxEnabled = !!form.getFieldValue(['mux', 'enabled']);
-                            return (
-                              <>
-                                <Form.Item
-                                  label={t('pages.settings.mux')}
-                                  name={['mux', 'enabled']}
-                                  valuePropName="checked"
-                                >
-                                  <Switch />
-                                </Form.Item>
-                                {muxEnabled && (
-                                  <>
-                                    <Form.Item
-                                      label={t('pages.settings.subFormats.concurrency')}
-                                      name={['mux', 'concurrency']}
-                                    >
-                                      <InputNumber min={-1} max={1024} />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.settings.subFormats.xudpConcurrency')}
-                                      name={['mux', 'xudpConcurrency']}
-                                    >
-                                      <InputNumber min={-1} max={1024} />
-                                    </Form.Item>
-                                    <Form.Item
-                                      label={t('pages.settings.subFormats.xudpUdp443')}
-                                      name={['mux', 'xudpProxyUDP443']}
-                                    >
-                                      <Select
-                                        options={['reject', 'allow', 'skip'].map((v) => ({
-                                          value: v,
-                                          label: v,
-                                        }))}
-                                      />
-                                    </Form.Item>
-                                  </>
-                                )}
-                              </>
-                            );
-                          }}
-                        </Form.Item>
-                      );
-                    })()}
-                  </>
-                ),
-              },
-              {
-                key: '2',
-                label: 'JSON',
-                children: (
-                  <Space orientation="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
-                    <Input.Search
-                      value={linkInput}
-                      placeholder="vmess:// vless:// trojan:// ss:// hysteria2:// wireguard://"
-                      enterButton="Import"
-                      onChange={(e) => setLinkInput(e.target.value)}
-                      onSearch={importLink}
-                    />
-                    <JsonEditor
-                      value={jsonText}
-                      onChange={(next) => {
-                        setJsonText(next);
-                        setJsonDirty(true);
-                      }}
-                      minHeight="360px"
-                      maxHeight="600px"
-                    />
-                  </Space>
-                ),
-              },
-            ]}
-          />
-        </Form>
-      </Modal>
-    </>
-  );
-}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio