1
0

7 کامیت‌ها 08bc481ae3 ... b770287995

نویسنده SHA1 پیام تاریخ
  MHSanaei b770287995 fix(sub): stop appending the node name to subscription remarks (#5231) 1 روز پیش
  MHSanaei 3c68b039f6 fix(sub): deliver vision flow for VLESS+XHTTP+REALITY in share links and Clash (#5232) 1 روز پیش
  MHSanaei c200e248f7 fix(script): report per-file geo update status and skip restart when nothing changed 1 روز پیش
  MHSanaei b5ef412b8d v3.3.1 1 روز پیش
  MHSanaei 41cb0b8ae7 fix(inbounds): show remark first, else inbound tag, in client labels 1 روز پیش
  MHSanaei cd46730bb9 Bump Go indirect deps; update frontend lock 1 روز پیش
  MHSanaei 4eab37b66c feat(clients): restore reset traffic button in edit client form 1 روز پیش

+ 98 - 95
frontend/package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "3x-ui-frontend",
   "name": "3x-ui-frontend",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "3x-ui-frontend",
       "name": "3x-ui-frontend",
-      "version": "0.3.0",
+      "version": "0.3.1",
       "dependencies": {
       "dependencies": {
         "@ant-design/icons": "^6.2.5",
         "@ant-design/icons": "^6.2.5",
         "@codemirror/lang-json": "^6.0.2",
         "@codemirror/lang-json": "^6.0.2",
@@ -518,9 +518,9 @@
       }
       }
     },
     },
     "node_modules/@codemirror/lint": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@codemirror/state": "^6.0.0",
         "@codemirror/state": "^6.0.0",
@@ -561,9 +561,9 @@
       }
       }
     },
     },
     "node_modules/@codemirror/view": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@codemirror/state": "^6.6.0",
         "@codemirror/state": "^6.6.0",
@@ -617,9 +617,9 @@
       }
       }
     },
     },
     "node_modules/@csstools/css-color-parser": {
     "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,
       "dev": true,
       "funding": [
       "funding": [
         {
         {
@@ -1062,14 +1062,14 @@
       "license": "MIT"
       "license": "MIT"
     },
     },
     "node_modules/@napi-rs/wasm-runtime": {
     "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,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
       "optional": true,
       "optional": true,
       "dependencies": {
       "dependencies": {
-        "@tybys/wasm-util": "^0.10.1"
+        "@tybys/wasm-util": "^0.10.2"
       },
       },
       "funding": {
       "funding": {
         "type": "github",
         "type": "github",
@@ -1103,9 +1103,9 @@
       }
       }
     },
     },
     "node_modules/@rc-component/async-validator": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.24.4"
         "@babel/runtime": "^7.24.4"
@@ -1115,14 +1115,14 @@
       }
       }
     },
     },
     "node_modules/@rc-component/cascader": {
     "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",
       "license": "MIT",
       "dependencies": {
       "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"
         "clsx": "^2.1.1"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
@@ -1236,12 +1236,12 @@
       }
       }
     },
     },
     "node_modules/@rc-component/form": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "@rc-component/async-validator": "^5.1.0",
+        "@rc-component/async-validator": "^6.0.0",
         "@rc-component/util": "^1.11.1",
         "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
         "clsx": "^2.1.1"
       },
       },
@@ -1410,12 +1410,12 @@
       }
       }
     },
     },
     "node_modules/@rc-component/pagination": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "@rc-component/util": "^1.3.0",
+        "@rc-component/util": "^1.11.1",
         "clsx": "^2.1.1"
         "clsx": "^2.1.1"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
@@ -1493,9 +1493,9 @@
       }
       }
     },
     },
     "node_modules/@rc-component/qrcode": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@babel/runtime": "^7.24.7"
         "@babel/runtime": "^7.24.7"
@@ -1555,15 +1555,15 @@
       }
       }
     },
     },
     "node_modules/@rc-component/select": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@rc-component/overflow": "^1.0.0",
         "@rc-component/overflow": "^1.0.0",
         "@rc-component/trigger": "^3.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"
         "clsx": "^2.1.1"
       },
       },
       "engines": {
       "engines": {
@@ -1717,12 +1717,12 @@
       }
       }
     },
     },
     "node_modules/@rc-component/tree-select": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "@rc-component/select": "~1.6.0",
+        "@rc-component/select": "~1.7.0",
         "@rc-component/tree": "~1.3.0",
         "@rc-component/tree": "~1.3.0",
         "@rc-component/util": "^1.4.0",
         "@rc-component/util": "^1.4.0",
         "clsx": "^2.1.1"
         "clsx": "^2.1.1"
@@ -3284,9 +3284,9 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
     "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,
       "dev": true,
       "license": "ISC",
       "license": "ISC",
       "bin": {
       "bin": {
@@ -3478,9 +3478,9 @@
       }
       }
     },
     },
     "node_modules/acorn": {
     "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,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
       "bin": {
       "bin": {
@@ -3553,54 +3553,54 @@
       }
       }
     },
     },
     "node_modules/antd": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@ant-design/colors": "^8.0.1",
         "@ant-design/colors": "^8.0.1",
         "@ant-design/cssinjs": "^2.1.2",
         "@ant-design/cssinjs": "^2.1.2",
         "@ant-design/cssinjs-utils": "^2.1.2",
         "@ant-design/cssinjs-utils": "^2.1.2",
         "@ant-design/fast-color": "^3.0.1",
         "@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",
         "@ant-design/react-slick": "~2.0.0",
         "@babel/runtime": "^7.29.2",
         "@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/checkbox": "~2.0.0",
         "@rc-component/collapse": "~1.2.0",
         "@rc-component/collapse": "~1.2.0",
         "@rc-component/color-picker": "~3.1.1",
         "@rc-component/color-picker": "~3.1.1",
         "@rc-component/dialog": "~1.9.0",
         "@rc-component/dialog": "~1.9.0",
         "@rc-component/drawer": "~1.4.2",
         "@rc-component/drawer": "~1.4.2",
         "@rc-component/dropdown": "~1.0.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/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/input-number": "~1.6.2",
         "@rc-component/mentions": "~1.9.0",
         "@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/mutate-observer": "^2.0.1",
         "@rc-component/notification": "~2.0.7",
         "@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/picker": "~1.10.0",
         "@rc-component/progress": "~1.0.2",
         "@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/rate": "~1.0.1",
         "@rc-component/resize-observer": "^1.1.2",
         "@rc-component/resize-observer": "^1.1.2",
         "@rc-component/segmented": "~1.3.0",
         "@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/slider": "~1.0.1",
         "@rc-component/steps": "~1.2.2",
         "@rc-component/steps": "~1.2.2",
         "@rc-component/switch": "~1.0.3",
         "@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/tooltip": "~1.4.0",
         "@rc-component/tour": "~2.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",
         "clsx": "^2.1.1",
         "dayjs": "^1.11.11",
         "dayjs": "^1.11.11",
         "scroll-into-view-if-needed": "^3.1.0",
         "scroll-into-view-if-needed": "^3.1.0",
@@ -3719,9 +3719,9 @@
       "license": "MIT"
       "license": "MIT"
     },
     },
     "node_modules/baseline-browser-mapping": {
     "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,
       "dev": true,
       "license": "Apache-2.0",
       "license": "Apache-2.0",
       "bin": {
       "bin": {
@@ -3859,9 +3859,9 @@
       }
       }
     },
     },
     "node_modules/caniuse-lite": {
     "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,
       "dev": true,
       "funding": [
       "funding": [
         {
         {
@@ -4327,9 +4327,9 @@
       "license": "MIT"
       "license": "MIT"
     },
     },
     "node_modules/dompurify": {
     "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)",
       "license": "(MPL-2.0 OR Apache-2.0)",
       "optionalDependencies": {
       "optionalDependencies": {
         "@types/trusted-types": "^2.0.7"
         "@types/trusted-types": "^2.0.7"
@@ -4359,9 +4359,9 @@
       }
       }
     },
     },
     "node_modules/electron-to-chromium": {
     "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,
       "dev": true,
       "license": "ISC"
       "license": "ISC"
     },
     },
@@ -4431,9 +4431,9 @@
       }
       }
     },
     },
     "node_modules/es-toolkit": {
     "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",
       "license": "MIT",
       "workspaces": [
       "workspaces": [
         "docs",
         "docs",
@@ -4464,11 +4464,14 @@
       }
       }
     },
     },
     "node_modules/eslint": {
     "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,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
+      "workspaces": [
+        "packages/*"
+      ],
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/regexpp": "^4.12.2",
         "@eslint-community/regexpp": "^4.12.2",
@@ -4806,16 +4809,16 @@
       }
       }
     },
     },
     "node_modules/form-data": {
     "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",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "asynckit": "^0.4.0",
         "asynckit": "^0.4.0",
         "combined-stream": "^1.0.8",
         "combined-stream": "^1.0.8",
         "es-set-tostringtag": "^2.1.0",
         "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": {
       "engines": {
         "node": ">= 6"
         "node": ">= 6"
@@ -5994,9 +5997,9 @@
       }
       }
     },
     },
     "node_modules/obug": {
     "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,
       "dev": true,
       "funding": [
       "funding": [
         "https://github.com/sponsors/sxzz",
         "https://github.com/sponsors/sxzz",

+ 2 - 2
frontend/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "3x-ui-frontend",
   "name": "3x-ui-frontend",
   "private": true,
   "private": true,
-  "version": "0.3.0",
+  "version": "0.3.1",
   "type": "module",
   "type": "module",
   "description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
   "description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
   "engines": {
   "engines": {
@@ -73,4 +73,4 @@
     "core-js-pure": false,
     "core-js-pure": false,
     "tree-sitter-json": 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 {
 export function formatInboundLabel(tag?: string, remark?: string): string {
-  const tagText = (tag || '').trim();
   const remarkText = (remark || '').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,
   Input,
   InputNumber,
   InputNumber,
   Modal,
   Modal,
+  Popconfirm,
   Row,
   Row,
   Select,
   Select,
   Space,
   Space,
@@ -17,7 +18,7 @@ import {
   Tooltip,
   Tooltip,
   message,
   message,
 } from 'antd';
 } from 'antd';
-import { EyeOutlined, ReloadOutlined } from '@ant-design/icons';
+import { EyeOutlined, ReloadOutlined, RetweetOutlined } from '@ant-design/icons';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import type { Dayjs } from 'dayjs';
 import type { Dayjs } from 'dayjs';
 import { HttpUtil, RandomUtil } from '@/utils';
 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> {
 interface ApiMsg<T = unknown> {
   success?: boolean;
   success?: boolean;
+  msg?: string;
   obj?: T;
   obj?: T;
 }
 }
 
 
@@ -72,6 +74,7 @@ interface ClientFormModalProps {
     payload: Record<string, unknown> | SaveCreatePayload,
     payload: Record<string, unknown> | SaveCreatePayload,
     meta: SaveMetaEdit | SaveMetaCreate,
     meta: SaveMetaEdit | SaveMetaCreate,
   ) => Promise<ApiMsg | null>;
   ) => Promise<ApiMsg | null>;
+  resetTraffic?: (client: ClientRecord) => Promise<ApiMsg | null>;
   onOpenChange: (open: boolean) => void;
   onOpenChange: (open: boolean) => void;
 }
 }
 
 
@@ -140,6 +143,7 @@ export default function ClientFormModal({
   tgBotEnable = false,
   tgBotEnable = false,
   groups = [],
   groups = [],
   save,
   save,
+  resetTraffic,
   onOpenChange,
   onOpenChange,
 }: ClientFormModalProps) {
 }: ClientFormModalProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -148,6 +152,7 @@ export default function ClientFormModal({
 
 
   const [form, setForm] = useState<FormState>(emptyForm);
   const [form, setForm] = useState<FormState>(emptyForm);
   const [submitting, setSubmitting] = useState(false);
   const [submitting, setSubmitting] = useState(false);
+  const [resetting, setResetting] = useState(false);
   const [clientIps, setClientIps] = useState<string[]>([]);
   const [clientIps, setClientIps] = useState<string[]>([]);
   const [ipsLoading, setIpsLoading] = useState(false);
   const [ipsLoading, setIpsLoading] = useState(false);
   const [ipsClearing, setIpsClearing] = useState(false);
   const [ipsClearing, setIpsClearing] = useState(false);
@@ -328,6 +333,21 @@ export default function ClientFormModal({
     onOpenChange(false);
     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() {
   async function onSubmit() {
     const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
     const schema = isEdit ? ClientFormSchema : ClientCreateFormSchema;
     const validated = schema.safeParse({
     const validated = schema.safeParse({
@@ -413,15 +433,35 @@ export default function ClientFormModal({
         open={open}
         open={open}
         title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
         title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')}
         destroyOnHidden
         destroyOnHidden
-        okText={isEdit ? t('save') : t('create')}
-        cancelText={t('cancel')}
-        okButtonProps={{ loading: submitting }}
         width={720}
         width={720}
         zIndex={CLIENT_FORM_MODAL_Z_INDEX}
         zIndex={CLIENT_FORM_MODAL_Z_INDEX}
         style={{ top: 20 }}
         style={{ top: 20 }}
         styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
         styles={{ body: { maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', overflowX: 'hidden' } }}
-        onOk={onSubmit}
         onCancel={close}
         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">
         <Form layout="vertical">
           <Tabs
           <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 }[] = [
 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];
 const DEFAULT_SORT = SORT_OPTIONS[0];
@@ -743,6 +743,7 @@ export default function ClientsPage() {
     {
     {
       title: t('pages.clients.traffic'),
       title: t('pages.clients.traffic'),
       key: 'traffic',
       key: 'traffic',
+      width: 300,
       render: (_v, record) => (
       render: (_v, record) => (
         <ClientTrafficCell
         <ClientTrafficCell
           up={record.traffic?.up}
           up={record.traffic?.up}
@@ -924,40 +925,40 @@ export default function ClientsPage() {
                             menu={{
                             menu={{
                               items: selectedRowKeys.length > 0
                               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 />}>
                             <Button icon={<MoreOutlined />}>
@@ -1246,6 +1247,7 @@ export default function ClientsPage() {
             tgBotEnable={tgBotEnable}
             tgBotEnable={tgBotEnable}
             groups={allGroups}
             groups={allGroups}
             save={onSave}
             save={onSave}
+            resetTraffic={resetTraffic}
             onOpenChange={setFormOpen}
             onOpenChange={setFormOpen}
           />
           />
         </LazyMount>
         </LazyMount>

+ 3 - 3
go.mod

@@ -102,15 +102,15 @@ require (
 	go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
 	go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
 	golang.org/x/arch v0.28.0 // 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/mod v0.37.0 // indirect
 	golang.org/x/net v0.56.0
 	golang.org/x/net v0.56.0
 	golang.org/x/sync v0.21.0 // indirect
 	golang.org/x/sync v0.21.0 // indirect
 	golang.org/x/time v0.15.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/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // 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
 	google.golang.org/protobuf v1.36.11 // indirect
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
 	lukechampine.com/blake3 v1.4.1 // 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/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 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
 golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
 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 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
 golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
 golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
 golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
 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/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 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
 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 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
 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=
 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=
 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 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
 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 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
 google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
 google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
 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:
 	case model.VLESS:
 		proxy["type"] = "vless"
 		proxy["type"] = "vless"
 		proxy["uuid"] = client.ID
 		proxy["uuid"] = client.ID
-		if client.Flow != "" && network == "tcp" {
-			proxy["flow"] = client.Flow
-		}
 		var inboundSettings map[string]any
 		var inboundSettings map[string]any
 		json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
 		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 {
 		if encryption, ok := inboundSettings["encryption"].(string); ok {
 			encryption = strings.TrimSpace(encryption)
 			encryption = strings.TrimSpace(encryption)
 			if encryption != "" && encryption != "none" {
 			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) {
 func TestBuildProxy_VLESSNoneEncryptionOmittedForClash(t *testing.T) {
 	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
 	svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
 	inbound := &model.Inbound{
 	inbound := &model.Inbound{

+ 20 - 24
internal/sub/service.go

@@ -484,6 +484,23 @@ func vlessEncryptionEnabled(settings map[string]any) bool {
 	return false
 	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 {
 func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	if inbound.Protocol != model.VLESS {
 	if inbound.Protocol != model.VLESS {
 		return ""
 		return ""
@@ -513,21 +530,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
 	switch security {
 	switch security {
 	case "tls":
 	case "tls":
 		applyShareTLSParams(stream, params)
 		applyShareTLSParams(stream, params)
-		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
-			params["flow"] = clients[clientIndex].Flow
-		}
 	case "reality":
 	case "reality":
 		applyShareRealityParams(stream, params)
 		applyShareRealityParams(stream, params)
-		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
-			params["flow"] = clients[clientIndex].Flow
-		}
 	default:
 	default:
 		params["security"] = "none"
 		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)
 	externalProxies, _ := stream["externalProxy"].([]any)
@@ -1519,19 +1528,6 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
 	if len(extra) > 0 {
 	if len(extra) > 0 {
 		orders['o'] = extra
 		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
 	var remark []string
 	for i := 0; i < len(orderChars); i++ {
 	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) {
 func TestFindClientIndex(t *testing.T) {
 	clients := []model.Client{
 	clients := []model.Client{
 		{Email: "[email protected]"},
 		{Email: "[email protected]"},

+ 1 - 1
main.go

@@ -418,7 +418,7 @@ func GetApiToken(getApiToken bool) {
 	if len(tokens) > 0 {
 	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.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).")
 		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
 		// Create a new fallback token so the CLI is still useful without the UI
 		fallbackName := fmt.Sprintf("cli-fallback-%d", time.Now().Unix())
 		fallbackName := fmt.Sprintf("cli-fallback-%d", time.Now().Unix())
 		created, err := apiTokenService.Create(fallbackName)
 		created, err := apiTokenService.Create(fallbackName)

+ 42 - 18
x-ui.sh

@@ -1148,9 +1148,11 @@ delete_ports() {
 }
 }
 
 
 update_all_geofiles() {
 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() {
 update_geofiles() {
@@ -1168,12 +1170,39 @@ update_geofiles() {
             dat_source="runetfreedom/russia-v2ray-rules-dat"
             dat_source="runetfreedom/russia-v2ray-rules-dat"
             ;;
             ;;
     esac
     esac
+    local failed=0 http_code
     for dat in "${dat_files[@]}"; do
     for dat in "${dat_files[@]}"; do
         # Remove suffix for remote filename (e.g., geoip_IR -> geoip)
         # Remove suffix for remote filename (e.g., geoip_IR -> geoip)
         remote_file="${dat%%_*}"
         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
     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() {
 update_geo() {
@@ -1189,24 +1218,16 @@ update_geo() {
             show_menu
             show_menu
             ;;
             ;;
         1)
         1)
-            update_geofiles "main"
-            echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
-            restart
+            run_geo_update "Loyalsoldier datasets" update_geofiles "main"
             ;;
             ;;
         2)
         2)
-            update_geofiles "IR"
-            echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
-            restart
+            run_geo_update "chocolate4u datasets" update_geofiles "IR"
             ;;
             ;;
         3)
         3)
-            update_geofiles "RU"
-            echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
-            restart
+            run_geo_update "runetfreedom datasets" update_geofiles "RU"
             ;;
             ;;
         4)
         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"
             echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
@@ -3254,7 +3275,10 @@ if [[ $# > 0 ]]; then
             check_install 0 && uninstall 0
             check_install 0 && uninstall 0
             ;;
             ;;
         "update-all-geofiles")
         "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")
         "migrateDB")
             migrate_db "$2" "$3"
             migrate_db "$2" "$3"