1
0

7 Коммиты 08bc481ae3 ... b770287995

Автор SHA1 Сообщение Дата
  MHSanaei b770287995 fix(sub): stop appending the node name to subscription remarks (#5231) 14 часов назад
  MHSanaei 3c68b039f6 fix(sub): deliver vision flow for VLESS+XHTTP+REALITY in share links and Clash (#5232) 14 часов назад
  MHSanaei c200e248f7 fix(script): report per-file geo update status and skip restart when nothing changed 14 часов назад
  MHSanaei b5ef412b8d v3.3.1 16 часов назад
  MHSanaei 41cb0b8ae7 fix(inbounds): show remark first, else inbound tag, in client labels 16 часов назад
  MHSanaei cd46730bb9 Bump Go indirect deps; update frontend lock 16 часов назад
  MHSanaei 4eab37b66c feat(clients): restore reset traffic button in edit client form 16 часов назад

+ 98 - 95
frontend/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "3x-ui-frontend",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "3x-ui-frontend",
-      "version": "0.3.0",
+      "version": "0.3.1",
       "dependencies": {
         "@ant-design/icons": "^6.2.5",
         "@codemirror/lang-json": "^6.0.2",
@@ -518,9 +518,9 @@
       }
     },
     "node_modules/@codemirror/lint": {
-      "version": "6.9.6",
-      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz",
-      "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==",
+      "version": "6.9.7",
+      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
+      "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/state": "^6.0.0",
@@ -561,9 +561,9 @@
       }
     },
     "node_modules/@codemirror/view": {
-      "version": "6.43.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz",
-      "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==",
+      "version": "6.43.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz",
+      "integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/state": "^6.6.0",
@@ -617,9 +617,9 @@
       }
     },
     "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==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.3.tgz",
+      "integrity": "sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==",
       "dev": true,
       "funding": [
         {
@@ -1062,14 +1062,14 @@
       "license": "MIT"
     },
     "node_modules/@napi-rs/wasm-runtime": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
-      "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+      "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
       "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
-        "@tybys/wasm-util": "^0.10.1"
+        "@tybys/wasm-util": "^0.10.2"
       },
       "funding": {
         "type": "github",
@@ -1103,9 +1103,9 @@
       }
     },
     "node_modules/@rc-component/async-validator": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.1.tgz",
-      "integrity": "sha512-T03+Wk31Kz/28OC+rLlHtSNwD5Io3OWw6rPFPAp898sqALB/XnTrr3trB3mPoj379v0aRaW6t09HUG6dUyHR3g==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz",
+      "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==",
       "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.24.4"
@@ -1115,14 +1115,14 @@
       }
     },
     "node_modules/@rc-component/cascader": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.15.0.tgz",
-      "integrity": "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==",
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.16.1.tgz",
+      "integrity": "sha512-wxLopwM+EBed0zNNGdnGE4coYoqcO+XD42fHgn+pDvO+XzhNFbdgSlSNXdKocIYqccvqgWvoxDPNb0OVRdi59A==",
       "license": "MIT",
       "dependencies": {
-        "@rc-component/select": "~1.6.0",
-        "@rc-component/tree": "~1.3.0",
-        "@rc-component/util": "^1.4.0",
+        "@rc-component/select": "~1.7.1",
+        "@rc-component/tree": "~1.3.2",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
       },
       "peerDependencies": {
@@ -1236,12 +1236,12 @@
       }
     },
     "node_modules/@rc-component/form": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.2.tgz",
-      "integrity": "sha512-ZidCvOLmM9Xr+3vzk4UAoR7Aj1W/5IHyrzlBB7sNkygpTeRVrohQSo4TN7W/nARTH+nt8zSAPsn4BEl4zLEO2g==",
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.3.tgz",
+      "integrity": "sha512-jNkat3uxZ246ELudKwnjQhnDI8+rSxgLxjztvQU3Mrb0G+LwDyOrPu9RNfekOjqU5GQ5QJepi225x+9LhCizJw==",
       "license": "MIT",
       "dependencies": {
-        "@rc-component/async-validator": "^5.1.0",
+        "@rc-component/async-validator": "^6.0.0",
         "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
       },
@@ -1410,12 +1410,12 @@
       }
     },
     "node_modules/@rc-component/pagination": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz",
-      "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.3.0.tgz",
+      "integrity": "sha512-12ahTY+HPITg1L2bjWKXUqBJe/oOnpA2QsChdCjthqLVf/e19StiCsv8OLKpWoHbc+8PFEkNjRqRqrLoRBHjFw==",
       "license": "MIT",
       "dependencies": {
-        "@rc-component/util": "^1.3.0",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
       },
       "peerDependencies": {
@@ -1493,9 +1493,9 @@
       }
     },
     "node_modules/@rc-component/qrcode": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.2.tgz",
-      "integrity": "sha512-CTXG18eP3sO3gc+96ep9HyVI/RzMup7L59apM/D0wWo1SHRdwOb7xyD4bMbmpu4dPlTch59Kxb8lU7U9ME60fg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-2.0.0.tgz",
+      "integrity": "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==",
       "license": "MIT",
       "dependencies": {
         "@babel/runtime": "^7.24.7"
@@ -1555,15 +1555,15 @@
       }
     },
     "node_modules/@rc-component/select": {
-      "version": "1.6.15",
-      "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.15.tgz",
-      "integrity": "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==",
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.7.1.tgz",
+      "integrity": "sha512-GZ1cMJk2xQh0VHyOQjjG8drYL4iu24NcbkXioUcReQOCUr+ub/3fmRonZe6cRPEZhWMbJdeHsqnEltogDaZ5Tg==",
       "license": "MIT",
       "dependencies": {
         "@rc-component/overflow": "^1.0.0",
         "@rc-component/trigger": "^3.0.0",
-        "@rc-component/util": "^1.3.0",
-        "@rc-component/virtual-list": "^1.0.1",
+        "@rc-component/util": "^1.11.1",
+        "@rc-component/virtual-list": "^1.2.0",
         "clsx": "^2.1.1"
       },
       "engines": {
@@ -1717,12 +1717,12 @@
       }
     },
     "node_modules/@rc-component/tree-select": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.9.0.tgz",
-      "integrity": "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==",
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.10.0.tgz",
+      "integrity": "sha512-E1U4pn2LAbXEhLJdzIzid7WYbIuFbkTIctuFoeC6weppf8UbPR3+YYB6/ay0c0ksand4gXMRQpa1Z60Auo7VJA==",
       "license": "MIT",
       "dependencies": {
-        "@rc-component/select": "~1.6.0",
+        "@rc-component/select": "~1.7.0",
         "@rc-component/tree": "~1.3.0",
         "@rc-component/util": "^1.4.0",
         "clsx": "^2.1.1"
@@ -3284,9 +3284,9 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz",
-      "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
+      "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
       "dev": true,
       "license": "ISC",
       "bin": {
@@ -3478,9 +3478,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.16.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
-      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "version": "8.17.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+      "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
       "dev": true,
       "license": "MIT",
       "bin": {
@@ -3553,54 +3553,54 @@
       }
     },
     "node_modules/antd": {
-      "version": "6.4.3",
-      "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.3.tgz",
-      "integrity": "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==",
+      "version": "6.4.4",
+      "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.4.tgz",
+      "integrity": "sha512-lgPz4KhfhiYddV/qPYo0ieqWimCVgV2OQF72mbeGNixE753JWNnmEc7UNGy08wBS/zZ7hxrmX0pc5aX7EUaIIg==",
       "license": "MIT",
       "dependencies": {
         "@ant-design/colors": "^8.0.1",
         "@ant-design/cssinjs": "^2.1.2",
         "@ant-design/cssinjs-utils": "^2.1.2",
         "@ant-design/fast-color": "^3.0.1",
-        "@ant-design/icons": "^6.2.3",
+        "@ant-design/icons": "^6.2.5",
         "@ant-design/react-slick": "~2.0.0",
         "@babel/runtime": "^7.29.2",
-        "@rc-component/cascader": "~1.15.0",
+        "@rc-component/cascader": "~1.16.1",
         "@rc-component/checkbox": "~2.0.0",
         "@rc-component/collapse": "~1.2.0",
         "@rc-component/color-picker": "~3.1.1",
         "@rc-component/dialog": "~1.9.0",
         "@rc-component/drawer": "~1.4.2",
         "@rc-component/dropdown": "~1.0.2",
-        "@rc-component/form": "~1.8.1",
+        "@rc-component/form": "~1.8.3",
         "@rc-component/image": "~1.9.0",
-        "@rc-component/input": "~1.3.0",
+        "@rc-component/input": "~1.3.1",
         "@rc-component/input-number": "~1.6.2",
         "@rc-component/mentions": "~1.9.0",
-        "@rc-component/menu": "~1.3.0",
-        "@rc-component/motion": "^1.3.2",
+        "@rc-component/menu": "~1.3.1",
+        "@rc-component/motion": "^1.3.3",
         "@rc-component/mutate-observer": "^2.0.1",
         "@rc-component/notification": "~2.0.7",
-        "@rc-component/pagination": "~1.2.0",
+        "@rc-component/pagination": "~1.3.0",
         "@rc-component/picker": "~1.10.0",
         "@rc-component/progress": "~1.0.2",
-        "@rc-component/qrcode": "~1.1.1",
+        "@rc-component/qrcode": "~2.0.0",
         "@rc-component/rate": "~1.0.1",
         "@rc-component/resize-observer": "^1.1.2",
         "@rc-component/segmented": "~1.3.0",
-        "@rc-component/select": "~1.6.15",
+        "@rc-component/select": "~1.7.1",
         "@rc-component/slider": "~1.0.1",
         "@rc-component/steps": "~1.2.2",
         "@rc-component/switch": "~1.0.3",
-        "@rc-component/table": "~1.10.0",
-        "@rc-component/tabs": "~1.9.0",
+        "@rc-component/table": "~1.10.2",
+        "@rc-component/tabs": "~1.9.1",
         "@rc-component/tooltip": "~1.4.0",
         "@rc-component/tour": "~2.4.0",
-        "@rc-component/tree": "~1.3.1",
-        "@rc-component/tree-select": "~1.9.0",
-        "@rc-component/trigger": "^3.9.0",
-        "@rc-component/upload": "~1.1.0",
-        "@rc-component/util": "^1.11.0",
+        "@rc-component/tree": "~1.3.2",
+        "@rc-component/tree-select": "~1.10.0",
+        "@rc-component/trigger": "^3.9.1",
+        "@rc-component/upload": "~1.1.1",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1",
         "dayjs": "^1.11.11",
         "scroll-into-view-if-needed": "^3.1.0",
@@ -3719,9 +3719,9 @@
       "license": "MIT"
     },
     "node_modules/baseline-browser-mapping": {
-      "version": "2.10.34",
-      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
-      "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==",
+      "version": "2.10.37",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
+      "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
       "dev": true,
       "license": "Apache-2.0",
       "bin": {
@@ -3859,9 +3859,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001797",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
-      "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
+      "version": "1.0.30001799",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+      "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
       "dev": true,
       "funding": [
         {
@@ -4327,9 +4327,9 @@
       "license": "MIT"
     },
     "node_modules/dompurify": {
-      "version": "3.4.8",
-      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
-      "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
+      "version": "3.4.10",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz",
+      "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
       "license": "(MPL-2.0 OR Apache-2.0)",
       "optionalDependencies": {
         "@types/trusted-types": "^2.0.7"
@@ -4359,9 +4359,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.368",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
-      "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==",
+      "version": "1.5.372",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
+      "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==",
       "dev": true,
       "license": "ISC"
     },
@@ -4431,9 +4431,9 @@
       }
     },
     "node_modules/es-toolkit": {
-      "version": "1.47.0",
-      "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz",
-      "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==",
+      "version": "1.47.1",
+      "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz",
+      "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==",
       "license": "MIT",
       "workspaces": [
         "docs",
@@ -4464,11 +4464,14 @@
       }
     },
     "node_modules/eslint": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz",
-      "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==",
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz",
+      "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
       "dev": true,
       "license": "MIT",
+      "workspaces": [
+        "packages/*"
+      ],
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/regexpp": "^4.12.2",
@@ -4806,16 +4809,16 @@
       }
     },
     "node_modules/form-data": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
-      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
       "license": "MIT",
       "dependencies": {
         "asynckit": "^0.4.0",
         "combined-stream": "^1.0.8",
         "es-set-tostringtag": "^2.1.0",
-        "hasown": "^2.0.2",
-        "mime-types": "^2.1.12"
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
       },
       "engines": {
         "node": ">= 6"
@@ -5994,9 +5997,9 @@
       }
     },
     "node_modules/obug": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
-      "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+      "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
       "dev": true,
       "funding": [
         "https://github.com/sponsors/sxzz",

+ 2 - 2
frontend/package.json

@@ -1,7 +1,7 @@
 {
   "name": "3x-ui-frontend",
   "private": true,
-  "version": "0.3.0",
+  "version": "0.3.1",
   "type": "module",
   "description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
   "engines": {
@@ -73,4 +73,4 @@
     "core-js-pure": false,
     "tree-sitter-json": false
   }
-}
+}

+ 4 - 7
frontend/src/lib/inbounds/label.ts

@@ -1,12 +1,9 @@
 /**
- * Display label for an inbound: `tag (remark)` when a distinct remark exists,
- * otherwise just the tag. Falls back to the remark when no tag is set, and to an
- * empty string when neither is present.
+ * Display label for an inbound: the remark when one is set, otherwise the
+ * inbound tag. Falls back to an empty string when neither is present.
  */
 export function formatInboundLabel(tag?: string, remark?: string): string {
-  const tagText = (tag || '').trim();
   const remarkText = (remark || '').trim();
-  if (!tagText) return remarkText;
-  if (!remarkText || remarkText === tagText) return tagText;
-  return `${tagText} (${remarkText})`;
+  if (remarkText) return remarkText;
+  return (tag || '').trim();
 }

+ 45 - 5
frontend/src/pages/clients/ClientFormModal.tsx

@@ -8,6 +8,7 @@ import {
   Input,
   InputNumber,
   Modal,
+  Popconfirm,
   Row,
   Select,
   Space,
@@ -17,7 +18,7 @@ import {
   Tooltip,
   message,
 } from 'antd';
-import { EyeOutlined, ReloadOutlined } from '@ant-design/icons';
+import { EyeOutlined, ReloadOutlined, RetweetOutlined } from '@ant-design/icons';
 import dayjs from 'dayjs';
 import type { Dayjs } from 'dayjs';
 import { HttpUtil, RandomUtil } from '@/utils';
@@ -39,6 +40,7 @@ const CLIENT_IP_LOG_MODAL_Z_INDEX = CLIENT_FORM_MODAL_Z_INDEX + 1;
 
 interface ApiMsg<T = unknown> {
   success?: boolean;
+  msg?: string;
   obj?: T;
 }
 
@@ -72,6 +74,7 @@ interface ClientFormModalProps {
     payload: Record<string, unknown> | SaveCreatePayload,
     meta: SaveMetaEdit | SaveMetaCreate,
   ) => Promise<ApiMsg | null>;
+  resetTraffic?: (client: ClientRecord) => Promise<ApiMsg | null>;
   onOpenChange: (open: boolean) => void;
 }
 
@@ -140,6 +143,7 @@ export default function ClientFormModal({
   tgBotEnable = false,
   groups = [],
   save,
+  resetTraffic,
   onOpenChange,
 }: ClientFormModalProps) {
   const { t } = useTranslation();
@@ -148,6 +152,7 @@ export default function ClientFormModal({
 
   const [form, setForm] = useState<FormState>(emptyForm);
   const [submitting, setSubmitting] = useState(false);
+  const [resetting, setResetting] = useState(false);
   const [clientIps, setClientIps] = useState<string[]>([]);
   const [ipsLoading, setIpsLoading] = useState(false);
   const [ipsClearing, setIpsClearing] = useState(false);
@@ -328,6 +333,21 @@ export default function ClientFormModal({
     onOpenChange(false);
   }
 
+  async function onResetTraffic() {
+    if (!isEdit || !client?.email || !resetTraffic) return;
+    setResetting(true);
+    try {
+      const msg = await resetTraffic(client);
+      if (msg?.success) {
+        messageApi.success(t('pages.clients.toasts.trafficReset'));
+      } else {
+        messageApi.error(msg?.msg || t('somethingWentWrong'));
+      }
+    } finally {
+      setResetting(false);
+    }
+  }
+
   async function onSubmit() {
     const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
     const validated = schema.safeParse({
@@ -413,15 +433,35 @@ export default function ClientFormModal({
         open={open}
         title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
         destroyOnHidden
-        okText={isEdit ? t('save') : t('create')}
-        cancelText={t('cancel')}
-        okButtonProps={{ loading: submitting }}
         width={720}
         zIndex={CLIENT_FORM_MODAL_Z_INDEX}
         style={{ top: 20 }}
         styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
-        onOk={onSubmit}
         onCancel={close}
+        footer={
+          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+            {isEdit && resetTraffic && (
+              <Popconfirm
+                title={t('pages.inbounds.resetTraffic')}
+                description={t('pages.inbounds.resetTrafficContent')}
+                okText={t('reset')}
+                cancelText={t('cancel')}
+                zIndex={CLIENT_IP_LOG_MODAL_Z_INDEX}
+                onConfirm={onResetTraffic}
+              >
+                <Button color="danger" variant="filled" icon={<RetweetOutlined />} loading={resetting}>
+                  {t('pages.inbounds.resetTraffic')}
+                </Button>
+              </Popconfirm>
+            )}
+            <div style={{ marginInlineStart: 'auto', display: 'flex', gap: 8 }}>
+              <Button onClick={close}>{t('cancel')}</Button>
+              <Button type="primary" loading={submitting} onClick={onSubmit}>
+                {isEdit ? t('save') : t('create')}
+              </Button>
+            </div>
+          </div>
+        }
       >
         <Form layout="vertical">
           <Tabs

+ 44 - 42
frontend/src/pages/clients/ClientsPage.tsx

@@ -165,15 +165,15 @@ function gbToBytes(gb: number | undefined): number {
 }
 
 const SORT_OPTIONS: { value: string; column: string; order: 'ascend' | 'descend'; labelKey: string }[] = [
-  { value: 'createdAt:ascend',    column: 'createdAt',  order: 'ascend',   labelKey: 'pages.clients.sortOldest' },
-  { value: 'createdAt:descend',   column: 'createdAt',  order: 'descend',  labelKey: 'pages.clients.sortNewest' },
-  { value: 'updatedAt:descend',   column: 'updatedAt',  order: 'descend',  labelKey: 'pages.clients.sortRecentlyUpdated' },
-  { value: 'lastOnline:descend',  column: 'lastOnline', order: 'descend',  labelKey: 'pages.clients.sortRecentlyOnline' },
-  { value: 'email:ascend',        column: 'email',      order: 'ascend',   labelKey: 'pages.clients.sortEmailAZ' },
-  { value: 'email:descend',       column: 'email',      order: 'descend',  labelKey: 'pages.clients.sortEmailZA' },
-  { value: 'traffic:descend',     column: 'traffic',    order: 'descend',  labelKey: 'pages.clients.sortMostTraffic' },
-  { value: 'remaining:descend',   column: 'remaining',  order: 'descend',  labelKey: 'pages.clients.sortHighestRemaining' },
-  { value: 'expiryTime:ascend',   column: 'expiryTime', order: 'ascend',   labelKey: 'pages.clients.sortExpiringSoonest' },
+  { value: 'createdAt:ascend', column: 'createdAt', order: 'ascend', labelKey: 'pages.clients.sortOldest' },
+  { value: 'createdAt:descend', column: 'createdAt', order: 'descend', labelKey: 'pages.clients.sortNewest' },
+  { value: 'updatedAt:descend', column: 'updatedAt', order: 'descend', labelKey: 'pages.clients.sortRecentlyUpdated' },
+  { value: 'lastOnline:descend', column: 'lastOnline', order: 'descend', labelKey: 'pages.clients.sortRecentlyOnline' },
+  { value: 'email:ascend', column: 'email', order: 'ascend', labelKey: 'pages.clients.sortEmailAZ' },
+  { value: 'email:descend', column: 'email', order: 'descend', labelKey: 'pages.clients.sortEmailZA' },
+  { value: 'traffic:descend', column: 'traffic', order: 'descend', labelKey: 'pages.clients.sortMostTraffic' },
+  { value: 'remaining:descend', column: 'remaining', order: 'descend', labelKey: 'pages.clients.sortHighestRemaining' },
+  { value: 'expiryTime:ascend', column: 'expiryTime', order: 'ascend', labelKey: 'pages.clients.sortExpiringSoonest' },
 ];
 
 const DEFAULT_SORT = SORT_OPTIONS[0];
@@ -743,6 +743,7 @@ export default function ClientsPage() {
     {
       title: t('pages.clients.traffic'),
       key: 'traffic',
+      width: 300,
       render: (_v, record) => (
         <ClientTrafficCell
           up={record.traffic?.up}
@@ -924,40 +925,40 @@ export default function ClientsPage() {
                             menu={{
                               items: selectedRowKeys.length > 0
                                 ? [
-                                    {
-                                      key: 'adjust',
-                                      icon: <ClockCircleOutlined />,
-                                      label: t('pages.clients.adjust'),
-                                      onClick: () => setBulkAdjustOpen(true),
-                                    },
-                                    {
-                                      key: 'subLinks',
-                                      icon: <LinkOutlined />,
-                                      label: t('pages.clients.subLinks'),
-                                      onClick: () => setSubLinksOpen(true),
-                                    },
-                                  ]
+                                  {
+                                    key: 'adjust',
+                                    icon: <ClockCircleOutlined />,
+                                    label: t('pages.clients.adjust'),
+                                    onClick: () => setBulkAdjustOpen(true),
+                                  },
+                                  {
+                                    key: 'subLinks',
+                                    icon: <LinkOutlined />,
+                                    label: t('pages.clients.subLinks'),
+                                    onClick: () => setSubLinksOpen(true),
+                                  },
+                                ]
                                 : [
-                                    {
-                                      key: 'bulk',
-                                      icon: <UsergroupAddOutlined />,
-                                      label: t('pages.clients.bulk'),
-                                      onClick: () => setBulkAddOpen(true),
-                                    },
-                                    {
-                                      key: 'resetAll',
-                                      icon: <RetweetOutlined />,
-                                      label: t('pages.clients.resetAllTraffics'),
-                                      onClick: onResetAllTraffics,
-                                    },
-                                    {
-                                      key: 'delDepleted',
-                                      icon: <RestOutlined />,
-                                      label: t('pages.clients.delDepleted'),
-                                      danger: true,
-                                      onClick: onDelDepleted,
-                                    },
-                                  ],
+                                  {
+                                    key: 'bulk',
+                                    icon: <UsergroupAddOutlined />,
+                                    label: t('pages.clients.bulk'),
+                                    onClick: () => setBulkAddOpen(true),
+                                  },
+                                  {
+                                    key: 'resetAll',
+                                    icon: <RetweetOutlined />,
+                                    label: t('pages.clients.resetAllTraffics'),
+                                    onClick: onResetAllTraffics,
+                                  },
+                                  {
+                                    key: 'delDepleted',
+                                    icon: <RestOutlined />,
+                                    label: t('pages.clients.delDepleted'),
+                                    danger: true,
+                                    onClick: onDelDepleted,
+                                  },
+                                ],
                             }}
                           >
                             <Button icon={<MoreOutlined />}>
@@ -1246,6 +1247,7 @@ export default function ClientsPage() {
             tgBotEnable={tgBotEnable}
             groups={allGroups}
             save={onSave}
+            resetTraffic={resetTraffic}
             onOpenChange={setFormOpen}
           />
         </LazyMount>

+ 3 - 3
go.mod

@@ -102,15 +102,15 @@ require (
 	go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
 	golang.org/x/arch v0.28.0 // indirect
-	golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
+	golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
 	golang.org/x/mod v0.37.0 // indirect
 	golang.org/x/net v0.56.0
 	golang.org/x/sync v0.21.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
-	golang.org/x/tools v0.45.0 // indirect
+	golang.org/x/tools v0.46.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
 	google.golang.org/protobuf v1.36.11 // indirect
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	lukechampine.com/blake3 v1.4.1 // indirect

+ 6 - 6
go.sum

@@ -250,8 +250,8 @@ golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
 golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
 golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
 golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
-golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
-golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
+golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
+golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
 golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
 golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
 golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
@@ -269,8 +269,8 @@ golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
 golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
-golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
-golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
+golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
 golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w=
@@ -279,8 +279,8 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
 golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab h1:cY0oV1VnAqvaim8VsR8ZyEKAudzbRJMRGwD3W/L7yOw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
 google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

+ 1 - 1
internal/config/version

@@ -1 +1 @@
-3.3.0
+3.3.1

+ 4 - 3
internal/sub/clash_service.go

@@ -208,11 +208,12 @@ func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client
 	case model.VLESS:
 		proxy["type"] = "vless"
 		proxy["uuid"] = client.ID
-		if client.Flow != "" && network == "tcp" {
-			proxy["flow"] = client.Flow
-		}
 		var inboundSettings map[string]any
 		json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
+		streamSecurity, _ := stream["security"].(string)
+		if client.Flow != "" && vlessFlowAllowed(network, streamSecurity, inboundSettings) {
+			proxy["flow"] = client.Flow
+		}
 		if encryption, ok := inboundSettings["encryption"].(string); ok {
 			encryption = strings.TrimSpace(encryption)
 			if encryption != "" && encryption != "none" {

+ 57 - 0
internal/sub/clash_service_test.go

@@ -148,6 +148,63 @@ func TestBuildProxy_VLESSPostQuantumEncryptionUsesMihomoEncryptionField(t *testi
 	}
 }
 
+func TestBuildProxy_VLESSFlowXhttpRealityVlessenc(t *testing.T) {
+	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
+	encryption := "mlkem768x25519plus.native.0rtt.client"
+	inbound := &model.Inbound{
+		Listen:   "203.0.113.1",
+		Port:     443,
+		Protocol: model.VLESS,
+		Remark:   "pq-flow",
+		Settings: `{"encryption":"` + encryption + `"}`,
+	}
+	client := model.Client{ID: "11111111-2222-4333-8444-555555555555", Flow: "xtls-rprx-vision"}
+	stream := map[string]any{
+		"network": "xhttp",
+		"xhttpSettings": map[string]any{
+			"path": "/",
+			"mode": "auto",
+		},
+		"security": "reality",
+		"realitySettings": map[string]any{
+			"publicKey":  "pub",
+			"serverName": "example.com",
+			"shortId":    "abcd",
+		},
+	}
+
+	proxy := svc.buildProxy(inbound, client, stream, "")
+
+	if proxy["flow"] != "xtls-rprx-vision" {
+		t.Fatalf("xhttp+reality+vlessenc Clash proxy must carry the vision flow (#5232): %#v", proxy)
+	}
+}
+
+func TestBuildProxy_VLESSFlowDroppedWithoutVisionSupport(t *testing.T) {
+	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
+	inbound := &model.Inbound{
+		Listen:   "203.0.113.1",
+		Port:     443,
+		Protocol: model.VLESS,
+		Remark:   "plain-flow",
+		Settings: `{"encryption":"none"}`,
+	}
+	client := model.Client{ID: "11111111-2222-4333-8444-555555555555", Flow: "xtls-rprx-vision"}
+	stream := map[string]any{
+		"network":  "tcp",
+		"security": "none",
+		"tcpSettings": map[string]any{
+			"header": map[string]any{"type": "none"},
+		},
+	}
+
+	proxy := svc.buildProxy(inbound, client, stream, "")
+
+	if _, ok := proxy["flow"]; ok {
+		t.Fatalf("tcp without tls/reality must not carry a flow: %#v", proxy)
+	}
+}
+
 func TestBuildProxy_VLESSNoneEncryptionOmittedForClash(t *testing.T) {
 	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
 	inbound := &model.Inbound{

+ 20 - 24
internal/sub/service.go

@@ -484,6 +484,23 @@ func vlessEncryptionEnabled(settings map[string]any) bool {
 	return false
 }
 
+// vlessFlowAllowed reports whether a client's XTLS Vision flow belongs in
+// generated links/configs. Mirrors inboundCanEnableTlsFlow in
+// internal/web/service: Vision runs on TCP with tls/reality (classic), and on
+// XHTTP whenever VLESS encryption (vlessenc / ML-KEM) is enabled — there the
+// VLESS-level encryption stands in for the transport TLS that Vision relies
+// on, regardless of the stream security layer (so XHTTP+REALITY+vlessenc
+// keeps its flow too).
+func vlessFlowAllowed(network, security string, settings map[string]any) bool {
+	switch network {
+	case "tcp":
+		return security == "tls" || security == "reality"
+	case "xhttp":
+		return vlessEncryptionEnabled(settings)
+	}
+	return false
+}
+
 func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	if inbound.Protocol != model.VLESS {
 		return ""
@@ -513,21 +530,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	switch security {
 	case "tls":
 		applyShareTLSParams(stream, params)
-		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
-			params["flow"] = clients[clientIndex].Flow
-		}
 	case "reality":
 		applyShareRealityParams(stream, params)
-		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
-			params["flow"] = clients[clientIndex].Flow
-		}
 	default:
 		params["security"] = "none"
-		// VLESS encryption (vlessenc / ML-KEM) carries XTLS Vision over XHTTP
-		// without transport TLS.
-		if streamNetwork == "xhttp" && len(clients[clientIndex].Flow) > 0 && vlessEncryptionEnabled(settings) {
-			params["flow"] = clients[clientIndex].Flow
-		}
+	}
+	if len(clients[clientIndex].Flow) > 0 && vlessFlowAllowed(streamNetwork, security, settings) {
+		params["flow"] = clients[clientIndex].Flow
 	}
 
 	externalProxies, _ := stream["externalProxy"].([]any)
@@ -1519,19 +1528,6 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
 	if len(extra) > 0 {
 		orders['o'] = extra
 	}
-	// A node-hosted inbound usually shares its remark with the local copy it
-	// was synced from, so a multi-node subscription would list several
-	// identically-named entries differing only by address (#5035). Tag such
-	// entries with the node name unless the admin already put it in the remark.
-	if inbound.NodeID != nil && s.nodesByID != nil {
-		if n, ok := s.nodesByID[*inbound.NodeID]; ok && n != nil && n.Name != "" && !strings.Contains(orders['i'], n.Name) {
-			if orders['i'] != "" {
-				orders['i'] += "@" + n.Name
-			} else {
-				orders['i'] = n.Name
-			}
-		}
-	}
 
 	var remark []string
 	for i := 0; i < len(orderChars); i++ {

+ 100 - 0
internal/sub/service_flow_test.go

@@ -0,0 +1,100 @@
+package sub
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+// Issue #5232: a vision flow set on a VLESS+XHTTP+REALITY (vlessenc) client
+// must survive into subscription output, not just the inbound JSON.
+
+const testMlkemEncryption = "mlkem768x25519plus.native.0rtt.dGVzdC1rZXk"
+
+func TestVlessFlowAllowed(t *testing.T) {
+	enc := map[string]any{"encryption": testMlkemEncryption}
+	noEnc := map[string]any{"encryption": "none"}
+
+	tests := []struct {
+		name     string
+		network  string
+		security string
+		settings map[string]any
+		want     bool
+	}{
+		{"tcp tls", "tcp", "tls", noEnc, true},
+		{"tcp reality", "tcp", "reality", noEnc, true},
+		{"tcp none", "tcp", "none", noEnc, false},
+		{"tcp none vlessenc", "tcp", "none", enc, false},
+		{"xhttp none vlessenc", "xhttp", "none", enc, true},
+		{"xhttp reality vlessenc (#5232)", "xhttp", "reality", enc, true},
+		{"xhttp tls vlessenc", "xhttp", "tls", enc, true},
+		{"xhttp reality no vlessenc", "xhttp", "reality", noEnc, false},
+		{"ws tls", "ws", "tls", noEnc, false},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if got := vlessFlowAllowed(tc.network, tc.security, tc.settings); got != tc.want {
+				t.Fatalf("vlessFlowAllowed(%q, %q, %v) = %v, want %v", tc.network, tc.security, tc.settings, got, tc.want)
+			}
+		})
+	}
+}
+
+func flowTestInbound(streamSettings, encryption string) *model.Inbound {
+	return &model.Inbound{
+		Listen:   "203.0.113.1",
+		Port:     443,
+		Protocol: model.VLESS,
+		Remark:   "flowtest",
+		Settings: `{"clients":[{"id":"11111111-2222-4333-8444-555555555555","email":"user","flow":"xtls-rprx-vision"}],` +
+			`"decryption":"` + encryption + `","encryption":"` + encryption + `"}`,
+		StreamSettings: streamSettings,
+	}
+}
+
+const xhttpRealityStream = `{
+	"network": "xhttp",
+	"security": "reality",
+	"xhttpSettings": {"path": "/", "mode": "auto"},
+	"realitySettings": {
+		"serverNames": ["example.com"],
+		"shortIds": ["abcd"],
+		"settings": {"publicKey": "pub", "fingerprint": "chrome"}
+	}
+}`
+
+func TestGenVlessLink_FlowXhttpRealityVlessenc(t *testing.T) {
+	s := &SubService{remarkModel: "-ieo"}
+	link := s.genVlessLink(flowTestInbound(xhttpRealityStream, testMlkemEncryption), "user")
+	if !strings.Contains(link, "flow=xtls-rprx-vision") {
+		t.Fatalf("xhttp+reality+vlessenc link must carry the vision flow (#5232), got %q", link)
+	}
+}
+
+func TestGenVlessLink_NoFlowXhttpRealityWithoutVlessenc(t *testing.T) {
+	s := &SubService{remarkModel: "-ieo"}
+	link := s.genVlessLink(flowTestInbound(xhttpRealityStream, "none"), "user")
+	if strings.Contains(link, "flow=") {
+		t.Fatalf("xhttp+reality without vlessenc must not carry a flow, got %q", link)
+	}
+}
+
+func TestGenVlessLink_FlowTcpRealityStillWorks(t *testing.T) {
+	stream := `{
+		"network": "tcp",
+		"security": "reality",
+		"tcpSettings": {"header": {"type": "none"}},
+		"realitySettings": {
+			"serverNames": ["example.com"],
+			"shortIds": ["abcd"],
+			"settings": {"publicKey": "pub", "fingerprint": "chrome"}
+		}
+	}`
+	s := &SubService{remarkModel: "-ieo"}
+	link := s.genVlessLink(flowTestInbound(stream, "none"), "user")
+	if !strings.Contains(link, "flow=xtls-rprx-vision") {
+		t.Fatalf("tcp+reality link must keep the vision flow, got %q", link)
+	}
+}

+ 15 - 0
internal/sub/service_test.go

@@ -26,6 +26,21 @@ func TestSubscriptionExpiryFromClient(t *testing.T) {
 	}
 }
 
+// The name an admin gives a node is panel-internal and must not leak into
+// the remarks end users see in their client apps (#5231) — not even for
+// node-hosted inbounds, which briefly carried a node-name suffix (#5035).
+func TestGenRemarkOmitsNodeName(t *testing.T) {
+	nodeID := 7
+	s := &SubService{
+		remarkModel: "-ieo",
+		nodesByID:   map[int]*model.Node{7: {Id: 7, Name: "Berlin", Address: "node7.example.com"}},
+	}
+	ib := &model.Inbound{Remark: "vless-tcp", NodeID: &nodeID}
+	if got := s.genRemark(ib, "", ""); got != "vless-tcp" {
+		t.Fatalf("remark = %q, want %q (node name must not leak into client-visible remarks)", got, "vless-tcp")
+	}
+}
+
 func TestFindClientIndex(t *testing.T) {
 	clients := []model.Client{
 		{Email: "[email protected]"},

+ 1 - 1
main.go

@@ -418,7 +418,7 @@ func GetApiToken(getApiToken bool) {
 	if len(tokens) > 0 {
 		fmt.Printf("There are %d API token(s) configured. Existing tokens cannot be retrieved in plaintext because only hashes are stored.\n", len(tokens))
 		fmt.Println("If you have lost your token, you can manage and generate new tokens through the Panel UI (Settings -> API Tokens).")
-		
+
 		// Create a new fallback token so the CLI is still useful without the UI
 		fallbackName := fmt.Sprintf("cli-fallback-%d", time.Now().Unix())
 		created, err := apiTokenService.Create(fallbackName)

+ 42 - 18
x-ui.sh

@@ -1148,9 +1148,11 @@ delete_ports() {
 }
 
 update_all_geofiles() {
-    update_geofiles "main"
-    update_geofiles "IR"
-    update_geofiles "RU"
+    local failed=0
+    update_geofiles "main" || failed=1
+    update_geofiles "IR" || failed=1
+    update_geofiles "RU" || failed=1
+    return $failed
 }
 
 update_geofiles() {
@@ -1168,12 +1170,39 @@ update_geofiles() {
             dat_source="runetfreedom/russia-v2ray-rules-dat"
             ;;
     esac
+    local failed=0 http_code
     for dat in "${dat_files[@]}"; do
         # Remove suffix for remote filename (e.g., geoip_IR -> geoip)
         remote_file="${dat%%_*}"
-        curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \
-            https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat
+        # -z skips the download (server answers 304) when the local copy is already current
+        http_code=$(curl -sSfLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat -w '%{http_code}' \
+            https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat)
+        if [[ $? -ne 0 ]]; then
+            echo -e "${red}${dat}.dat: download failed${plain}"
+            failed=1
+        elif [[ "$http_code" == "304" ]]; then
+            echo -e "${dat}.dat: already up to date"
+        else
+            echo -e "${green}${dat}.dat: updated${plain}"
+            geo_updated=1
+        fi
     done
+    return $failed
+}
+
+run_geo_update() {
+    local name="$1"
+    shift
+    geo_updated=0
+    "$@"
+    if [[ $? -ne 0 ]]; then
+        echo -e "${red}Some ${name} could not be updated. Check the errors above.${plain}"
+    elif [[ $geo_updated -eq 1 ]]; then
+        echo -e "${green}${name} have been updated successfully!${plain}"
+        restart
+    else
+        echo -e "${green}${name} are already up to date, restart is not needed.${plain}"
+    fi
 }
 
 update_geo() {
@@ -1189,24 +1218,16 @@ update_geo() {
             show_menu
             ;;
         1)
-            update_geofiles "main"
-            echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
-            restart
+            run_geo_update "Loyalsoldier datasets" update_geofiles "main"
             ;;
         2)
-            update_geofiles "IR"
-            echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
-            restart
+            run_geo_update "chocolate4u datasets" update_geofiles "IR"
             ;;
         3)
-            update_geofiles "RU"
-            echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
-            restart
+            run_geo_update "runetfreedom datasets" update_geofiles "RU"
             ;;
         4)
-            update_all_geofiles
-            echo -e "${green}All geo files have been updated successfully!${plain}"
-            restart
+            run_geo_update "geo files" update_all_geofiles
             ;;
         *)
             echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
@@ -3254,7 +3275,10 @@ if [[ $# > 0 ]]; then
             check_install 0 && uninstall 0
             ;;
         "update-all-geofiles")
-            check_install 0 && update_all_geofiles 0 && restart 0
+            geo_updated=0
+            if check_install 0 && update_all_geofiles 0; then
+                [[ $geo_updated -eq 0 ]] || restart 0
+            fi
             ;;
         "migrateDB")
             migrate_db "$2" "$3"