4 Revize 51e2fb6dbf ... 2b83dc047b

Autor SHA1 Zpráva Datum
  MHSanaei 2b83dc047b Bump Go module dependency versions před 1 dnem
  MHSanaei c90f8a05bf fix(security): sanitize remote IP headers and escape log viewer output před 1 dnem
  MHSanaei 9f96ef83ec Freedom outbound: Add finalRules před 1 dnem
  MHSanaei e19061d513 TLS: Remove ECH Force Query před 1 dnem
71 změnil soubory, kde provedl 3927 přidání a 3974 odebrání
  1. 1 1
      DockerInit.sh
  2. 10 10
      go.mod
  3. 30 30
      go.sum
  4. 223 223
      install.sh
  5. 283 283
      update.sh
  6. 0 4
      web/assets/js/model/inbound.js
  7. 54 4
      web/assets/js/model/outbound.js
  8. 52 10
      web/controller/util.go
  9. 39 26
      web/html/common/page.html
  10. 60 27
      web/html/component/aClientTable.html
  11. 11 9
      web/html/component/aCustomStatistic.html
  12. 4 3
      web/html/component/aPersianDatepicker.html
  13. 2 2
      web/html/component/aSettingListItem.html
  14. 4 5
      web/html/component/aSidebar.html
  15. 11 10
      web/html/component/aTableSortable.html
  16. 6 4
      web/html/component/aThemeSwitch.html
  17. 29 98
      web/html/form/client.html
  18. 14 14
      web/html/form/inbound.html
  19. 205 476
      web/html/form/outbound.html
  20. 3 3
      web/html/form/protocol/dokodemo.html
  21. 3 2
      web/html/form/protocol/http.html
  22. 4 8
      web/html/form/protocol/hysteria.html
  23. 13 33
      web/html/form/protocol/shadowsocks.html
  24. 11 38
      web/html/form/protocol/socks.html
  25. 32 32
      web/html/form/protocol/trojan.html
  26. 12 48
      web/html/form/protocol/tun.html
  27. 102 103
      web/html/form/protocol/vless.html
  28. 3 3
      web/html/form/protocol/vmess.html
  29. 7 23
      web/html/form/sniffing.html
  30. 12 48
      web/html/form/stream/external_proxy.html
  31. 297 401
      web/html/form/stream/stream_finalmask.html
  32. 2 6
      web/html/form/stream/stream_grpc.html
  33. 12 37
      web/html/form/stream/stream_httpupgrade.html
  34. 19 61
      web/html/form/stream/stream_hysteria.html
  35. 8 32
      web/html/form/stream/stream_kcp.html
  36. 14 22
      web/html/form/stream/stream_settings.html
  37. 19 60
      web/html/form/stream/stream_sockopt.html
  38. 30 91
      web/html/form/stream/stream_tcp.html
  39. 10 34
      web/html/form/stream/stream_ws.html
  40. 43 129
      web/html/form/stream/stream_xhttp.html
  41. 12 31
      web/html/form/tls_settings.html
  42. 251 266
      web/html/inbounds.html
  43. 299 148
      web/html/index.html
  44. 22 6
      web/html/login.html
  45. 46 70
      web/html/modals/client_bulk_modal.html
  46. 43 28
      web/html/modals/client_modal.html
  47. 10 5
      web/html/modals/dns_presets_modal.html
  48. 31 10
      web/html/modals/inbound_info_modal.html
  49. 16 18
      web/html/modals/inbound_modal.html
  50. 45 24
      web/html/modals/nord_modal.html
  51. 7 12
      web/html/modals/prompt_modal.html
  52. 22 17
      web/html/modals/qrcode_modal.html
  53. 3 3
      web/html/modals/text_modal.html
  54. 29 29
      web/html/modals/two_factor_modal.html
  55. 12 10
      web/html/modals/warp_modal.html
  56. 18 19
      web/html/modals/xray_balancer_modal.html
  57. 13 5
      web/html/modals/xray_dns_modal.html
  58. 13 4
      web/html/modals/xray_fakedns_modal.html
  59. 30 24
      web/html/modals/xray_outbound_modal.html
  60. 60 34
      web/html/modals/xray_reverse_modal.html
  61. 21 10
      web/html/modals/xray_rule_modal.html
  62. 174 65
      web/html/settings.html
  63. 14 7
      web/html/settings/panel/general.html
  64. 6 12
      web/html/settings/panel/subscription/general.html
  65. 7 6
      web/html/settings/panel/subscription/subpage.html
  66. 5 3
      web/html/settings/xray/balancers.html
  67. 35 70
      web/html/settings/xray/basics.html
  68. 2 1
      web/html/settings/xray/dns.html
  69. 16 35
      web/html/settings/xray/outbounds.html
  70. 620 207
      web/html/xray.html
  71. 351 342
      x-ui.sh

+ 1 - 1
DockerInit.sh

@@ -37,4 +37,4 @@ curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/release
 curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
 curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
 curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
-cd ../../
+cd ../../

+ 10 - 10
go.mod

@@ -15,9 +15,9 @@ require (
 	github.com/mymmrac/telego v1.8.0
 	github.com/nicksnyder/go-i18n/v2 v2.6.1
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
-	github.com/pelletier/go-toml/v2 v2.3.0
+	github.com/pelletier/go-toml/v2 v2.3.1
 	github.com/robfig/cron/v3 v3.0.1
-	github.com/shirou/gopsutil/v4 v4.26.3
+	github.com/shirou/gopsutil/v4 v4.26.4
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/valyala/fasthttp v1.70.0
 	github.com/xlzd/gotp v0.1.0
@@ -26,7 +26,7 @@ require (
 	golang.org/x/crypto v0.50.0
 	golang.org/x/sys v0.43.0
 	golang.org/x/text v0.36.0
-	google.golang.org/grpc v1.80.0
+	google.golang.org/grpc v1.81.0
 	gorm.io/driver/sqlite v1.6.0
 	gorm.io/gorm v1.31.1
 )
@@ -36,10 +36,10 @@ require (
 	github.com/andybalholm/brotli v1.2.1 // indirect
 	github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
 	github.com/bytedance/gopkg v0.1.4 // indirect
-	github.com/bytedance/sonic v1.15.0 // indirect
+	github.com/bytedance/sonic v1.15.1 // indirect
 	github.com/bytedance/sonic/loader v0.5.1 // indirect
 	github.com/cloudflare/circl v1.6.3 // indirect
-	github.com/cloudwego/base64x v0.1.6 // indirect
+	github.com/cloudwego/base64x v0.1.7 // indirect
 	github.com/ebitengine/purego v0.10.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.13 // indirect
 	github.com/gin-contrib/sse v1.1.1 // indirect
@@ -57,12 +57,12 @@ require (
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/juju/ratelimit v1.0.2 // indirect
-	github.com/klauspost/compress v1.18.5 // indirect
+	github.com/klauspost/compress v1.18.6 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
-	github.com/mattn/go-isatty v0.0.21 // indirect
-	github.com/mattn/go-sqlite3 v1.14.42 // indirect
+	github.com/mattn/go-isatty v0.0.22 // indirect
+	github.com/mattn/go-sqlite3 v1.14.44 // indirect
 	github.com/miekg/dns v1.1.72 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -84,7 +84,7 @@ require (
 	github.com/vishvananda/netns v0.0.5 // indirect
 	github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
-	go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
+	go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
 	golang.org/x/arch v0.26.0 // indirect
 	golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
@@ -95,7 +95,7 @@ require (
 	golang.org/x/tools v0.44.0 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
 	golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // 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

+ 30 - 30
go.sum

@@ -10,16 +10,16 @@ github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEc
 github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
 github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
 github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
-github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
-github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
+github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
+github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
 github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
 github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
 github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
-github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
-github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
+github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -107,8 +107,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
 github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
-github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
-github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -119,10 +119,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
 github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
-github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
-github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
-github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
-github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
+github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
 github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
 github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -138,8 +138,8 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0C
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
-github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
+github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
 github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -160,8 +160,8 @@ github.com/sagernet/sing v0.8.9 h1:iX8FyMrWNl/divVgTe7cLT9n36v6bfzfnCYlcM1cLaU=
 github.com/sagernet/sing v0.8.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
 github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
 github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
-github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
-github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
+github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
+github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -203,20 +203,20 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
-go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
-go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
+go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
+go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
-go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
-go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
-go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
-go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
-go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
-go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
-go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
-go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
-go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
+go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
+go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
+go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
+go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
+go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
+go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
+go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
+go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
+go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -256,10 +256,10 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
 golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
 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-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
-google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
+google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
 google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 223 - 223
install.sh

@@ -18,7 +18,7 @@ xui_service="${XUI_SERVICE:=/etc/systemd/system}"
 if [[ -f /etc/os-release ]]; then
     source /etc/os-release
     release=$ID
-    elif [[ -f /usr/lib/os-release ]]; then
+elif [[ -f /usr/lib/os-release ]]; then
     source /usr/lib/os-release
     release=$ID
 else
@@ -59,16 +59,16 @@ is_domain() {
 # Port helpers
 is_port_in_use() {
     local port="$1"
-    if command -v ss >/dev/null 2>&1; then
-        ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
+    if command -v ss > /dev/null 2>&1; then
+        ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
         return
     fi
-    if command -v netstat >/dev/null 2>&1; then
-        netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
+    if command -v netstat > /dev/null 2>&1; then
+        netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
         return
     fi
-    if command -v lsof >/dev/null 2>&1; then
-        lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
+    if command -v lsof > /dev/null 2>&1; then
+        lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0
     fi
     return 1
 }
@@ -77,35 +77,35 @@ install_base() {
     case "${release}" in
         ubuntu | debian | armbian)
             apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
-        ;;
+            ;;
         fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
             dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
-        ;;
+            ;;
         centos)
             if [[ "${VERSION_ID}" =~ ^7 ]]; then
                 yum -y update && yum install -y cronie curl tar tzdata socat ca-certificates openssl
             else
                 dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
             fi
-        ;;
+            ;;
         arch | manjaro | parch)
             pacman -Syu && pacman -Syu --noconfirm cronie curl tar tzdata socat ca-certificates openssl
-        ;;
+            ;;
         opensuse-tumbleweed | opensuse-leap)
             zypper refresh && zypper -q install -y cron curl tar timezone socat ca-certificates openssl
-        ;;
+            ;;
         alpine)
             apk update && apk add dcron curl tar tzdata socat ca-certificates openssl
-        ;;
+            ;;
         *)
             apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
-        ;;
+            ;;
     esac
 }
 
 gen_random_string() {
     local length="$1"
-    openssl rand -base64 $(( length * 2 )) \
+    openssl rand -base64 $((length * 2)) \
         | tr -dc 'a-zA-Z0-9' \
         | head -c "$length"
 }
@@ -113,7 +113,7 @@ gen_random_string() {
 install_acme() {
     echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
     cd ~ || return 1
-    curl -s https://get.acme.sh | sh >/dev/null 2>&1
+    curl -s https://get.acme.sh | sh > /dev/null 2>&1
     if [ $? -ne 0 ]; then
         echo -e "${red}Failed to install acme.sh${plain}"
         return 1
@@ -128,60 +128,60 @@ setup_ssl_certificate() {
     local server_ip="$2"
     local existing_port="$3"
     local existing_webBasePath="$4"
-    
+
     echo -e "${green}Setting up SSL certificate...${plain}"
-    
+
     # Check if acme.sh is installed
-    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
+    if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
         install_acme
         if [ $? -ne 0 ]; then
             echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
             return 1
         fi
     fi
-    
+
     # Create certificate directory
     local certPath="/root/cert/${domain}"
     mkdir -p "$certPath"
-    
+
     # Issue certificate
     echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
     echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
-    
-    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
+
+    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
     ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
-    
+
     if [ $? -ne 0 ]; then
         echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
         echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
-        rm -rf ~/.acme.sh/${domain} 2>/dev/null
-        rm -rf "$certPath" 2>/dev/null
+        rm -rf ~/.acme.sh/${domain} 2> /dev/null
+        rm -rf "$certPath" 2> /dev/null
         return 1
     fi
-    
+
     # Install certificate
     ~/.acme.sh/acme.sh --installcert -d ${domain} \
         --key-file /root/cert/${domain}/privkey.pem \
         --fullchain-file /root/cert/${domain}/fullchain.pem \
-        --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
-    
+        --reloadcmd "systemctl restart x-ui" > /dev/null 2>&1
+
     if [ $? -ne 0 ]; then
         echo -e "${yellow}Failed to install certificate${plain}"
         return 1
     fi
-    
+
     # Enable auto-renew
-    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
+    ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
     # Secure permissions: private key readable only by owner
-    chmod 600 $certPath/privkey.pem 2>/dev/null
-    chmod 644 $certPath/fullchain.pem 2>/dev/null
-    
+    chmod 600 $certPath/privkey.pem 2> /dev/null
+    chmod 644 $certPath/fullchain.pem 2> /dev/null
+
     # Set certificate for panel
     local webCertFile="/root/cert/${domain}/fullchain.pem"
     local webKeyFile="/root/cert/${domain}/privkey.pem"
-    
+
     if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
-        ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
+        ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" > /dev/null 2>&1
         echo -e "${green}SSL certificate installed and configured successfully!${plain}"
         return 0
     else
@@ -194,14 +194,14 @@ setup_ssl_certificate() {
 # Requires acme.sh and port 80 open for HTTP-01 challenge
 setup_ip_certificate() {
     local ipv4="$1"
-    local ipv6="$2"  # optional
+    local ipv6="$2" # optional
 
     echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
     echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
     echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
 
     # Check for acme.sh
-    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
+    if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
         install_acme
         if [ $? -ne 0 ]; then
             echo -e "${red}Failed to install acme.sh${plain}"
@@ -273,8 +273,8 @@ setup_ip_certificate() {
 
     # Issue certificate with shortlived profile
     echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
-    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
-    
+    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
+
     ~/.acme.sh/acme.sh --issue \
         ${domain_args} \
         --standalone \
@@ -288,9 +288,9 @@ setup_ip_certificate() {
         echo -e "${red}Failed to issue IP certificate${plain}"
         echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
         # Cleanup acme.sh data for both IPv4 and IPv6 if specified
-        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
-        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
-        rm -rf ${certDir} 2>/dev/null
+        rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
+        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
+        rm -rf ${certDir} 2> /dev/null
         return 1
     fi
 
@@ -308,25 +308,25 @@ setup_ip_certificate() {
     if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
         echo -e "${red}Certificate files not found after installation${plain}"
         # Cleanup acme.sh data for both IPv4 and IPv6 if specified
-        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
-        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
-        rm -rf ${certDir} 2>/dev/null
+        rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
+        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
+        rm -rf ${certDir} 2> /dev/null
         return 1
     fi
-    
+
     echo -e "${green}Certificate files installed successfully${plain}"
 
     # Enable auto-upgrade for acme.sh (ensures cron job runs)
-    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
+    ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
 
     # Secure permissions: private key readable only by owner
-    chmod 600 ${certDir}/privkey.pem 2>/dev/null
-    chmod 644 ${certDir}/fullchain.pem 2>/dev/null
+    chmod 600 ${certDir}/privkey.pem 2> /dev/null
+    chmod 644 ${certDir}/fullchain.pem 2> /dev/null
 
     # Configure panel to use the certificate
     echo -e "${green}Setting certificate paths for the panel...${plain}"
     ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
-    
+
     if [ $? -ne 0 ]; then
         echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}"
         echo -e "${yellow}Certificate files are at:${plain}"
@@ -346,9 +346,9 @@ setup_ip_certificate() {
 ssl_cert_issue() {
     local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
     local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
-    
+
     # check for acme.sh first
-    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
+    if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
         echo "acme.sh could not be found. Installing now..."
         cd ~ || return 1
         curl -s https://get.acme.sh | sh
@@ -364,18 +364,18 @@ ssl_cert_issue() {
     local domain=""
     while true; do
         read -rp "Please enter your domain name: " domain
-        domain="${domain// /}"  # Trim whitespace
-        
+        domain="${domain// /}" # Trim whitespace
+
         if [[ -z "$domain" ]]; then
             echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
             continue
         fi
-        
+
         if ! is_domain "$domain"; then
             echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
             continue
         fi
-        
+
         break
     done
     echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
@@ -383,9 +383,9 @@ ssl_cert_issue() {
 
     # detect existing certificate and reuse it if present
     local cert_exists=0
-    if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
+    if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
         cert_exists=1
-        local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}")
+        local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
         echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}"
         [[ -n "${certInfo}" ]] && echo "$certInfo"
     else
@@ -412,7 +412,7 @@ ssl_cert_issue() {
 
     # Stop panel temporarily
     echo -e "${yellow}Stopping panel temporarily...${plain}"
-    systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
+    systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
 
     if [[ ${cert_exists} -eq 0 ]]; then
         # issue the certificate
@@ -421,7 +421,7 @@ ssl_cert_issue() {
         if [ $? -ne 0 ]; then
             echo -e "${red}Issuing certificate failed, please check logs.${plain}"
             rm -rf ~/.acme.sh/${domain}
-            systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
+            systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
             return 1
         else
             echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
@@ -441,18 +441,18 @@ ssl_cert_issue() {
         echo -e "${green}\t0.${plain} Keep default reloadcmd"
         read -rp "Choose an option: " choice
         case "$choice" in
-        1)
-            echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
-            reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
-            ;;
-        2)
-            echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
-            read -rp "Please enter your custom reloadcmd: " reloadCmd
-            echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
-            ;;
-        *)
-            echo -e "${green}Keeping default reloadcmd${plain}"
-            ;;
+            1)
+                echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
+                reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
+                ;;
+            2)
+                echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
+                read -rp "Please enter your custom reloadcmd: " reloadCmd
+                echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
+                ;;
+            *)
+                echo -e "${green}Keeping default reloadcmd${plain}"
+                ;;
         esac
     fi
 
@@ -469,14 +469,14 @@ ssl_cert_issue() {
         installWroteFiles=1
     fi
 
-    if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then
+    if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
         echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
     else
         echo -e "${red}Installing certificate failed, exiting.${plain}"
         if [[ ${cert_exists} -eq 0 ]]; then
             rm -rf ~/.acme.sh/${domain}
         fi
-        systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
+        systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
         return 1
     fi
 
@@ -486,18 +486,18 @@ ssl_cert_issue() {
         echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
         ls -lah /root/cert/${domain}/
         # Secure permissions: private key readable only by owner
-        chmod 600 $certPath/privkey.pem 2>/dev/null
-        chmod 644 $certPath/fullchain.pem 2>/dev/null
+        chmod 600 $certPath/privkey.pem 2> /dev/null
+        chmod 644 $certPath/fullchain.pem 2> /dev/null
     else
         echo -e "${green}Auto renew succeeded, certificate details:${plain}"
         ls -lah /root/cert/${domain}/
         # Secure permissions: private key readable only by owner
-        chmod 600 $certPath/privkey.pem 2>/dev/null
-        chmod 644 $certPath/fullchain.pem 2>/dev/null
+        chmod 600 $certPath/privkey.pem 2> /dev/null
+        chmod 644 $certPath/fullchain.pem 2> /dev/null
     fi
 
     # start panel
-    systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
+    systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
 
     # Prompt user to set panel paths after successful certificate installation
     read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
@@ -513,14 +513,14 @@ ssl_cert_issue() {
             echo ""
             echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
             echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
-            systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
+            systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
         else
             echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
         fi
     else
         echo -e "${yellow}Skipping panel path setting.${plain}"
     fi
-    
+
     return 0
 }
 
@@ -528,7 +528,7 @@ ssl_cert_issue() {
 # Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage
 prompt_and_setup_ssl() {
     local panel_port="$1"
-    local web_base_path="$2"   # expected without leading slash
+    local web_base_path="$2" # expected without leading slash
     local server_ip="$3"
 
     local ssl_choice=""
@@ -539,124 +539,124 @@ prompt_and_setup_ssl() {
     echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
     echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
     read -rp "Choose an option (default 2 for IP): " ssl_choice
-    ssl_choice="${ssl_choice// /}"  # Trim whitespace
-    
+    ssl_choice="${ssl_choice// /}" # Trim whitespace
+
     # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
     if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
         ssl_choice="2"
     fi
 
     case "$ssl_choice" in
-    1)
-        # User chose Let's Encrypt domain option
-        echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
-        if ssl_cert_issue; then
-            local cert_domain="${SSL_ISSUED_DOMAIN}"
-            if [[ -z "${cert_domain}" ]]; then
-                cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
+        1)
+            # User chose Let's Encrypt domain option
+            echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
+            if ssl_cert_issue; then
+                local cert_domain="${SSL_ISSUED_DOMAIN}"
+                if [[ -z "${cert_domain}" ]]; then
+                    cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
+                fi
+
+                if [[ -n "${cert_domain}" ]]; then
+                    SSL_HOST="${cert_domain}"
+                    echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
+                else
+                    echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
+                    SSL_HOST="${server_ip}"
+                fi
+            else
+                echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
+                SSL_HOST="${server_ip}"
             fi
+            ;;
+        2)
+            # User chose Let's Encrypt IP certificate option
+            echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
+
+            # Ask for optional IPv6
+            local ipv6_addr=""
+            read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
+            ipv6_addr="${ipv6_addr// /}" # Trim whitespace
 
-            if [[ -n "${cert_domain}" ]]; then
-                SSL_HOST="${cert_domain}"
-                echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
+            # Stop panel if running (port 80 needed)
+            if [[ $release == "alpine" ]]; then
+                rc-service x-ui stop > /dev/null 2>&1
             else
-                echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
-                SSL_HOST="${server_ip}"
+                systemctl stop x-ui > /dev/null 2>&1
             fi
-        else
-            echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
-            SSL_HOST="${server_ip}"
-        fi
-        ;;
-    2)
-        # User chose Let's Encrypt IP certificate option
-        echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
-        
-        # Ask for optional IPv6
-        local ipv6_addr=""
-        read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
-        ipv6_addr="${ipv6_addr// /}"  # Trim whitespace
-        
-        # Stop panel if running (port 80 needed)
-        if [[ $release == "alpine" ]]; then
-            rc-service x-ui stop >/dev/null 2>&1
-        else
-            systemctl stop x-ui >/dev/null 2>&1
-        fi
-        
-        setup_ip_certificate "${server_ip}" "${ipv6_addr}"
-        if [ $? -eq 0 ]; then
-            SSL_HOST="${server_ip}"
-            echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
-        else
-            echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
-            SSL_HOST="${server_ip}"
-        fi
-        ;;
-    3)
-        # User chose Custom Paths (User Provided) option
-        echo -e "${green}Using custom existing certificate...${plain}"
-        local custom_cert=""
-        local custom_key=""
-        local custom_domain=""
-
-        # 3.1 Request Domain to compose Panel URL later
-        read -rp "Please enter domain name certificate issued for: " custom_domain
-        custom_domain="${custom_domain// /}" # Remove spaces
-
-        # 3.2 Loop for Certificate Path
-        while true; do
-            read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
-            # Strip quotes if present
-            custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
-
-            if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
-                break
-            elif [[ ! -f "$custom_cert" ]]; then
-                echo -e "${red}Error: File does not exist! Try again.${plain}"
-            elif [[ ! -r "$custom_cert" ]]; then
-                echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+
+            setup_ip_certificate "${server_ip}" "${ipv6_addr}"
+            if [ $? -eq 0 ]; then
+                SSL_HOST="${server_ip}"
+                echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
             else
-                echo -e "${red}Error: File is empty!${plain}"
+                echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
+                SSL_HOST="${server_ip}"
             fi
-        done
-
-        # 3.3 Loop for Private Key Path
-        while true; do
-            read -rp "Input private key path (keywords: .key / privatekey): " custom_key
-            # Strip quotes if present
-            custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
-
-            if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
-                break
-            elif [[ ! -f "$custom_key" ]]; then
-                echo -e "${red}Error: File does not exist! Try again.${plain}"
-            elif [[ ! -r "$custom_key" ]]; then
-                echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+            ;;
+        3)
+            # User chose Custom Paths (User Provided) option
+            echo -e "${green}Using custom existing certificate...${plain}"
+            local custom_cert=""
+            local custom_key=""
+            local custom_domain=""
+
+            # 3.1 Request Domain to compose Panel URL later
+            read -rp "Please enter domain name certificate issued for: " custom_domain
+            custom_domain="${custom_domain// /}" # Remove spaces
+
+            # 3.2 Loop for Certificate Path
+            while true; do
+                read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
+                # Strip quotes if present
+                custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
+
+                if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
+                    break
+                elif [[ ! -f "$custom_cert" ]]; then
+                    echo -e "${red}Error: File does not exist! Try again.${plain}"
+                elif [[ ! -r "$custom_cert" ]]; then
+                    echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+                else
+                    echo -e "${red}Error: File is empty!${plain}"
+                fi
+            done
+
+            # 3.3 Loop for Private Key Path
+            while true; do
+                read -rp "Input private key path (keywords: .key / privatekey): " custom_key
+                # Strip quotes if present
+                custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
+
+                if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
+                    break
+                elif [[ ! -f "$custom_key" ]]; then
+                    echo -e "${red}Error: File does not exist! Try again.${plain}"
+                elif [[ ! -r "$custom_key" ]]; then
+                    echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+                else
+                    echo -e "${red}Error: File is empty!${plain}"
+                fi
+            done
+
+            # 3.4 Apply Settings via x-ui binary
+            ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
+
+            # Set SSL_HOST for composing Panel URL
+            if [[ -n "$custom_domain" ]]; then
+                SSL_HOST="$custom_domain"
             else
-                echo -e "${red}Error: File is empty!${plain}"
+                SSL_HOST="${server_ip}"
             fi
-        done
-
-        # 3.4 Apply Settings via x-ui binary
-        ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
-        
-        # Set SSL_HOST for composing Panel URL
-        if [[ -n "$custom_domain" ]]; then
-            SSL_HOST="$custom_domain"
-        else
-            SSL_HOST="${server_ip}"
-        fi
 
-        echo -e "${green}✓ Custom certificate paths applied.${plain}"
-        echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
+            echo -e "${green}✓ Custom certificate paths applied.${plain}"
+            echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
 
-        systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
-        ;;
-    *)
-        echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
-        SSL_HOST="${server_ip}"
-        ;;
+            systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
+            ;;
+        *)
+            echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
+            SSL_HOST="${server_ip}"
+            ;;
     esac
 }
 
@@ -676,7 +676,7 @@ config_after_install() {
     )
     local server_ip=""
     for ip_address in "${URL_lists[@]}"; do
-        local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
+        local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
         local http_code=$(echo "$response" | tail -n1)
         local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
         if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
@@ -684,13 +684,13 @@ config_after_install() {
             break
         fi
     done
-    
+
     if [[ ${#existing_webBasePath} -lt 4 ]]; then
         if [[ "$existing_hasDefaultCredential" == "true" ]]; then
             local config_webBasePath=$(gen_random_string 18)
             local config_username=$(gen_random_string 10)
             local config_password=$(gen_random_string 10)
-            
+
             read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
             if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
                 read -rp "Please set up the panel port: " config_port
@@ -699,9 +699,9 @@ config_after_install() {
                 local config_port=$(shuf -i 1024-62000 -n 1)
                 echo -e "${yellow}Generated random port: ${config_port}${plain}"
             fi
-            
+
             ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
-            
+
             echo ""
             echo -e "${green}═══════════════════════════════════════════${plain}"
             echo -e "${green}     SSL Certificate Setup (MANDATORY)     ${plain}"
@@ -711,7 +711,7 @@ config_after_install() {
             echo ""
 
             prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
-            
+
             # Display final credentials and access information
             echo ""
             echo -e "${green}═══════════════════════════════════════════${plain}"
@@ -750,7 +750,7 @@ config_after_install() {
         if [[ "$existing_hasDefaultCredential" == "true" ]]; then
             local config_username=$(gen_random_string 10)
             local config_password=$(gen_random_string 10)
-            
+
             echo -e "${yellow}Default credentials detected. Security update required...${plain}"
             ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}"
             echo -e "Generated new random login credentials:"
@@ -778,13 +778,13 @@ config_after_install() {
             echo -e "${green}SSL certificate already configured. No action needed.${plain}"
         fi
     fi
-    
+
     ${xui_folder}/x-ui migrate
 }
 
 install_x-ui() {
     cd ${xui_folder%/x-ui}/
-    
+
     # Download resources
     if [ $# == 0 ]; then
         tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
@@ -806,12 +806,12 @@ install_x-ui() {
         tag_version=$1
         tag_version_numeric=${tag_version#v}
         min_version="2.3.5"
-        
+
         if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
             echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
             exit 1
         fi
-        
+
         url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
         echo -e "Beginning to install x-ui $1"
         curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url}
@@ -825,7 +825,7 @@ install_x-ui() {
         echo -e "${red}Failed to download x-ui.sh${plain}"
         exit 1
     fi
-    
+
     # Stop x-ui service and remove old resources
     if [[ -e ${xui_folder}/ ]]; then
         if [[ $release == "alpine" ]]; then
@@ -835,22 +835,22 @@ install_x-ui() {
         fi
         rm ${xui_folder}/ -rf
     fi
-    
+
     # Extract resources and set permissions
     tar zxvf x-ui-linux-$(arch).tar.gz
     rm x-ui-linux-$(arch).tar.gz -f
-    
+
     cd x-ui
     chmod +x x-ui
     chmod +x x-ui.sh
-    
+
     # Check the system's architecture and rename the file accordingly
     if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
         mv bin/xray-linux-$(arch) bin/xray-linux-arm
         chmod +x bin/xray-linux-arm
     fi
     chmod +x x-ui bin/xray-linux-$(arch)
-    
+
     # Update x-ui cli and se set permission
     mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
     chmod +x /usr/bin/x-ui
@@ -870,7 +870,7 @@ install_x-ui() {
             echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}"
         fi
     fi
-    
+
     if [[ $release == "alpine" ]]; then
         curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
         if [[ $? -ne 0 ]]; then
@@ -883,73 +883,73 @@ install_x-ui() {
     else
         # Install systemd service file
         service_installed=false
-        
+
         if [ -f "x-ui.service" ]; then
             echo -e "${green}Found x-ui.service in extracted files, installing...${plain}"
-            cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
+            cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
             if [[ $? -eq 0 ]]; then
                 service_installed=true
             fi
         fi
-        
+
         if [ "$service_installed" = false ]; then
             case "${release}" in
                 ubuntu | debian | armbian)
                     if [ -f "x-ui.service.debian" ]; then
                         echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}"
-                        cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
+                        cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
                         if [[ $? -eq 0 ]]; then
                             service_installed=true
                         fi
                     fi
-                ;;
+                    ;;
                 arch | manjaro | parch)
                     if [ -f "x-ui.service.arch" ]; then
                         echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}"
-                        cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
+                        cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
                         if [[ $? -eq 0 ]]; then
                             service_installed=true
                         fi
                     fi
-                ;;
+                    ;;
                 *)
                     if [ -f "x-ui.service.rhel" ]; then
                         echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
-                        cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
+                        cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
                         if [[ $? -eq 0 ]]; then
                             service_installed=true
                         fi
                     fi
-                ;;
+                    ;;
             esac
         fi
-        
+
         # If service file not found in tar.gz, download from GitHub
         if [ "$service_installed" = false ]; then
             echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
             case "${release}" in
                 ubuntu | debian | armbian)
-                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
-                ;;
+                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian > /dev/null 2>&1
+                    ;;
                 arch | manjaro | parch)
-                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
-                ;;
+                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch > /dev/null 2>&1
+                    ;;
                 *)
-                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
-                ;;
+                    curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel > /dev/null 2>&1
+                    ;;
             esac
-            
+
             if [[ $? -ne 0 ]]; then
                 echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
                 exit 1
             fi
             service_installed=true
         fi
-        
+
         if [ "$service_installed" = true ]; then
             echo -e "${green}Setting up systemd unit...${plain}"
-            chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
-            chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1
+            chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
+            chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1
             systemctl daemon-reload
             systemctl enable x-ui
             systemctl start x-ui
@@ -958,7 +958,7 @@ install_x-ui() {
             exit 1
         fi
     fi
-    
+
     echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
     echo -e ""
     echo -e "┌───────────────────────────────────────────────────────┐

+ 283 - 283
update.sh

@@ -12,16 +12,16 @@ xui_service="${XUI_SERVICE:=/etc/systemd/system}"
 # Don't edit this config
 b_source="${BASH_SOURCE[0]}"
 while [ -h "$b_source" ]; do
-    b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
+    b_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
     b_source="$(readlink "$b_source")"
     [[ $b_source != /* ]] && b_source="$b_dir/$b_source"
 done
-cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
+cur_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)"
 script_name=$(basename "$0")
 
 # Check command exist function
 _command_exists() {
-    type "$1" &>/dev/null
+    type "$1" &> /dev/null
 }
 
 # Fail, log and exit script function
@@ -44,7 +44,7 @@ fi
 if [[ -f /etc/os-release ]]; then
     source /etc/os-release
     release=$ID
-    elif [[ -f /usr/lib/os-release ]]; then
+elif [[ -f /usr/lib/os-release ]]; then
     source /usr/lib/os-release
     release=$ID
 else
@@ -61,7 +61,7 @@ arch() {
         armv6* | armv6) echo 'armv6' ;;
         armv5* | armv5) echo 'armv5' ;;
         s390x) echo 's390x' ;;
-        *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
+        *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" > /dev/null 2>&1 && exit 2 ;;
     esac
 }
 
@@ -84,23 +84,23 @@ is_domain() {
 # Port helpers
 is_port_in_use() {
     local port="$1"
-    if command -v ss >/dev/null 2>&1; then
-        ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
+    if command -v ss > /dev/null 2>&1; then
+        ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
         return
     fi
-    if command -v netstat >/dev/null 2>&1; then
-        netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
+    if command -v netstat > /dev/null 2>&1; then
+        netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
         return
     fi
-    if command -v lsof >/dev/null 2>&1; then
-        lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
+    if command -v lsof > /dev/null 2>&1; then
+        lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0
     fi
     return 1
 }
 
 gen_random_string() {
     local length="$1"
-    openssl rand -base64 $(( length * 2 )) \
+    openssl rand -base64 $((length * 2)) \
         | tr -dc 'a-zA-Z0-9' \
         | head -c "$length"
 }
@@ -109,37 +109,37 @@ install_base() {
     echo -e "${green}Updating and install dependency packages...${plain}"
     case "${release}" in
         ubuntu | debian | armbian)
-            apt-get update >/dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl >/dev/null 2>&1
-        ;;
+            apt-get update > /dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
+            ;;
         fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
-            dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1
-        ;;
+            dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
+            ;;
         centos)
             if [[ "${VERSION_ID}" =~ ^7 ]]; then
-                yum -y update >/dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1
+                yum -y update > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
             else
-                dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1
+                dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
             fi
-        ;;
+            ;;
         arch | manjaro | parch)
-            pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl >/dev/null 2>&1
-        ;;
+            pacman -Syu > /dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl > /dev/null 2>&1
+            ;;
         opensuse-tumbleweed | opensuse-leap)
-            zypper refresh >/dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl >/dev/null 2>&1
-        ;;
+            zypper refresh > /dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl > /dev/null 2>&1
+            ;;
         alpine)
-            apk update >/dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl>/dev/null 2>&1
-        ;;
+            apk update > /dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl > /dev/null 2>&1
+            ;;
         *)
-            apt-get update >/dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl >/dev/null 2>&1
-        ;;
+            apt-get update > /dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
+            ;;
     esac
 }
 
 install_acme() {
     echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
     cd ~ || return 1
-    curl -s https://get.acme.sh | sh >/dev/null 2>&1
+    curl -s https://get.acme.sh | sh > /dev/null 2>&1
     if [ $? -ne 0 ]; then
         echo -e "${red}Failed to install acme.sh${plain}"
         return 1
@@ -154,59 +154,59 @@ setup_ssl_certificate() {
     local server_ip="$2"
     local existing_port="$3"
     local existing_webBasePath="$4"
-    
+
     echo -e "${green}Setting up SSL certificate...${plain}"
-    
+
     # Check if acme.sh is installed
-    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
+    if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
         install_acme
         if [ $? -ne 0 ]; then
             echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
             return 1
         fi
     fi
-    
+
     # Create certificate directory
     local certPath="/root/cert/${domain}"
     mkdir -p "$certPath"
-    
+
     # Issue certificate
     echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
     echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
-    
-    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
+
+    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
     ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
-    
+
     if [ $? -ne 0 ]; then
         echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
         echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
-        rm -rf ~/.acme.sh/${domain} 2>/dev/null
-        rm -rf "$certPath" 2>/dev/null
+        rm -rf ~/.acme.sh/${domain} 2> /dev/null
+        rm -rf "$certPath" 2> /dev/null
         return 1
     fi
-    
+
     # Install certificate
     ~/.acme.sh/acme.sh --installcert -d ${domain} \
         --key-file /root/cert/${domain}/privkey.pem \
         --fullchain-file /root/cert/${domain}/fullchain.pem \
-        --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
-    
+        --reloadcmd "systemctl restart x-ui" > /dev/null 2>&1
+
     if [ $? -ne 0 ]; then
         echo -e "${yellow}Failed to install certificate${plain}"
         return 1
     fi
-    
+
     # Enable auto-renew
-    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
-    chmod 600 $certPath/privkey.pem 2>/dev/null
-    chmod 644 $certPath/fullchain.pem 2>/dev/null
-    
+    ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
+    chmod 600 $certPath/privkey.pem 2> /dev/null
+    chmod 644 $certPath/fullchain.pem 2> /dev/null
+
     # Set certificate for panel
     local webCertFile="/root/cert/${domain}/fullchain.pem"
     local webKeyFile="/root/cert/${domain}/privkey.pem"
-    
+
     if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
-        ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
+        ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" > /dev/null 2>&1
         echo -e "${green}SSL certificate installed and configured successfully!${plain}"
         return 0
     else
@@ -219,14 +219,14 @@ setup_ssl_certificate() {
 # Requires acme.sh and port 80 open for HTTP-01 challenge
 setup_ip_certificate() {
     local ipv4="$1"
-    local ipv6="$2"  # optional
+    local ipv6="$2" # optional
 
     echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
     echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
     echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
 
     # Check for acme.sh
-    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
+    if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
         install_acme
         if [ $? -ne 0 ]; then
             echo -e "${red}Failed to install acme.sh${plain}"
@@ -298,8 +298,8 @@ setup_ip_certificate() {
 
     # Issue certificate with shortlived profile
     echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
-    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
-    
+    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
+
     ~/.acme.sh/acme.sh --issue \
         ${domain_args} \
         --standalone \
@@ -313,9 +313,9 @@ setup_ip_certificate() {
         echo -e "${red}Failed to issue IP certificate${plain}"
         echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
         # Cleanup acme.sh data for both IPv4 and IPv6 if specified
-        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
-        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
-        rm -rf ${certDir} 2>/dev/null
+        rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
+        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
+        rm -rf ${certDir} 2> /dev/null
         return 1
     fi
 
@@ -333,19 +333,19 @@ setup_ip_certificate() {
     if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
         echo -e "${red}Certificate files not found after installation${plain}"
         # Cleanup acme.sh data for both IPv4 and IPv6 if specified
-        rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
-        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
-        rm -rf ${certDir} 2>/dev/null
+        rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
+        [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
+        rm -rf ${certDir} 2> /dev/null
         return 1
     fi
-    
+
     echo -e "${green}Certificate files installed successfully${plain}"
 
     # Enable auto-upgrade for acme.sh (ensures cron job runs)
-    ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
+    ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
 
-    chmod 600 ${certDir}/privkey.pem 2>/dev/null
-    chmod 644 ${certDir}/fullchain.pem 2>/dev/null
+    chmod 600 ${certDir}/privkey.pem 2> /dev/null
+    chmod 644 ${certDir}/fullchain.pem 2> /dev/null
 
     # Configure panel to use the certificate
     echo -e "${green}Setting certificate paths for the panel...${plain}"
@@ -369,9 +369,9 @@ setup_ip_certificate() {
 ssl_cert_issue() {
     local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
     local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
-    
+
     # check for acme.sh first
-    if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
+    if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
         echo "acme.sh could not be found. Installing now..."
         cd ~ || return 1
         curl -s https://get.acme.sh | sh
@@ -387,18 +387,18 @@ ssl_cert_issue() {
     local domain=""
     while true; do
         read -rp "Please enter your domain name: " domain
-        domain="${domain// /}"  # Trim whitespace
-        
+        domain="${domain// /}" # Trim whitespace
+
         if [[ -z "$domain" ]]; then
             echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
             continue
         fi
-        
+
         if ! is_domain "$domain"; then
             echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
             continue
         fi
-        
+
         break
     done
     echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
@@ -406,9 +406,9 @@ ssl_cert_issue() {
 
     # detect existing certificate and reuse it if present
     local cert_exists=0
-    if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
+    if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
         cert_exists=1
-        local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}")
+        local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
         echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}"
         [[ -n "${certInfo}" ]] && echo "$certInfo"
     else
@@ -435,7 +435,7 @@ ssl_cert_issue() {
 
     # Stop panel temporarily
     echo -e "${yellow}Stopping panel temporarily...${plain}"
-    systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
+    systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
 
     if [[ ${cert_exists} -eq 0 ]]; then
         # issue the certificate
@@ -444,7 +444,7 @@ ssl_cert_issue() {
         if [ $? -ne 0 ]; then
             echo -e "${red}Issuing certificate failed, please check logs.${plain}"
             rm -rf ~/.acme.sh/${domain}
-            systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
+            systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
             return 1
         else
             echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
@@ -464,18 +464,18 @@ ssl_cert_issue() {
         echo -e "${green}\t0.${plain} Keep default reloadcmd"
         read -rp "Choose an option: " choice
         case "$choice" in
-        1)
-            echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
-            reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
-            ;;
-        2)
-            echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
-            read -rp "Please enter your custom reloadcmd: " reloadCmd
-            echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
-            ;;
-        *)
-            echo -e "${green}Keeping default reloadcmd${plain}"
-            ;;
+            1)
+                echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
+                reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
+                ;;
+            2)
+                echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
+                read -rp "Please enter your custom reloadcmd: " reloadCmd
+                echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
+                ;;
+            *)
+                echo -e "${green}Keeping default reloadcmd${plain}"
+                ;;
         esac
     fi
 
@@ -492,14 +492,14 @@ ssl_cert_issue() {
         installWroteFiles=1
     fi
 
-    if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then
+    if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
         echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
     else
         echo -e "${red}Installing certificate failed, exiting.${plain}"
         if [[ ${cert_exists} -eq 0 ]]; then
             rm -rf ~/.acme.sh/${domain}
         fi
-        systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
+        systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
         return 1
     fi
 
@@ -518,7 +518,7 @@ ssl_cert_issue() {
     fi
 
     # Restart panel
-    systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
+    systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
 
     # Prompt user to set panel paths after successful certificate installation
     read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
@@ -534,21 +534,21 @@ ssl_cert_issue() {
             echo ""
             echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
             echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
-            systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
+            systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
         else
             echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
         fi
     else
         echo -e "${yellow}Skipping panel path setting.${plain}"
     fi
-    
+
     return 0
 }
 # Unified interactive SSL setup (domain or IP)
 # Sets global `SSL_HOST` to the chosen domain/IP
 prompt_and_setup_ssl() {
     local panel_port="$1"
-    local web_base_path="$2"   # expected without leading slash
+    local web_base_path="$2" # expected without leading slash
     local server_ip="$3"
 
     local ssl_choice=""
@@ -559,132 +559,132 @@ prompt_and_setup_ssl() {
     echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
     echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
     read -rp "Choose an option (default 2 for IP): " ssl_choice
-    ssl_choice="${ssl_choice// /}"  # Trim whitespace
-    
+    ssl_choice="${ssl_choice// /}" # Trim whitespace
+
     # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
     if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
         ssl_choice="2"
     fi
 
     case "$ssl_choice" in
-    1)
-        # User chose Let's Encrypt domain option
-        echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
-        if ssl_cert_issue; then
-            local cert_domain="${SSL_ISSUED_DOMAIN}"
-            if [[ -z "${cert_domain}" ]]; then
-                cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
-            fi
+        1)
+            # User chose Let's Encrypt domain option
+            echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
+            if ssl_cert_issue; then
+                local cert_domain="${SSL_ISSUED_DOMAIN}"
+                if [[ -z "${cert_domain}" ]]; then
+                    cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
+                fi
 
-            if [[ -n "${cert_domain}" ]]; then
-                SSL_HOST="${cert_domain}"
-                echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
+                if [[ -n "${cert_domain}" ]]; then
+                    SSL_HOST="${cert_domain}"
+                    echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
+                else
+                    echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
+                    SSL_HOST="${server_ip}"
+                fi
             else
-                echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
+                echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
                 SSL_HOST="${server_ip}"
             fi
-        else
-            echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
-            SSL_HOST="${server_ip}"
-        fi
-        ;;
-    2)
-        # User chose Let's Encrypt IP certificate option
-        echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
-        
-        # Ask for optional IPv6
-        local ipv6_addr=""
-        read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
-        ipv6_addr="${ipv6_addr// /}"  # Trim whitespace
-        
-        # Stop panel if running (port 80 needed)
-        if [[ $release == "alpine" ]]; then
-            rc-service x-ui stop >/dev/null 2>&1
-        else
-            systemctl stop x-ui >/dev/null 2>&1
-        fi
-        
-        setup_ip_certificate "${server_ip}" "${ipv6_addr}"
-        if [ $? -eq 0 ]; then
-            SSL_HOST="${server_ip}"
-            echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
-        else
-            echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
-            SSL_HOST="${server_ip}"
-        fi
-        
-        # Restart panel after SSL is configured (restart applies new cert settings)
-        if [[ $release == "alpine" ]]; then
-            rc-service x-ui restart >/dev/null 2>&1
-        else
-            systemctl restart x-ui >/dev/null 2>&1
-        fi
+            ;;
+        2)
+            # User chose Let's Encrypt IP certificate option
+            echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
 
-        ;;
-    3)
-        # User chose Custom Paths (User Provided) option
-        echo -e "${green}Using custom existing certificate...${plain}"
-        local custom_cert=""
-        local custom_key=""
-        local custom_domain=""
-
-        # 3.1 Request Domain to compose Panel URL later
-        read -rp "Please enter domain name certificate issued for: " custom_domain
-        custom_domain="${custom_domain// /}" # Remove spaces
-
-        # 3.2 Loop for Certificate Path
-        while true; do
-            read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
-            # Strip quotes if present
-            custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
-
-            if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
-                break
-            elif [[ ! -f "$custom_cert" ]]; then
-                echo -e "${red}Error: File does not exist! Try again.${plain}"
-            elif [[ ! -r "$custom_cert" ]]; then
-                echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+            # Ask for optional IPv6
+            local ipv6_addr=""
+            read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
+            ipv6_addr="${ipv6_addr// /}" # Trim whitespace
+
+            # Stop panel if running (port 80 needed)
+            if [[ $release == "alpine" ]]; then
+                rc-service x-ui stop > /dev/null 2>&1
             else
-                echo -e "${red}Error: File is empty!${plain}"
+                systemctl stop x-ui > /dev/null 2>&1
             fi
-        done
-
-        # 3.3 Loop for Private Key Path
-        while true; do
-            read -rp "Input private key path (keywords: .key / privatekey): " custom_key
-            # Strip quotes if present
-            custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
-
-            if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
-                break
-            elif [[ ! -f "$custom_key" ]]; then
-                echo -e "${red}Error: File does not exist! Try again.${plain}"
-            elif [[ ! -r "$custom_key" ]]; then
-                echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+
+            setup_ip_certificate "${server_ip}" "${ipv6_addr}"
+            if [ $? -eq 0 ]; then
+                SSL_HOST="${server_ip}"
+                echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
             else
-                echo -e "${red}Error: File is empty!${plain}"
+                echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
+                SSL_HOST="${server_ip}"
             fi
-        done
 
-        # 3.4 Apply Settings via x-ui binary
-        ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
+            # Restart panel after SSL is configured (restart applies new cert settings)
+            if [[ $release == "alpine" ]]; then
+                rc-service x-ui restart > /dev/null 2>&1
+            else
+                systemctl restart x-ui > /dev/null 2>&1
+            fi
 
-        # Set SSL_HOST for composing Panel URL
-        if [[ -n "$custom_domain" ]]; then
-            SSL_HOST="$custom_domain"
-        else
-            SSL_HOST="${server_ip}"
-        fi
+            ;;
+        3)
+            # User chose Custom Paths (User Provided) option
+            echo -e "${green}Using custom existing certificate...${plain}"
+            local custom_cert=""
+            local custom_key=""
+            local custom_domain=""
+
+            # 3.1 Request Domain to compose Panel URL later
+            read -rp "Please enter domain name certificate issued for: " custom_domain
+            custom_domain="${custom_domain// /}" # Remove spaces
+
+            # 3.2 Loop for Certificate Path
+            while true; do
+                read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
+                # Strip quotes if present
+                custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
+
+                if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
+                    break
+                elif [[ ! -f "$custom_cert" ]]; then
+                    echo -e "${red}Error: File does not exist! Try again.${plain}"
+                elif [[ ! -r "$custom_cert" ]]; then
+                    echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+                else
+                    echo -e "${red}Error: File is empty!${plain}"
+                fi
+            done
+
+            # 3.3 Loop for Private Key Path
+            while true; do
+                read -rp "Input private key path (keywords: .key / privatekey): " custom_key
+                # Strip quotes if present
+                custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
+
+                if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
+                    break
+                elif [[ ! -f "$custom_key" ]]; then
+                    echo -e "${red}Error: File does not exist! Try again.${plain}"
+                elif [[ ! -r "$custom_key" ]]; then
+                    echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
+                else
+                    echo -e "${red}Error: File is empty!${plain}"
+                fi
+            done
 
-        echo -e "${green}✓ Custom certificate paths applied.${plain}"
-        echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
+            # 3.4 Apply Settings via x-ui binary
+            ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
 
-        systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
-        ;;
-    *)
-        echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
-        SSL_HOST="${server_ip}"
-        ;;
+            # Set SSL_HOST for composing Panel URL
+            if [[ -n "$custom_domain" ]]; then
+                SSL_HOST="$custom_domain"
+            else
+                SSL_HOST="${server_ip}"
+            fi
+
+            echo -e "${green}✓ Custom certificate paths applied.${plain}"
+            echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
+
+            systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
+            ;;
+        *)
+            echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
+            SSL_HOST="${server_ip}"
+            ;;
     esac
 }
 
@@ -692,12 +692,12 @@ config_after_update() {
     echo -e "${yellow}x-ui settings:${plain}"
     ${xui_folder}/x-ui setting -show true
     ${xui_folder}/x-ui migrate
-    
+
     # Properly detect empty cert by checking if cert: line exists and has content after it
-    local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
+    local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2> /dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
     local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
     local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
-    
+
     # Get server IP
     local URL_lists=(
         "https://api4.ipify.org"
@@ -709,7 +709,7 @@ config_after_update() {
     )
     local server_ip=""
     for ip_address in "${URL_lists[@]}"; do
-        local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
+        local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
         local http_code=$(echo "$response" | tail -n1)
         local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
         if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
@@ -717,7 +717,7 @@ config_after_update() {
             break
         fi
     done
-    
+
     # Handle missing/short webBasePath
     if [[ ${#existing_webBasePath} -lt 4 ]]; then
         echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
@@ -726,7 +726,7 @@ config_after_update() {
         existing_webBasePath="${config_webBasePath}"
         echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
     fi
-    
+
     # Check and prompt for SSL if missing
     if [[ -z "$existing_cert" ]]; then
         echo ""
@@ -736,16 +736,16 @@ config_after_update() {
         echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
         echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
         echo ""
-        
+
         if [[ -z "${server_ip}" ]]; then
             echo -e "${red}Failed to detect server IP${plain}"
             echo -e "${yellow}Please configure SSL manually using: x-ui${plain}"
             return
         fi
-        
+
         # Prompt and setup SSL (domain or IP)
         prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
-        
+
         echo ""
         echo -e "${green}═══════════════════════════════════════════${plain}"
         echo -e "${green}     Panel Access Information              ${plain}"
@@ -768,17 +768,17 @@ config_after_update() {
 
 update_x-ui() {
     cd ${xui_folder%/x-ui}/
-    
+
     if [ -f "${xui_folder}/x-ui" ]; then
         current_xui_version=$(${xui_folder}/x-ui -v)
         echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
     else
         _fail "ERROR: Current x-ui version: unknown"
     fi
-    
+
     echo -e "${green}Downloading new x-ui version...${plain}"
-    
-    tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
+
+    tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2> /dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
     if [[ ! -n "$tag_version" ]]; then
         echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
         tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
@@ -787,110 +787,110 @@ update_x-ui() {
         fi
     fi
     echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
-    ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
+    ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
     if [[ $? -ne 0 ]]; then
         echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
-        ${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
+        ${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
         if [[ $? -ne 0 ]]; then
             _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
         fi
     fi
-    
+
     if [[ -e ${xui_folder}/ ]]; then
         echo -e "${green}Stopping x-ui...${plain}"
         if [[ $release == "alpine" ]]; then
             if [ -f "/etc/init.d/x-ui" ]; then
-                rc-service x-ui stop >/dev/null 2>&1
-                rc-update del x-ui >/dev/null 2>&1
+                rc-service x-ui stop > /dev/null 2>&1
+                rc-update del x-ui > /dev/null 2>&1
                 echo -e "${green}Removing old service unit version...${plain}"
-                rm -f /etc/init.d/x-ui >/dev/null 2>&1
+                rm -f /etc/init.d/x-ui > /dev/null 2>&1
             else
-                rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
+                rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
                 _fail "ERROR: x-ui service unit not installed."
             fi
         else
             if [ -f "${xui_service}/x-ui.service" ]; then
-                systemctl stop x-ui >/dev/null 2>&1
-                systemctl disable x-ui >/dev/null 2>&1
+                systemctl stop x-ui > /dev/null 2>&1
+                systemctl disable x-ui > /dev/null 2>&1
                 echo -e "${green}Removing old systemd unit version...${plain}"
-                rm ${xui_service}/x-ui.service -f >/dev/null 2>&1
-                systemctl daemon-reload >/dev/null 2>&1
+                rm ${xui_service}/x-ui.service -f > /dev/null 2>&1
+                systemctl daemon-reload > /dev/null 2>&1
             else
-                rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
+                rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
                 _fail "ERROR: x-ui systemd unit not installed."
             fi
         fi
         echo -e "${green}Removing old x-ui version...${plain}"
-        rm ${xui_folder} -f >/dev/null 2>&1
-        rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
-        rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
-        rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1
-        rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
-        rm ${xui_folder}/x-ui -f >/dev/null 2>&1
-        rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
+        rm ${xui_folder} -f > /dev/null 2>&1
+        rm ${xui_folder}/x-ui.service -f > /dev/null 2>&1
+        rm ${xui_folder}/x-ui.service.debian -f > /dev/null 2>&1
+        rm ${xui_folder}/x-ui.service.arch -f > /dev/null 2>&1
+        rm ${xui_folder}/x-ui.service.rhel -f > /dev/null 2>&1
+        rm ${xui_folder}/x-ui -f > /dev/null 2>&1
+        rm ${xui_folder}/x-ui.sh -f > /dev/null 2>&1
         echo -e "${green}Removing old xray version...${plain}"
-        rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1
+        rm ${xui_folder}/bin/xray-linux-amd64 -f > /dev/null 2>&1
         echo -e "${green}Removing old README and LICENSE file...${plain}"
-        rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1
-        rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1
+        rm ${xui_folder}/bin/README.md -f > /dev/null 2>&1
+        rm ${xui_folder}/bin/LICENSE -f > /dev/null 2>&1
     else
-        rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
+        rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
         _fail "ERROR: x-ui not installed."
     fi
-    
+
     echo -e "${green}Installing new x-ui version...${plain}"
-    tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
-    rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
-    cd x-ui >/dev/null 2>&1
-    chmod +x x-ui >/dev/null 2>&1
-    
+    tar zxvf x-ui-linux-$(arch).tar.gz > /dev/null 2>&1
+    rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
+    cd x-ui > /dev/null 2>&1
+    chmod +x x-ui > /dev/null 2>&1
+
     # Check the system's architecture and rename the file accordingly
     if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
-        mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
-        chmod +x bin/xray-linux-arm >/dev/null 2>&1
+        mv bin/xray-linux-$(arch) bin/xray-linux-arm > /dev/null 2>&1
+        chmod +x bin/xray-linux-arm > /dev/null 2>&1
     fi
-    
-    chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
-    
+
+    chmod +x x-ui bin/xray-linux-$(arch) > /dev/null 2>&1
+
     echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
-    ${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
+    ${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh > /dev/null 2>&1
     if [[ $? -ne 0 ]]; then
         echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
-        ${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
+        ${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh > /dev/null 2>&1
         if [[ $? -ne 0 ]]; then
             _fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
         fi
     fi
-    
-    chmod +x ${xui_folder}/x-ui.sh >/dev/null 2>&1
-    chmod +x /usr/bin/x-ui >/dev/null 2>&1
-    mkdir -p /var/log/x-ui >/dev/null 2>&1
-    
+
+    chmod +x ${xui_folder}/x-ui.sh > /dev/null 2>&1
+    chmod +x /usr/bin/x-ui > /dev/null 2>&1
+    mkdir -p /var/log/x-ui > /dev/null 2>&1
+
     echo -e "${green}Changing owner...${plain}"
-    chown -R root:root ${xui_folder} >/dev/null 2>&1
-    
+    chown -R root:root ${xui_folder} > /dev/null 2>&1
+
     if [ -f "${xui_folder}/bin/config.json" ]; then
         echo -e "${green}Changing on config file permissions...${plain}"
-        chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1
+        chmod 640 ${xui_folder}/bin/config.json > /dev/null 2>&1
     fi
-    
+
     if [[ $release == "alpine" ]]; then
         echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
-        ${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
+        ${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc > /dev/null 2>&1
         if [[ $? -ne 0 ]]; then
-            ${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
+            ${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc > /dev/null 2>&1
             if [[ $? -ne 0 ]]; then
                 _fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
             fi
         fi
-        chmod +x /etc/init.d/x-ui >/dev/null 2>&1
-        chown root:root /etc/init.d/x-ui >/dev/null 2>&1
-        rc-update add x-ui >/dev/null 2>&1
-        rc-service x-ui start >/dev/null 2>&1
+        chmod +x /etc/init.d/x-ui > /dev/null 2>&1
+        chown root:root /etc/init.d/x-ui > /dev/null 2>&1
+        rc-update add x-ui > /dev/null 2>&1
+        rc-service x-ui start > /dev/null 2>&1
     else
         if [ -f "x-ui.service" ]; then
             echo -e "${green}Installing systemd unit...${plain}"
-            cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
+            cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
             if [[ $? -ne 0 ]]; then
                 echo -e "${red}Failed to copy x-ui.service${plain}"
                 exit 1
@@ -901,62 +901,62 @@ update_x-ui() {
                 ubuntu | debian | armbian)
                     if [ -f "x-ui.service.debian" ]; then
                         echo -e "${green}Installing debian-like systemd unit...${plain}"
-                        cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
+                        cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
                         if [[ $? -eq 0 ]]; then
                             service_installed=true
                         fi
                     fi
-                ;;
+                    ;;
                 arch | manjaro | parch)
                     if [ -f "x-ui.service.arch" ]; then
                         echo -e "${green}Installing arch-like systemd unit...${plain}"
-                        cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
+                        cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
                         if [[ $? -eq 0 ]]; then
                             service_installed=true
                         fi
                     fi
-                ;;
+                    ;;
                 *)
                     if [ -f "x-ui.service.rhel" ]; then
                         echo -e "${green}Installing rhel-like systemd unit...${plain}"
-                        cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
+                        cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
                         if [[ $? -eq 0 ]]; then
                             service_installed=true
                         fi
                     fi
-                ;;
+                    ;;
             esac
-            
+
             # If service file not found in tar.gz, download from GitHub
             if [ "$service_installed" = false ]; then
                 echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
                 case "${release}" in
                     ubuntu | debian | armbian)
-                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
-                    ;;
+                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian > /dev/null 2>&1
+                        ;;
                     arch | manjaro | parch)
-                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
-                    ;;
+                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch > /dev/null 2>&1
+                        ;;
                     *)
-                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
-                    ;;
+                        ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel > /dev/null 2>&1
+                        ;;
                 esac
-                
+
                 if [[ $? -ne 0 ]]; then
                     echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
                     exit 1
                 fi
             fi
         fi
-        chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
-        chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1
-        systemctl daemon-reload >/dev/null 2>&1
-        systemctl enable x-ui >/dev/null 2>&1
-        systemctl start x-ui >/dev/null 2>&1
+        chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
+        chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1
+        systemctl daemon-reload > /dev/null 2>&1
+        systemctl enable x-ui > /dev/null 2>&1
+        systemctl start x-ui > /dev/null 2>&1
     fi
-    
+
     config_after_update
-    
+
     echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
     echo -e ""
     echo -e "┌───────────────────────────────────────────────────────┐

+ 0 - 4
web/assets/js/model/inbound.js

@@ -697,7 +697,6 @@ class TlsStreamSettings extends XrayCommonClass {
         certificates = [new TlsStreamSettings.Cert()],
         alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
         echServerKeys = '',
-        echForceQuery = 'none',
         settings = new TlsStreamSettings.Settings()
     ) {
         super();
@@ -711,7 +710,6 @@ class TlsStreamSettings extends XrayCommonClass {
         this.certs = certificates;
         this.alpn = alpn;
         this.echServerKeys = echServerKeys;
-        this.echForceQuery = echForceQuery;
         this.settings = settings;
     }
 
@@ -744,7 +742,6 @@ class TlsStreamSettings extends XrayCommonClass {
             certs,
             json.alpn,
             json.echServerKeys,
-            json.echForceQuery,
             settings,
         );
     }
@@ -761,7 +758,6 @@ class TlsStreamSettings extends XrayCommonClass {
             certificates: TlsStreamSettings.toJsonArray(this.certs),
             alpn: this.alpn,
             echServerKeys: this.echServerKeys,
-            echForceQuery: this.echForceQuery,
             settings: this.settings,
         };
     }

+ 54 - 4
web/assets/js/model/outbound.js

@@ -1425,14 +1425,16 @@ Outbound.FreedomSettings = class extends CommonClass {
         redirect = '',
         fragment = {},
         noises = [],
-        ipsBlocked = [],
+        finalRules = [],
     ) {
         super();
         this.domainStrategy = domainStrategy;
         this.redirect = redirect;
         this.fragment = fragment || {};
         this.noises = Array.isArray(noises) ? noises : [];
-        this.ipsBlocked = Array.isArray(ipsBlocked) ? ipsBlocked : [];
+        this.finalRules = Array.isArray(finalRules)
+            ? finalRules.map(rule => rule instanceof Outbound.FreedomSettings.FinalRule ? rule : Outbound.FreedomSettings.FinalRule.fromJson(rule))
+            : [];
     }
 
     addNoise() {
@@ -1443,13 +1445,30 @@ Outbound.FreedomSettings = class extends CommonClass {
         this.noises.splice(index, 1);
     }
 
+    addFinalRule(action = 'block') {
+        this.finalRules.push(new Outbound.FreedomSettings.FinalRule(action));
+    }
+
+    delFinalRule(index) {
+        this.finalRules.splice(index, 1);
+    }
+
     static fromJson(json = {}) {
+        const finalRules = Array.isArray(json.finalRules)
+            ? json.finalRules.map(rule => Outbound.FreedomSettings.FinalRule.fromJson(rule))
+            : [];
+
+        // Backward compatibility: map legacy ipsBlocked entries to blocking finalRules.
+        if (finalRules.length === 0 && Array.isArray(json.ipsBlocked) && json.ipsBlocked.length > 0) {
+            finalRules.push(new Outbound.FreedomSettings.FinalRule('block', '', '', json.ipsBlocked, ''));
+        }
+
         return new Outbound.FreedomSettings(
             json.domainStrategy,
             json.redirect,
             json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : {},
             json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : [],
-            json.ipsBlocked || [],
+            finalRules,
         );
     }
 
@@ -1459,7 +1478,7 @@ Outbound.FreedomSettings = class extends CommonClass {
             redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
             fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
             noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
-            ipsBlocked: this.ipsBlocked.length === 0 ? undefined : this.ipsBlocked,
+            finalRules: this.finalRules.length === 0 ? undefined : Outbound.FreedomSettings.FinalRule.toJsonArray(this.finalRules),
         };
     }
 };
@@ -1521,6 +1540,37 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
     }
 };
 
+Outbound.FreedomSettings.FinalRule = class extends CommonClass {
+    constructor(action = 'block', network = '', port = '', ip = [], blockDelay = '') {
+        super();
+        this.action = action;
+        this.network = network;
+        this.port = port;
+        this.ip = Array.isArray(ip) ? ip : [];
+        this.blockDelay = blockDelay;
+    }
+
+    static fromJson(json = {}) {
+        return new Outbound.FreedomSettings.FinalRule(
+            json.action,
+            Array.isArray(json.network) ? json.network.join(',') : json.network,
+            json.port,
+            json.ip || [],
+            json.blockDelay,
+        );
+    }
+
+    toJson() {
+        return {
+            action: ['allow', 'block'].includes(this.action) ? this.action : 'block',
+            network: ObjectUtil.isEmpty(this.network) ? undefined : this.network,
+            port: ObjectUtil.isEmpty(this.port) ? undefined : this.port,
+            ip: this.ip.length === 0 ? undefined : this.ip,
+            blockDelay: this.action === 'block' && !ObjectUtil.isEmpty(this.blockDelay) ? this.blockDelay : undefined,
+        };
+    }
+};
+
 Outbound.BlackholeSettings = class extends CommonClass {
     constructor(type) {
         super();

+ 52 - 10
web/controller/util.go

@@ -1,8 +1,10 @@
 package controller
 
 import (
+	"fmt"
 	"net"
 	"net/http"
+	"net/netip"
 	"strings"
 
 	"github.com/mhsanaei/3x-ui/v2/config"
@@ -14,18 +16,58 @@ import (
 
 // getRemoteIp extracts the real IP address from the request headers or remote address.
 func getRemoteIp(c *gin.Context) string {
-	value := c.GetHeader("X-Real-IP")
-	if value != "" {
-		return value
+	if ip, ok := extractTrustedIP(c.GetHeader("X-Real-IP")); ok {
+		return ip
 	}
-	value = c.GetHeader("X-Forwarded-For")
-	if value != "" {
-		ips := strings.Split(value, ",")
-		return ips[0]
+
+	if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
+		for _, part := range strings.Split(xff, ",") {
+			if ip, ok := extractTrustedIP(part); ok {
+				return ip
+			}
+		}
+	}
+
+	if ip, ok := extractTrustedIP(c.Request.RemoteAddr); ok {
+		return ip
+	}
+
+	return "unknown"
+}
+
+func extractTrustedIP(value string) (string, bool) {
+	candidate := strings.TrimSpace(value)
+	if candidate == "" {
+		return "", false
+	}
+
+	if ip, ok := parseIPCandidate(candidate); ok {
+		return ip.String(), true
+	}
+
+	if host, _, err := net.SplitHostPort(candidate); err == nil {
+		if ip, ok := parseIPCandidate(host); ok {
+			return ip.String(), true
+		}
+	}
+
+	if strings.Count(candidate, ":") == 1 {
+		if host, _, err := net.SplitHostPort(fmt.Sprintf("[%s]", candidate)); err == nil {
+			if ip, ok := parseIPCandidate(host); ok {
+				return ip.String(), true
+			}
+		}
+	}
+
+	return "", false
+}
+
+func parseIPCandidate(value string) (netip.Addr, bool) {
+	ip, err := netip.ParseAddr(strings.TrimSpace(value))
+	if err != nil {
+		return netip.Addr{}, false
 	}
-	addr := c.Request.RemoteAddr
-	ip, _, _ := net.SplitHostPort(addr)
-	return ip
+	return ip.Unmap(), true
 }
 
 // jsonMsg sends a JSON response with a message and error status.

+ 39 - 26
web/html/common/page.html

@@ -1,6 +1,7 @@
 {{ define "page/head_start" }}
 <!DOCTYPE html>
 <html>
+
 <head>
   <meta charset="UTF-8">
   <meta name="renderer" content="webkit">
@@ -12,6 +13,7 @@
     [v-cloak] {
       display: none;
     }
+
     /* vazirmatn-regular - arabic_latin_latin-ext */
     @font-face {
       font-display: swap;
@@ -21,10 +23,11 @@
       src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
       unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
     }
+
     body {
       font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
     }
-    
+
     /* mobile touch scrolling for tabs */
     @media (max-width: 576px) {
       .ant-tabs-nav-container {
@@ -34,59 +37,69 @@
         overscroll-behavior-x: contain;
         white-space: nowrap;
         max-width: 100%;
-        padding: 0 !important; /* Remove padding for arrows */
+        padding: 0 !important;
+        /* Remove padding for arrows */
       }
+
       .ant-tabs-nav-wrap {
         overflow: visible !important;
         padding: 0 !important;
       }
+
       .ant-tabs-nav-scroll {
         overflow: visible !important;
         box-shadow: none !important;
       }
+
       .ant-tabs-nav {
-         display: flex !important;
-         transform: none !important; /* Disable JS transform */
-         width: auto !important;
-         margin: 0 !important;
+        display: flex !important;
+        transform: none !important;
+        /* Disable JS transform */
+        width: auto !important;
+        margin: 0 !important;
       }
+
       .ant-tabs-tab-prev,
       .ant-tabs-tab-next {
-        display: none !important; /* Hide arrows */
+        display: none !important;
+        /* Hide arrows */
       }
+
       .ant-tabs-nav-container::-webkit-scrollbar {
         display: none;
       }
     }
   </style>
   <title>{{ .host }} – {{ i18n .title}}</title>
-{{ end }}
+  {{ end }}
 
-{{ define "page/head_end" }}
+  {{ define "page/head_end" }}
 </head>
 {{ end }}
 
 {{ define "page/body_start" }}
+
 <body>
   <div id="message"></div>
-{{ end }}
+  {{ end }}
 
-{{ define "page/body_scripts" }}
-<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
-<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
-<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
-<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
-<script>
-  const basePath = '{{ .base_path }}';
-  axios.defaults.baseURL = basePath;
-</script>
-<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
-{{ end }}
-  
-{{ define "page/body_end" }}
+  {{ define "page/body_scripts" }}
+  <script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
+  <script src="{{ .base_path }}assets/moment/moment.min.js"></script>
+  <script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
+  <script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
+  <script src="{{ .base_path }}assets/qs/qs.min.js"></script>
+  <script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
+  <script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
+  <script>
+    const basePath = '{{ .base_path }}';
+    axios.defaults.baseURL = basePath;
+  </script>
+  <script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
+  {{ end }}
+
+  {{ define "page/body_end" }}
 </body>
+
 </html>
 {{ end }}

+ 60 - 27
web/html/component/aClientTable.html

@@ -2,30 +2,39 @@
 <template slot="actions" slot-scope="text, client, index">
   <a-tooltip>
     <template slot="title">{{ i18n "qrCode" }}</template>
-    <a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
+    <a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode"
+      v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
   </a-tooltip>
   <a-tooltip>
     <template slot="title">{{ i18n "pages.client.edit" }}</template>
-    <a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
+    <a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit"
+      @click="openEditClient(record.id,client);"></a-icon>
   </a-tooltip>
   <a-tooltip>
     <template slot="title">{{ i18n "info" }}</template>
-    <a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
+    <a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle"
+      @click="showInfo(record.id,client);"></a-icon>
   </a-tooltip>
   <a-tooltip>
     <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
-    <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
+    <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
+      title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme"
+      ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
       <a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
-      <a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
+      <a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet"
+        v-if="client.email.length > 0"></a-icon>
     </a-popconfirm>
   </a-tooltip>
   <a-tooltip>
     <template slot="title">
       <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
     </template>
-    <a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
+    <a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}'
+      :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger"
+      cancel-text='{{ i18n "cancel"}}'>
       <a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
-      <a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
+      <a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete"
+        v-if="isRemovable(record.id)"></a-icon>
     </a-popconfirm>
   </a-tooltip>
 </template>
@@ -34,7 +43,7 @@
 </template>
 <template slot="online" slot-scope="text, client, index">
   <a-popover :overlay-class-name="themeSwitcher.currentTheme">
-    <template slot="content" >
+    <template slot="content">
       {{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
     </template>
     <template v-if="client.enable && isClientOnline(client.email)">
@@ -53,7 +62,8 @@
         <template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
         <template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
       </template>
-      <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
+      <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''"
+        :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
     </a-tooltip>
     <a-space direction="vertical" :size="2">
       <span class="client-email">[[ client.email ]]</span>
@@ -87,10 +97,13 @@
       <tr class="tr-table-box">
         <td class="tr-table-rt"> [[ SizeFormatter.sizeFormat(getSumStats(record, client.email)) ]] </td>
         <td class="tr-table-bar" v-if="!client.enable">
-          <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
+          <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false"
+            :percent="statsProgress(record, client.email)" />
         </td>
         <td class="tr-table-bar" v-else-if="client.totalGB > 0">
-          <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
+          <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false"
+            :status="isClientDepleted(record, client.email)? 'exception' : ''"
+            :percent="statsProgress(record, client.email)" />
         </td>
         <td v-else class="infinite-bar tr-table-bar">
           <a-progress :show-info="false" :percent="100"></a-progress>
@@ -118,7 +131,8 @@
         <tr class="tr-table-box">
           <td class="tr-table-rt"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
           <td class="infinite-bar tr-table-bar">
-            <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
+            <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''"
+              :percent="expireProgress(client.expiryTime, client.reset)" />
           </td>
           <td class="tr-table-lt">[[ client.reset + "d" ]]</td>
         </tr>
@@ -131,11 +145,16 @@
         <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
         <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
       </template>
-      <a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
+      <a-tag :style="{ minWidth: '50px', border: 'none' }"
+        :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[
+        IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
     </a-popover>
-    <a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag">
+    <a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"
+      :style="{ border: 'none' }" class="infinite-tag">
       <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
-        <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
+        <path
+          d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
+          fill="currentColor"></path>
       </svg>
     </a-tag>
   </template>
@@ -165,7 +184,8 @@
         <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
       </a-menu-item>
       <a-menu-item>
-        <a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id, client, $event)"></a-switch>
+        <a-switch v-model="client.enable" size="small"
+          @change="switchEnableClient(record.id, client, $event)"></a-switch>
         {{ i18n "enable"}}
       </a-menu-item>
     </a-menu>
@@ -179,9 +199,11 @@
           <td colspan="3" :style="{ textAlign: 'center' }">{{ i18n "pages.inbounds.traffic" }}</td>
         </tr>
         <tr>
-          <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
+          <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[
+            SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
           <td width="120px" v-if="!client.enable">
-            <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
+            <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false"
+              :percent="statsProgress(record, client.email)" />
           </td>
           <td width="120px" v-else-if="client.totalGB > 0">
             <a-popover :overlay-class-name="themeSwitcher.currentTheme">
@@ -197,11 +219,14 @@
                   </tr>
                 </table>
               </template>
-              <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
+              <a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false"
+                :status="isClientDepleted(record, client.email)? 'exception' : ''"
+                :percent="statsProgress(record, client.email)" />
             </a-popover>
           </td>
           <td width="120px" v-else class="infinite-bar">
-            <a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false" :percent="100"></a-progress>
+            <a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false"
+              :percent="100"></a-progress>
           </td>
           <td width="80px">
             <template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
@@ -216,14 +241,16 @@
         </tr>
         <tr>
           <template v-if="client.expiryTime !=0 && client.reset >0">
-            <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
+            <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[
+              IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
             <td width="120px" class="infinite-bar">
               <a-popover :overlay-class-name="themeSwitcher.currentTheme">
                 <template slot="content">
                   <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
                   <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
                 </template>
-                <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
+                <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''"
+                  :percent="expireProgress(client.expiryTime, client.reset)" />
               </a-popover>
             </td>
             <td width="60px">[[ client.reset + "d" ]]</td>
@@ -235,11 +262,16 @@
                   <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
                   <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
                 </template>
-                <a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
+                <a-tag :style="{ minWidth: '50px', border: 'none' }"
+                  :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[
+                  IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
               </a-popover>
-              <a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
+              <a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"
+                class="infinite-tag">
                 <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
-                  <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
+                  <path
+                    d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
+                    fill="currentColor"></path>
                 </svg>
               </a-tag>
           </template>
@@ -248,7 +280,8 @@
       </table>
     </template>
     <a-badge>
-      <a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
+      <a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled"
+        :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
       <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
         <a-icon type="solution"></a-icon>
       </a-button>
@@ -271,4 +304,4 @@
     -
   </template>
 </template>
-{{end}}
+{{end}}

+ 11 - 9
web/html/component/aCustomStatistic.html

@@ -1,13 +1,13 @@
 {{define "component/customStatistic"}}
 <template>
-    <a-statistic :title="title" :value="value">
-        <template #prefix>
-            <slot name="prefix"></slot>
-        </template>
-        <template #suffix>
-            <slot name="suffix"></slot>
-        </template>
-    </a-statistic>
+  <a-statistic :title="title" :value="value">
+    <template #prefix>
+      <slot name="prefix"></slot>
+    </template>
+    <template #suffix>
+      <slot name="suffix"></slot>
+    </template>
+  </a-statistic>
 </template>
 {{end}}
 
@@ -16,9 +16,11 @@
   .dark .ant-statistic-content {
     color: var(--dark-color-text-primary)
   }
+
   .dark .ant-statistic-title {
     color: rgba(255, 255, 255, 0.55)
   }
+
   .ant-statistic-content {
     font-size: 16px;
   }
@@ -36,7 +38,7 @@
         required: false
       }
     },
-    template: `{{template "component/customStatistic"}}`,
+    template: `{{template "component/customStatistic" .}}`,
   });
 </script>
 {{end}}

+ 4 - 3
web/html/component/aPersianDatepicker.html

@@ -34,7 +34,7 @@
                 required: false,
             },
         },
-        template: `{{template "component/persianDatepickerTemplate"}}`,
+        template: `{{template "component/persianDatepickerTemplate" .}}`,
         data() {
             return {
                 date: '',
@@ -42,7 +42,7 @@
             };
         },
         watch: {
-            value: function (date) {
+            value: function(date) {
                 this.date = this.convertToJalalian(date)
             }
         },
@@ -52,7 +52,8 @@
         },
         methods: {
             convertToGregorian(date) {
-                return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) : null
+                return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) :
+                    null
             },
             convertToJalalian(date) {
                 return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null

+ 2 - 2
web/html/component/aSettingListItem.html

@@ -26,7 +26,7 @@
                 type: String,
                 required: false,
                 defaultValue: "default",
-                validator: function (value) {
+                validator: function(value) {
                     return ['small', 'default'].includes(value)
                 }
             }
@@ -46,4 +46,4 @@
         }
     })
 </script>
-{{end}}
+{{end}}

+ 4 - 5
web/html/component/aSidebar.html

@@ -43,8 +43,7 @@
     Vue.component('a-sidebar', {
         data() {
             return {
-                tabs: [
-                    {
+                tabs: [{
                         key: '{{ .base_path }}panel/',
                         icon: 'dashboard',
                         title: '{{ i18n "menu.dashboard"}}'
@@ -79,8 +78,8 @@
         },
         methods: {
             openLink(key) {
-                return key.startsWith('http') ? 
-                    window.open(key) : 
+                return key.startsWith('http') ?
+                    window.open(key) :
                     location.href = key
             },
             closeDrawer() {
@@ -97,7 +96,7 @@
                 }
             }
         },
-        template: `{{template "component/sidebar/content"}}`,
+        template: `{{template "component/sidebar/content" .}}`,
     });
 </script>
 {{end}}

+ 11 - 10
web/html/component/aTableSortable.html

@@ -1,6 +1,6 @@
 {{define "component/sortableTableTrigger"}}
-<a-icon type="drag" class="sortable-icon" :style="{ cursor: 'move' }" @mouseup="mouseUpHandler" @mousedown="mouseDownHandler"
-  @click="clickHandler" />
+<a-icon type="drag" class="sortable-icon" :style="{ cursor: 'move' }" @mouseup="mouseUpHandler"
+  @mousedown="mouseDownHandler" @click="clickHandler" />
 {{end}}
 
 {{define "component/aTableSortable"}}
@@ -49,7 +49,7 @@
         sortable,
       }
     },
-    render: function (createElement) {
+    render: function(createElement) {
       return createElement('a-table', {
         class: {
           'ant-table-is-sorting': this.isDragging(),
@@ -64,12 +64,12 @@
           drop: (e) => this.dropHandler(e),
         },
         scopedSlots: this.$scopedSlots,
-        locale: { 
-          filterConfirm: `{{ i18n "confirm" }}`, 
-          filterReset: `{{ i18n "reset" }}`, 
-          emptyText: `{{ i18n "noData" }}` 
+        locale: {
+          filterConfirm: `{{ i18n "confirm" }}`,
+          filterReset: `{{ i18n "reset" }}`,
+          emptyText: `{{ i18n "noData" }}`
         }
-      }, this.$slots.default,)
+      }, this.$slots.default, )
     },
     created() {
       this.$memoSort = {};
@@ -148,7 +148,8 @@
           class: {
             ...(parentMethodResult?.class || {}),
             [DRAGGABLE_ROW_CLASS]: true,
-            ['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false,
+            ['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) :
+              false,
           },
         };
       }
@@ -174,7 +175,7 @@
     }
   });
   Vue.component('a-table-sort-trigger', {
-    template: `{{template "component/sortableTableTrigger"}}`,
+    template: `{{template "component/sortableTableTrigger" .}}`,
     props: {
       'item-index': {
         type: undefined,

+ 6 - 4
web/html/component/aThemeSwitch.html

@@ -24,9 +24,11 @@
 
 {{define "component/themeSwitchTemplateLogin"}}
 <template>
-  <a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }">
+  <a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10"
+    :style="{ width: '100%' }">
     <a-space direction="horizontal" size="small">
-      <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
+      <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
+        @change="themeSwitcher.toggleTheme()"></a-switch>
       <span>{{ i18n "menu.dark" }}</span>
     </a-space>
     <a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small">
@@ -93,7 +95,7 @@
   }
   const themeSwitcher = createThemeSwitcher();
   Vue.component('a-theme-switch', {
-    template: `{{template "component/themeSwitchTemplate"}}`,
+    template: `{{template "component/themeSwitchTemplate" .}}`,
     data: () => ({
       themeSwitcher
     }),
@@ -105,7 +107,7 @@
     }
   });
   Vue.component('a-theme-switch-login', {
-    template: `{{template "component/themeSwitchTemplateLogin"}}`,
+    template: `{{template "component/themeSwitchTemplateLogin" .}}`,
     data: () => ({
       themeSwitcher
     }),

+ 29 - 98
web/html/form/client.html

@@ -1,11 +1,5 @@
 {{define "form/client"}}
-<a-form
-  layout="horizontal"
-  v-if="client"
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
     <a-switch v-model="client.enable"></a-switch>
   </a-form-item>
@@ -16,33 +10,22 @@
           <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
         </template>
         {{ i18n "pages.inbounds.email" }}
-        <a-icon
-          type="sync"
-          @click="client.email = RandomUtil.randomLowerAndNum(9)"
-        ></a-icon>
+        <a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
       </a-tooltip>
     </template>
     <a-input v-model.trim="client.email"></a-input>
   </a-form-item>
-  <a-form-item
-    v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS"
-  >
+  <a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
     <template slot="label">
       <a-tooltip>
         <template slot="title">
           <span>{{ i18n "reset" }}</span>
         </template>
         {{ i18n "password" }}
-        <a-icon
-          v-if="inbound.protocol === Protocols.SHADOWSOCKS"
-          @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
-          type="sync"
-        ></a-icon>
-        <a-icon
-          v-if="inbound.protocol === Protocols.TROJAN"
-          @click="client.password = RandomUtil.randomSeq(10)"
-          type="sync"
-        >
+        <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
+          @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
+        <a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"
+          type="sync">
         </a-icon>
       </a-tooltip>
     </template>
@@ -55,42 +38,26 @@
           <span>{{ i18n "reset" }}</span>
         </template>
         Auth Password
-        <a-icon
-          @click="client.auth = RandomUtil.randomSeq(10)"
-          type="sync"
-        ></a-icon>
+        <a-icon @click="client.auth = RandomUtil.randomSeq(10)" type="sync"></a-icon>
       </a-tooltip>
     </template>
     <a-input v-model.trim="client.auth"></a-input>
   </a-form-item>
-  <a-form-item
-    v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"
-  >
+  <a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
     <template slot="label">
       <a-tooltip>
         <template slot="title">
           <span>{{ i18n "reset" }}</span>
         </template>
         ID
-        <a-icon
-          @click="client.id = RandomUtil.randomUUID()"
-          type="sync"
-        ></a-icon>
+        <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
       </a-tooltip>
     </template>
     <a-input v-model.trim="client.id"></a-input>
   </a-form-item>
-  <a-form-item
-    v-if="inbound.protocol === Protocols.VMESS"
-    label='{{ i18n "security" }}'
-  >
-    <a-select
-      v-model="client.security"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
-      <a-select-option v-for="key in USERS_SECURITY" :value="key"
-        >[[ key ]]</a-select-option
-      >
+  <a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
+    <a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
+      <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
     </a-select>
   </a-form-item>
   <a-form-item v-if="client.email && app.subSettings?.enable">
@@ -100,10 +67,7 @@
           <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
         </template>
         Subscription
-        <a-icon
-          @click="client.subId = RandomUtil.randomLowerAndNum(16)"
-          type="sync"
-        ></a-icon>
+        <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
       </a-tooltip>
     </template>
     <a-input v-model.trim="client.subId"></a-input>
@@ -118,11 +82,7 @@
         <a-icon type="question-circle"></a-icon>
       </a-tooltip>
     </template>
-    <a-input-number
-      :style="{ width: '50%' }"
-      v-model.number="client.tgId"
-      min="0"
-    ></a-input-number>
+    <a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
   </a-form-item>
   <a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
     <a-input v-model.trim="client.comment"></a-input>
@@ -139,9 +99,7 @@
     </template>
     <a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
   </a-form-item>
-  <a-form-item
-    v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"
-  >
+  <a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
     <template slot="label">
       <a-tooltip>
         <template slot="title">
@@ -160,25 +118,15 @@
       </span>
     </a-tooltip>
     <a-form layout="block">
-      <a-textarea
-        id="clientIPs"
-        readonly
-        @click="getDBClientIps(client.email)"
-        placeholder="Click To Get IPs"
-        :auto-size="{ minRows: 5, maxRows: 10 }"
-      >
+      <a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)" placeholder="Click To Get IPs"
+        :auto-size="{ minRows: 5, maxRows: 10 }">
       </a-textarea>
     </a-form>
   </a-form-item>
   <a-form-item v-if="inbound.canEnableTlsFlow()" label="Flow">
-    <a-select
-      v-model="client.flow"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+    <a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value selected>{{ i18n "none" }}</a-select-option>
-      <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key"
-        >[[ key ]]</a-select-option
-      >
+      <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
     </a-select>
   </a-form-item>
   <a-form-item>
@@ -201,45 +149,28 @@
     </a-tag>
     <a-tooltip>
       <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
-      <a-icon
-        type="retweet"
-        @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
-        v-if="client.email.length > 0"
-      ></a-icon>
+      <a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
+        v-if="client.email.length > 0"></a-icon>
     </a-tooltip>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
     <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
   </a-form-item>
   <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
-    <a-input-number
-      v-model.number="delayedExpireDays"
-      :min="0"
-    ></a-input-number>
+    <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
   </a-form-item>
   <a-form-item v-else>
     <template slot="label">
       <a-tooltip>
-        <template slot="title"
-          >{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template
-        >
+        <template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template>
         {{ i18n "pages.inbounds.expireDate" }}
         <a-icon type="question-circle"></a-icon>
       </a-tooltip>
     </template>
-    <a-date-picker
-      v-if="datepicker == 'gregorian'"
-      :show-time="{ format: 'HH:mm:ss' }"
-      format="YYYY-MM-DD HH:mm:ss"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-      v-model="client._expiryTime"
-    ></a-date-picker>
-    <a-persian-datepicker
-      v-else
-      placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
-      value="client._expiryTime"
-      v-model="client._expiryTime"
-    ></a-persian-datepicker>
+    <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
+      :dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
+    <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
+      value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker>
     <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
   </a-form-item>
   <a-form-item v-if="client.expiryTime != 0">
@@ -253,4 +184,4 @@
     <a-input-number v-model.number="client.reset" :min="0"></a-input-number>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 14 - 14
web/html/form/inbound.html

@@ -102,69 +102,69 @@
 
 <!-- vmess settings -->
 <template v-if="inbound.protocol === Protocols.VMESS">
-    {{template "form/vmess"}}
+    {{template "form/vmess" .}}
 </template>
 
 <!-- vless settings -->
 <template v-if="inbound.protocol === Protocols.VLESS">
-    {{template "form/vless"}}
+    {{template "form/vless" .}}
 </template>
 
 <!-- trojan settings -->
 <template v-if="inbound.protocol === Protocols.TROJAN">
-    {{template "form/trojan"}}
+    {{template "form/trojan" .}}
 </template>
 
 <!-- shadowsocks -->
 <template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
-    {{template "form/shadowsocks"}}
+    {{template "form/shadowsocks" .}}
 </template>
 
 <!-- tunnel -->
 <template v-if="inbound.protocol === Protocols.TUNNEL">
-    {{template "form/tunnel"}}
+    {{template "form/tunnel" .}}
 </template>
 
 <!-- mixed -->
 <template v-if="inbound.protocol === Protocols.MIXED">
-    {{template "form/mixed"}}
+    {{template "form/mixed" .}}
 </template>
 
 <!-- http -->
 <template v-if="inbound.protocol === Protocols.HTTP">
-    {{template "form/http"}}
+    {{template "form/http" .}}
 </template>
 
 <!-- wireguard -->
 <template v-if="inbound.protocol === Protocols.WIREGUARD">
-    {{template "form/wireguard"}}
+    {{template "form/wireguard" .}}
 </template>
 
 <!-- tun -->
 <template v-if="inbound.protocol === Protocols.TUN">
-    {{template "form/tun"}}
+    {{template "form/tun" .}}
 </template>
 
 <!-- hysteria -->
 <template v-if="inbound.protocol === Protocols.HYSTERIA">
-    {{template "form/hysteria"}}
+    {{template "form/hysteria" .}}
 </template>
 
 <!-- stream settings -->
 <template v-if="inbound.canEnableStream()">
-    {{template "form/streamSettings"}}
-    {{template "form/externalProxy" }}
+    {{template "form/streamSettings" .}}
+    {{template "form/externalProxy" .}}
 </template>
 
 <!-- tls settings -->
 <template v-if="inbound.canEnableTls()">
-    {{template "form/tlsSettings"}}
+    {{template "form/tlsSettings" .}}
 </template>
 
 <!-- sniffing -->
 <a-collapse>
     <a-collapse-panel header='Sniffing'>
-        {{template "form/sniffing"}}
+        {{template "form/sniffing" .}}
     </a-collapse-panel>
 </a-collapse>
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 205 - 476
web/html/form/outbound.html


+ 3 - 3
web/html/form/protocol/dokodemo.html

@@ -25,13 +25,13 @@
             <a-select-option value="tcp">TCP</a-select-option>
             <a-select-option value="udp">UDP</a-select-option>
         </a-select>
-    </a-form-item>           
+    </a-form-item>
     <a-form-item label='Follow Redirect'>
         <a-switch v-model="inbound.settings.followRedirect"></a-switch>
     </a-form-item>
 </a-form>
 <!-- sockopt -->
 <template>
-    {{template "form/streamSockopt"}}
+    {{template "form/streamSockopt" .}}
 </template>
-{{end}}
+{{end}}

+ 3 - 2
web/html/form/protocol/http.html

@@ -5,7 +5,8 @@
       <td width="45%">{{ i18n "username" }}</td>
       <td width="45%">{{ i18n "password" }}</td>
       <td>
-        <a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
+        <a-button icon="plus" size="small"
+          @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
       </td>
     </tr>
   </table>
@@ -23,4 +24,4 @@
     <a-switch v-model="inbound.settings.allowTransparent" />
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 4 - 8
web/html/form/protocol/hysteria.html

@@ -1,9 +1,7 @@
 {{define "form/hysteria"}}
-<a-collapse activeKey="0"
-    v-for="(client, index) in inbound.settings.hysterias.slice(0,1)"
-    v-if="!isEdit">
+<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.hysterias.slice(0,1)" v-if="!isEdit">
     <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
-        {{template "form/client"}}
+        {{template "form/client" .}}
     </a-collapse-panel>
 </a-collapse>
 <a-collapse v-else>
@@ -22,11 +20,9 @@
         </table>
     </a-collapse-panel>
 </a-collapse>
-<a-form :colon="false" :label-col="{ md: {span:8} }"
-    :wrapper-col="{ md: {span:14} }">
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
     <a-form-item :label="'{{ i18n "pages.inbounds.stream.tcp.version" }}'">
-        <a-input-number v-model.number="inbound.settings.version" :min="2"
-            :max="2" disabled></a-input-number>
+        <a-input-number v-model.number="inbound.settings.version" :min="2" :max="2" disabled></a-input-number>
     </a-form-item>
 </a-form>
 {{end}}

+ 13 - 33
web/html/form/protocol/shadowsocks.html

@@ -1,12 +1,8 @@
 {{define "form/shadowsocks"}}
 <template v-if="inbound.isSSMultiUser">
-  <a-collapse
-    activeKey="0"
-    v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)"
-    v-if="!isEdit"
-  >
+  <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
     <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
-      {{template "form/client"}}
+      {{template "form/client" .}}
     </a-collapse-panel>
   </a-collapse>
   <a-collapse v-else>
@@ -16,10 +12,8 @@
           <th>{{ i18n "pages.inbounds.email" }}</th>
           <th>Password</th>
         </tr>
-        <tr
-          v-for="(client, index) in inbound.settings.shadowsockses"
-          :class="index % 2 == 1 ? ' client-table-odd-row' : ''"
-        >
+        <tr v-for="(client, index) in inbound.settings.shadowsockses"
+          :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
           <td>[[ client.email ]]</td>
           <td>[[ client.password ]]</td>
         </tr>
@@ -27,20 +21,11 @@
     </a-collapse-panel>
   </a-collapse>
 </template>
-<a-form
-  :colon=" false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon=" false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label='{{ i18n "encryption" }}'>
-    <a-select
-      v-model="inbound.settings.method"
-      @change="SSMethodChange"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
-      <a-select-option v-for="(method,method_name) in SSMethods" :value="method"
-        >[[ method_name ]]</a-select-option
-      >
+    <a-select v-model="inbound.settings.method" @change="SSMethodChange"
+      :dropdown-class-name="themeSwitcher.currentTheme">
+      <a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
     </a-select>
   </a-form-item>
   <a-form-item v-if="inbound.isSS2022">
@@ -50,20 +35,15 @@
           <span>{{ i18n "reset" }}</span>
         </template>
         Password
-        <a-icon
-          @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
-          type="sync"
-        ></a-icon>
+        <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
+          type="sync"></a-icon>
       </a-tooltip>
     </template>
     <a-input v-model.trim="inbound.settings.password"></a-input>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
-    <a-select
-      v-model="inbound.settings.network"
-      :style="{ width: '100px' }"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+    <a-select v-model="inbound.settings.network" :style="{ width: '100px' }"
+      :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value="tcp,udp">TCP,UDP</a-select-option>
       <a-select-option value="tcp">TCP</a-select-option>
       <a-select-option value="udp">UDP</a-select-option>
@@ -73,4 +53,4 @@
     <a-switch v-model="inbound.settings.ivCheck"></a-switch>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 11 - 38
web/html/form/protocol/socks.html

@@ -1,9 +1,5 @@
 {{define "form/mixed"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
     <a-switch v-model="inbound.settings.udp"></a-switch>
   </a-form-item>
@@ -11,10 +7,8 @@
     <a-input v-model.trim="inbound.settings.ip"></a-input>
   </a-form-item>
   <a-form-item label='{{ i18n "password" }}'>
-    <a-switch
-      :checked="inbound.settings.auth === 'password'"
-      @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"
-    ></a-switch>
+    <a-switch :checked="inbound.settings.auth === 'password'"
+      @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
   </a-form-item>
   <template v-if="inbound.settings.auth === 'password'">
     <table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
@@ -22,42 +16,21 @@
         <td width="45%">{{ i18n "username" }}</td>
         <td width="45%">{{ i18n "password" }}</td>
         <td>
-          <a-button
-            icon="plus"
-            size="small"
-            @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"
-          ></a-button>
+          <a-button icon="plus" size="small"
+            @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
         </td>
       </tr>
     </table>
-    <a-input-group
-      compact
-      v-for="(account, index) in inbound.settings.accounts"
-      :style="{ marginBottom: '10px' }"
-    >
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="account.user"
-        placeholder='{{ i18n "username" }}'
-      >
-        <template slot="addonBefore" :style="{ margin: '0' }"
-          >[[ index+1 ]]</template
-        >
+    <a-input-group compact v-for="(account, index) in inbound.settings.accounts" :style="{ marginBottom: '10px' }">
+      <a-input :style="{ width: '50%' }" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
+        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
       </a-input>
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="account.pass"
-        placeholder='{{ i18n "password" }}'
-      >
+      <a-input :style="{ width: '50%' }" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
         <template slot="addonAfter">
-          <a-button
-            icon="minus"
-            size="small"
-            @click="inbound.settings.delAccount(index)"
-          ></a-button>
+          <a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
         </template>
       </a-input>
     </a-input-group>
   </template>
 </a-form>
-{{end}}
+{{end}}

+ 32 - 32
web/html/form/protocol/trojan.html

@@ -1,7 +1,7 @@
 {{define "form/trojan"}}
 <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
   <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
-    {{template "form/client"}}
+    {{template "form/client" .}}
   </a-collapse-panel>
 </a-collapse>
 <a-collapse v-else>
@@ -19,35 +19,35 @@
   </a-collapse-panel>
 </a-collapse>
 <template v-if=" inbound.isTcp">
-    <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-      <a-form-item label="Fallbacks">
-        <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
-      </a-form-item>
-    </a-form>
+  <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+    <a-form-item label="Fallbacks">
+      <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
+    </a-form-item>
+  </a-form>
 
-    <!-- trojan fallbacks -->
-    <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
-      :wrapper-col="{ md: {span:14} }">
-      <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
-          @click="() => inbound.settings.delFallback(index)"
-          :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
-      </a-divider>
-      <a-form-item label='SNI'>
-        <a-input v-model="fallback.name"></a-input>
-      </a-form-item>
-      <a-form-item label='ALPN'>
-        <a-input v-model="fallback.alpn"></a-input>
-      </a-form-item>
-      <a-form-item label='Path'>
-        <a-input v-model="fallback.path"></a-input>
-      </a-form-item>
-      <a-form-item label='Dest'>
-        <a-input v-model="fallback.dest"></a-input>
-      </a-form-item>
-      <a-form-item label='xVer'>
-        <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
-      </a-form-item>
-    </a-form>
-    <a-divider style="margin:5px 0;"></a-divider>
-    </template>
-    {{end}}
+  <!-- trojan fallbacks -->
+  <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
+    :wrapper-col="{ md: {span:14} }">
+    <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
+        @click="() => inbound.settings.delFallback(index)"
+        :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+    </a-divider>
+    <a-form-item label='SNI'>
+      <a-input v-model="fallback.name"></a-input>
+    </a-form-item>
+    <a-form-item label='ALPN'>
+      <a-input v-model="fallback.alpn"></a-input>
+    </a-form-item>
+    <a-form-item label='Path'>
+      <a-input v-model="fallback.path"></a-input>
+    </a-form-item>
+    <a-form-item label='Dest'>
+      <a-input v-model="fallback.dest"></a-input>
+    </a-form-item>
+    <a-form-item label='xVer'>
+      <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
+    </a-form-item>
+  </a-form>
+  <a-divider style="margin:5px 0;"></a-divider>
+</template>
+{{end}}

+ 12 - 48
web/html/form/protocol/tun.html

@@ -1,9 +1,5 @@
 {{define "form/tun"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item>
     <template slot="label">
       <a-tooltip>
@@ -26,38 +22,18 @@
         <a-icon type="question-circle"></a-icon>
       </a-tooltip>
     </template>
-    <a-input-number
-      v-model.number="inbound.settings.mtu[0]"
-      :min="1"
-      :max="9000"
-      placeholder="1500"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.settings.mtu[0]" :min="1" :max="9000" placeholder="1500"></a-input-number>
   </a-form-item>
   <a-form-item label="MTU IPv6">
-    <a-input-number
-      v-model.number="inbound.settings.mtu[1]"
-      :min="1"
-      :max="9000"
-      placeholder="1280"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.settings.mtu[1]" :min="1" :max="9000" placeholder="1280"></a-input-number>
   </a-form-item>
   <a-form-item label="Gateway">
-    <a-select
-      mode="tags"
-      v-model="inbound.settings.gateway"
-      :style="{ width: '100%' }"
-      :token-separators="[',']"
-      placeholder="IPv4/IPv6 gateway"
-    ></a-select>
+    <a-select mode="tags" v-model="inbound.settings.gateway" :style="{ width: '100%' }" :token-separators="[',']"
+      placeholder="IPv4/IPv6 gateway"></a-select>
   </a-form-item>
   <a-form-item label="DNS">
-    <a-select
-      mode="tags"
-      v-model="inbound.settings.dns"
-      :style="{ width: '100%' }"
-      :token-separators="[',']"
-      placeholder="DNS servers"
-    ></a-select>
+    <a-select mode="tags" v-model="inbound.settings.dns" :style="{ width: '100%' }" :token-separators="[',']"
+      placeholder="DNS servers"></a-select>
   </a-form-item>
   <a-form-item>
     <template slot="label">
@@ -69,26 +45,14 @@
         <a-icon type="question-circle"></a-icon>
       </a-tooltip>
     </template>
-    <a-input-number
-      v-model.number="inbound.settings.userLevel"
-      :min="0"
-      placeholder="0"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.settings.userLevel" :min="0" placeholder="0"></a-input-number>
   </a-form-item>
   <a-form-item label="Auto Routing Table">
-    <a-select
-      mode="tags"
-      v-model="inbound.settings.autoSystemRoutingTable"
-      :style="{ width: '100%' }"
-      :token-separators="[',']"
-      placeholder="e.g. vpn, proxy"
-    ></a-select>
+    <a-select mode="tags" v-model="inbound.settings.autoSystemRoutingTable" :style="{ width: '100%' }"
+      :token-separators="[',']" placeholder="e.g. vpn, proxy"></a-select>
   </a-form-item>
   <a-form-item label="Auto Outbounds">
-    <a-input
-      v-model.trim="inbound.settings.autoOutboundsInterface"
-      placeholder="auto"
-    ></a-input>
+    <a-input v-model.trim="inbound.settings.autoOutboundsInterface" placeholder="auto"></a-input>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 102 - 103
web/html/form/protocol/vless.html

@@ -1,7 +1,7 @@
 {{define "form/vless"}}
 <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
   <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
-    {{template "form/client"}}
+    {{template "form/client" .}}
   </a-collapse-panel>
 </a-collapse>
 <a-collapse v-else>
@@ -12,8 +12,7 @@
         <th>{{ i18n "pages.inbounds.email" }}</th>
         <th>ID</th>
       </tr>
-      <tr v-for="(client, index) in inbound.settings.vlesses"
-        :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
+      <tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
         <td>[[ client.email ]]</td>
         <td>[[ client.id ]]</td>
       </tr>
@@ -21,104 +20,104 @@
   </a-collapse-panel>
 </a-collapse>
 <template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
-    <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-      <a-form-item label="Authentication">
-        <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
-          :dropdown-class-name="themeSwitcher.currentTheme">
-          <a-select-option :value="undefined">None</a-select-option>
-          <a-select-option value="X25519, not Post-Quantum">X25519 (not
-            Post-Quantum)</a-select-option>
-          <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
-            (Post-Quantum)</a-select-option>
-        </a-select>
-      </a-form-item>
-      <a-form-item label="decryption">
-        <a-input v-model.trim="inbound.settings.decryption"></a-input>
-      </a-form-item>
-      <a-form-item label="encryption">
-        <a-input v-model="inbound.settings.encryption"></a-input>
-      </a-form-item>
-      <a-form-item label=" ">
-        <a-space>
-          <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
-            keys</a-button>
-          <a-button danger @click="clearVlessEnc">Clear</a-button>
-        </a-space>
-      </a-form-item>
-    </a-form>
-    <a-divider :style="{ margin: '5px 0' }"></a-divider>
-    </template>
-    <template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
-      <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-        <a-form-item label="Fallbacks">
-          <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
-        </a-form-item>
-      </a-form>
+  <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+    <a-form-item label="Authentication">
+      <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
+        :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select-option :value="undefined">None</a-select-option>
+        <a-select-option value="X25519, not Post-Quantum">X25519 (not
+          Post-Quantum)</a-select-option>
+        <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
+          (Post-Quantum)</a-select-option>
+      </a-select>
+    </a-form-item>
+    <a-form-item label="decryption">
+      <a-input v-model.trim="inbound.settings.decryption"></a-input>
+    </a-form-item>
+    <a-form-item label="encryption">
+      <a-input v-model="inbound.settings.encryption"></a-input>
+    </a-form-item>
+    <a-form-item label=" ">
+      <a-space>
+        <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
+          keys</a-button>
+        <a-button danger @click="clearVlessEnc">Clear</a-button>
+      </a-space>
+    </a-form-item>
+  </a-form>
+  <a-divider :style="{ margin: '5px 0' }"></a-divider>
+</template>
+<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
+  <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+    <a-form-item label="Fallbacks">
+      <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
+    </a-form-item>
+  </a-form>
 
-      <!-- vless fallbacks -->
-      <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
-        :wrapper-col="{ md: {span:14} }">
-        <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
-            @click="() => inbound.settings.delFallback(index)"
-            :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
-        </a-divider>
-        <a-form-item label='SNI'>
-          <a-input v-model="fallback.name"></a-input>
-        </a-form-item>
-        <a-form-item label='ALPN'>
-          <a-input v-model="fallback.alpn"></a-input>
-        </a-form-item>
-        <a-form-item label='Path'>
-          <a-input v-model="fallback.path"></a-input>
-        </a-form-item>
-        <a-form-item label='Dest'>
-          <a-input v-model="fallback.dest"></a-input>
-        </a-form-item>
-        <a-form-item label='xVer'>
-          <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
-        </a-form-item>
-      </a-form>
-      <a-divider :style="{ margin: '5px 0' }"></a-divider>
-    </template>
-    <template v-if="inbound.canEnableVisionSeed()">
-      <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
-        <a-form-item label="Vision Seed">
-          <a-row :gutter="8">
-            <a-col :span="6">
-              <a-input-number
-                :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
-                @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }"
-                placeholder="900" addon-before="[0]"></a-input-number>
-            </a-col>
-            <a-col :span="6">
-              <a-input-number
-                :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
-                @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }"
-                placeholder="500" addon-before="[1]"></a-input-number>
-            </a-col>
-            <a-col :span="6">
-              <a-input-number
-                :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
-                @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }"
-                placeholder="900" addon-before="[2]"></a-input-number>
-            </a-col>
-            <a-col :span="6">
-              <a-input-number
-                :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
-                @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }"
-                placeholder="256" addon-before="[3]"></a-input-number>
-            </a-col>
-          </a-row>
-          <a-space :size="8" :style="{ marginTop: '8px' }">
-            <a-button type="primary" @click="setRandomTestseed">
-              Rand
-            </a-button>
-            <a-button @click="resetTestseed">
-              Reset
-            </a-button>
-          </a-space>
-        </a-form-item>
-      </a-form>
-      <a-divider :style="{ margin: '5px 0' }"></a-divider>
-    </template>
-    {{end}}
+  <!-- vless fallbacks -->
+  <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
+    :wrapper-col="{ md: {span:14} }">
+    <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
+        @click="() => inbound.settings.delFallback(index)"
+        :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+    </a-divider>
+    <a-form-item label='SNI'>
+      <a-input v-model="fallback.name"></a-input>
+    </a-form-item>
+    <a-form-item label='ALPN'>
+      <a-input v-model="fallback.alpn"></a-input>
+    </a-form-item>
+    <a-form-item label='Path'>
+      <a-input v-model="fallback.path"></a-input>
+    </a-form-item>
+    <a-form-item label='Dest'>
+      <a-input v-model="fallback.dest"></a-input>
+    </a-form-item>
+    <a-form-item label='xVer'>
+      <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
+    </a-form-item>
+  </a-form>
+  <a-divider :style="{ margin: '5px 0' }"></a-divider>
+</template>
+<template v-if="inbound.canEnableVisionSeed()">
+  <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+    <a-form-item label="Vision Seed">
+      <a-row :gutter="8">
+        <a-col :span="6">
+          <a-input-number
+            :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
+            @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
+            addon-before="[0]"></a-input-number>
+        </a-col>
+        <a-col :span="6">
+          <a-input-number
+            :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
+            @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500"
+            addon-before="[1]"></a-input-number>
+        </a-col>
+        <a-col :span="6">
+          <a-input-number
+            :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
+            @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
+            addon-before="[2]"></a-input-number>
+        </a-col>
+        <a-col :span="6">
+          <a-input-number
+            :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
+            @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256"
+            addon-before="[3]"></a-input-number>
+        </a-col>
+      </a-row>
+      <a-space :size="8" :style="{ marginTop: '8px' }">
+        <a-button type="primary" @click="setRandomTestseed">
+          Rand
+        </a-button>
+        <a-button @click="resetTestseed">
+          Reset
+        </a-button>
+      </a-space>
+    </a-form-item>
+  </a-form>
+  <a-divider :style="{ margin: '5px 0' }"></a-divider>
+</template>
+{{end}}

+ 3 - 3
web/html/form/protocol/vmess.html

@@ -1,7 +1,7 @@
 {{define "form/vmess"}}
 <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
     <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
-        {{template "form/client"}}
+        {{template "form/client" .}}
     </a-collapse-panel>
 </a-collapse>
 <a-collapse v-else>
@@ -12,8 +12,8 @@
                 <th>ID</th>
                 <th>{{ i18n "security" }}</th>
             </tr>
-            <tr v-for="(client, index) in inbound.settings.vmesses" 
-            :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
+            <tr v-for="(client, index) in inbound.settings.vmesses"
+                :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
                 <td>[[ client.email ]]</td>
                 <td>[[ client.id ]]</td>
                 <td>[[ client.security ]]</td>

+ 7 - 23
web/html/form/sniffing.html

@@ -1,9 +1,5 @@
 {{define "form/sniffing"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item>
     <span slot="label">
       {{ i18n "enabled" }}
@@ -19,9 +15,7 @@
   <template v-if="inbound.sniffing.enabled">
     <a-form-item :wrapper-col="{span:24}">
       <a-checkbox-group v-model="inbound.sniffing.destOverride">
-        <a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key"
-          >[[ value ]]</a-checkbox
-        >
+        <a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
       </a-checkbox-group>
     </a-form-item>
     <a-form-item label="Metadata Only">
@@ -31,23 +25,13 @@
       <a-switch v-model="inbound.sniffing.routeOnly"></a-switch>
     </a-form-item>
     <a-form-item label="IPs Excluded">
-      <a-select
-        mode="tags"
-        v-model="inbound.sniffing.ipsExcluded"
-        :style="{ width: '100%' }"
-        :token-separators="[',']"
-        placeholder="IP/CIDR/geoip:*/ext:*"
-      ></a-select>
+      <a-select mode="tags" v-model="inbound.sniffing.ipsExcluded" :style="{ width: '100%' }" :token-separators="[',']"
+        placeholder="IP/CIDR/geoip:*/ext:*"></a-select>
     </a-form-item>
     <a-form-item label="Domains Excluded">
-      <a-select
-        mode="tags"
-        v-model="inbound.sniffing.domainsExcluded"
-        :style="{ width: '100%' }"
-        :token-separators="[',']"
-        placeholder="domain:*/ext:*"
-      ></a-select>
+      <a-select mode="tags" v-model="inbound.sniffing.domainsExcluded" :style="{ width: '100%' }"
+        :token-separators="[',']" placeholder="domain:*/ext:*"></a-select>
     </a-form-item>
   </template>
 </a-form>
-{{end}}
+{{end}}

+ 12 - 48
web/html/form/stream/external_proxy.html

@@ -1,67 +1,31 @@
 {{define "form/externalProxy"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
   <a-form-item label="External Proxy">
     <a-switch v-model="externalProxy"></a-switch>
-    <a-button
-      icon="plus"
-      v-if="externalProxy"
-      type="primary"
-      :style="{ marginLeft: '10px' }"
-      size="small"
-      @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"
-    ></a-button>
+    <a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small"
+      @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
   </a-form-item>
-  <a-input-group
-    :style="{ margin: '8px 0' }"
-    compact
-    v-for="(row, index) in inbound.stream.externalProxy"
-  >
+  <a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
     <template>
       <a-tooltip title="Force TLS">
-        <a-select
-          v-model="row.forceTls"
-          :style="{ width: '20%', margin: '0px' }"
-          :dropdown-class-name="themeSwitcher.currentTheme"
-        >
-          <a-select-option value="same"
-            >{{ i18n "pages.inbounds.same" }}</a-select-option
-          >
+        <a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }"
+          :dropdown-class-name="themeSwitcher.currentTheme">
+          <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
           <a-select-option value="none">{{ i18n "none" }}</a-select-option>
           <a-select-option value="tls">TLS</a-select-option>
         </a-select>
       </a-tooltip>
     </template>
-    <a-input
-      :style="{ width: '30%' }"
-      v-model.trim="row.dest"
-      placeholder='{{ i18n "host" }}'
-    ></a-input>
+    <a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
     <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
-      <a-input-number
-        :style="{ width: '15%' }"
-        v-model.number="row.port"
-        min="1"
-        max="65535"
-      ></a-input-number>
+      <a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
     </a-tooltip>
-    <a-input
-      :style="{ width: '30%', top: '0' }"
-      v-model.trim="row.remark"
-      placeholder='{{ i18n "remark" }}'
-    >
+    <a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
       <template slot="addonAfter">
-        <a-button
-          icon="minus"
-          size="small"
-          @click="inbound.stream.externalProxy.splice(index, 1)"
-        ></a-button>
+        <a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
       </template>
     </a-input>
   </a-input-group>
 </a-form>
-{{end}}
+{{end}}

+ 297 - 401
web/html/form/stream/stream_finalmask.html

@@ -1,446 +1,339 @@
 {{define "form/streamFinalMask"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
-  v-if="inbound.protocol == Protocols.HYSTERIA || ['kcp', 'xhttp', 'raw', 'tcp', 'httpupgrade', 'ws', 'grpc'].includes(inbound.stream.network)"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"
+  v-if="inbound.protocol == Protocols.HYSTERIA || ['kcp', 'xhttp', 'raw', 'tcp', 'httpupgrade', 'ws', 'grpc'].includes(inbound.stream.network)">
   <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
 
   <!-- TCP Masks – for raw/tcp/httpupgrade/ws/grpc/xhttp -->
   <template v-if="['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(inbound.stream.network)">
-  <a-form-item label="TCP Masks">
-    <a-button
-      icon="plus"
-      type="primary"
-      size="small"
-      @click="inbound.stream.addTcpMask('fragment')"
-    ></a-button>
-  </a-form-item>
-  <template v-if="inbound.stream.finalmask.tcp && inbound.stream.finalmask.tcp.length > 0">
-    <a-form
-      v-for="(mask, index) in inbound.stream.finalmask.tcp"
-      :key="index"
-      :colon="false"
-      :label-col="{ md: {span:8} }"
-      :wrapper-col="{ md: {span:14} }"
-    >
-      <a-divider :style="{ margin: '0' }">
-        TCP Mask [[ index + 1 ]]
-        <a-icon
-          type="delete"
-          @click="() => inbound.stream.delTcpMask(index)"
-          :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
-        ></a-icon>
-      </a-divider>
-      <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-        <a-select
-          v-model="mask.type"
-          @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); }"
-          :dropdown-class-name="themeSwitcher.currentTheme"
-        >
-          <a-select-option value="fragment">Fragment</a-select-option>
-          <a-select-option value="header-custom">Header Custom</a-select-option>
-          <a-select-option value="sudoku">Sudoku</a-select-option>
-        </a-select>
-      </a-form-item>
-
-      <!-- Fragment settings -->
-      <template v-if="mask.type === 'fragment'">
-        <a-form-item label="Packets">
-          <a-select
-            v-model="mask.settings.packets"
-            :dropdown-class-name="themeSwitcher.currentTheme"
-          >
-            <a-select-option value="tlshello">tlshello</a-select-option>
-            <a-select-option value="1-3">1-3</a-select-option>
-            <a-select-option value="1-5">1-5</a-select-option>
+    <a-form-item label="TCP Masks">
+      <a-button icon="plus" type="primary" size="small" @click="inbound.stream.addTcpMask('fragment')"></a-button>
+    </a-form-item>
+    <template v-if="inbound.stream.finalmask.tcp && inbound.stream.finalmask.tcp.length > 0">
+      <a-form v-for="(mask, index) in inbound.stream.finalmask.tcp" :key="index" :colon="false"
+        :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+        <a-divider :style="{ margin: '0' }">
+          TCP Mask [[ index + 1 ]]
+          <a-icon type="delete" @click="() => inbound.stream.delTcpMask(index)"
+            :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+        </a-divider>
+        <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+          <a-select v-model="mask.type" @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); }"
+            :dropdown-class-name="themeSwitcher.currentTheme">
+            <a-select-option value="fragment">Fragment</a-select-option>
+            <a-select-option value="header-custom">Header Custom</a-select-option>
+            <a-select-option value="sudoku">Sudoku</a-select-option>
           </a-select>
         </a-form-item>
-        <a-form-item label="Length">
-          <a-input v-model.trim="mask.settings.length" placeholder="e.g. 100-200" />
-        </a-form-item>
-        <a-form-item label="Delay">
-          <a-input v-model.trim="mask.settings.delay" placeholder="e.g. 10-20" />
-        </a-form-item>
-        <a-form-item label="Max Split">
-          <a-input v-model.trim="mask.settings.maxSplit" placeholder="e.g. 3-6" />
-        </a-form-item>
-      </template>
 
-      <!-- Sudoku settings (TCP) -->
-      <template v-if="mask.type === 'sudoku'">
-        <a-form-item label="Password">
-          <a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password" />
-        </a-form-item>
-        <a-form-item label="ASCII">
-          <a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" />
-        </a-form-item>
-        <a-form-item label="Custom Table">
-          <a-input v-model.trim="mask.settings.customTable" placeholder="Custom Table" />
-        </a-form-item>
-        <a-form-item label="Custom Tables">
-          <a-input v-model.trim="mask.settings.customTables" placeholder="Custom Tables" />
-        </a-form-item>
-        <a-form-item label="Padding Min">
-          <a-input-number v-model.number="mask.settings.paddingMin" :min="0" />
+        <!-- Fragment settings -->
+        <template v-if="mask.type === 'fragment'">
+          <a-form-item label="Packets">
+            <a-select v-model="mask.settings.packets" :dropdown-class-name="themeSwitcher.currentTheme">
+              <a-select-option value="tlshello">tlshello</a-select-option>
+              <a-select-option value="1-3">1-3</a-select-option>
+              <a-select-option value="1-5">1-5</a-select-option>
+            </a-select>
+          </a-form-item>
+          <a-form-item label="Length">
+            <a-input v-model.trim="mask.settings.length" placeholder="e.g. 100-200" />
+          </a-form-item>
+          <a-form-item label="Delay">
+            <a-input v-model.trim="mask.settings.delay" placeholder="e.g. 10-20" />
+          </a-form-item>
+          <a-form-item label="Max Split">
+            <a-input v-model.trim="mask.settings.maxSplit" placeholder="e.g. 3-6" />
+          </a-form-item>
+        </template>
+
+        <!-- Sudoku settings (TCP) -->
+        <template v-if="mask.type === 'sudoku'">
+          <a-form-item label="Password">
+            <a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password" />
+          </a-form-item>
+          <a-form-item label="ASCII">
+            <a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" />
+          </a-form-item>
+          <a-form-item label="Custom Table">
+            <a-input v-model.trim="mask.settings.customTable" placeholder="Custom Table" />
+          </a-form-item>
+          <a-form-item label="Custom Tables">
+            <a-input v-model.trim="mask.settings.customTables" placeholder="Custom Tables" />
+          </a-form-item>
+          <a-form-item label="Padding Min">
+            <a-input-number v-model.number="mask.settings.paddingMin" :min="0" />
+          </a-form-item>
+          <a-form-item label="Padding Max">
+            <a-input-number v-model.number="mask.settings.paddingMax" :min="0" />
+          </a-form-item>
+        </template>
+
+        <!-- Header Custom (TCP) – clients/servers/errors are 2D arrays of groups -->
+        <template v-if="mask.type === 'header-custom'">
+          <!-- Clients -->
+          <a-form-item label="Clients">
+            <a-button icon="plus" type="primary" size="small"
+              @click="mask.settings.clients.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"></a-button>
+          </a-form-item>
+          <template v-for="(group, gi) in mask.settings.clients" :key="'cg'+gi">
+            <a-divider :style="{ margin: '0' }">
+              Clients Group [[ gi + 1 ]]
+              <a-icon type="delete" @click="mask.settings.clients.splice(gi, 1)"
+                :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"></a-icon>
+            </a-divider>
+            <template v-for="(item, ii) in group" :key="'ci'+ii">
+              <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+                <a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
+                  @change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
+                  <a-select-option value="array">Array</a-select-option>
+                  <a-select-option value="str">String</a-select-option>
+                  <a-select-option value="hex">Hex</a-select-option>
+                  <a-select-option value="base64">Base64</a-select-option>
+                </a-select>
+              </a-form-item>
+              <a-form-item label="Delay (ms)">
+                <a-input-number v-model.number="item.delay" :min="0" />
+              </a-form-item>
+              <template v-if="item.type === 'array'">
+                <a-form-item label="Rand">
+                  <a-input-number v-model.number="item.rand" :min="0" />
+                </a-form-item>
+                <a-form-item label="Rand Range">
+                  <a-input v-model.trim="item.randRange" placeholder="0-255" />
+                </a-form-item>
+              </template>
+              <a-form-item v-else label="Packet">
+                <a-input-group compact v-if="item.type === 'base64'">
+                  <a-input v-model.trim="item.packet" placeholder="binary data"
+                    :style="{ width: 'calc(100% - 32px)' }" />
+                  <a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
+                </a-input-group>
+                <a-input v-else v-model.trim="item.packet" placeholder="binary data" />
+              </a-form-item>
+            </template>
+          </template>
+
+          <!-- Servers -->
+          <a-form-item label="Servers">
+            <a-button icon="plus" type="primary" size="small"
+              @click="mask.settings.servers.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"></a-button>
+          </a-form-item>
+          <template v-for="(group, gi) in mask.settings.servers" :key="'sg'+gi">
+            <a-divider :style="{ margin: '0' }">
+              Servers Group [[ gi + 1 ]]
+              <a-icon type="delete" @click="mask.settings.servers.splice(gi, 1)"
+                :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"></a-icon>
+            </a-divider>
+            <template v-for="(item, ii) in group" :key="'si'+ii">
+              <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+                <a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
+                  @change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
+                  <a-select-option value="array">Array</a-select-option>
+                  <a-select-option value="str">String</a-select-option>
+                  <a-select-option value="hex">Hex</a-select-option>
+                  <a-select-option value="base64">Base64</a-select-option>
+                </a-select>
+              </a-form-item>
+              <a-form-item label="Delay (ms)">
+                <a-input-number v-model.number="item.delay" :min="0" />
+              </a-form-item>
+              <template v-if="item.type === 'array'">
+                <a-form-item label="Rand">
+                  <a-input-number v-model.number="item.rand" :min="0" />
+                </a-form-item>
+                <a-form-item label="Rand Range">
+                  <a-input v-model.trim="item.randRange" placeholder="0-255" />
+                </a-form-item>
+              </template>
+              <a-form-item v-else label="Packet">
+                <a-input-group compact v-if="item.type === 'base64'">
+                  <a-input v-model.trim="item.packet" placeholder="binary data"
+                    :style="{ width: 'calc(100% - 32px)' }" />
+                  <a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
+                </a-input-group>
+                <a-input v-else v-model.trim="item.packet" placeholder="binary data" />
+              </a-form-item>
+            </template>
+          </template>
+        </template>
+
+      </a-form>
+    </template>
+  </template>
+
+  <template v-if="inbound.protocol == Protocols.HYSTERIA || inbound.stream.network == 'kcp'">
+    <a-form-item label="UDP Masks">
+      <a-button icon="plus" type="primary" size="small"
+        @click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')"></a-button>
+    </a-form-item>
+    <template v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0">
+      <a-form v-for="(mask, index) in inbound.stream.finalmask.udp" :key="index" :colon="false"
+        :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+        <a-divider :style="{ margin: '0' }">
+          UDP Mask [[ index + 1 ]]
+          <a-icon type="delete" @click="() => inbound.stream.delUdpMask(index)"
+            :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+        </a-divider>
+        <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+          <a-select v-model="mask.type"
+            @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
+            :dropdown-class-name="themeSwitcher.currentTheme">
+            <template v-if="inbound.protocol === Protocols.HYSTERIA">
+              <a-select-option value="salamander">Salamander (Hysteria2)</a-select-option>
+            </template>
+            <template v-else>
+              <a-select-option value="mkcp-aes128gcm">mKCP AES-128-GCM</a-select-option>
+              <a-select-option value="header-dns">Header DNS</a-select-option>
+              <a-select-option value="header-dtls">Header DTLS 1.2</a-select-option>
+              <a-select-option value="header-srtp">Header SRTP</a-select-option>
+              <a-select-option value="header-utp">Header uTP</a-select-option>
+              <a-select-option value="header-wechat">Header WeChat Video</a-select-option>
+              <a-select-option value="header-wireguard">Header WireGuard</a-select-option>
+              <a-select-option value="mkcp-original">mKCP Original</a-select-option>
+              <a-select-option value="xdns">xDNS</a-select-option>
+              <a-select-option value="xicmp">xICMP</a-select-option>
+              <a-select-option value="header-custom">Header Custom</a-select-option>
+              <a-select-option value="noise">Noise</a-select-option>
+            </template>
+          </a-select>
         </a-form-item>
-        <a-form-item label="Padding Max">
-          <a-input-number v-model.number="mask.settings.paddingMax" :min="0" />
+        <a-form-item label="Password" v-if="['mkcp-aes128gcm', 'salamander'].includes(mask.type)">
+          <a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password"></a-input>
         </a-form-item>
-      </template>
-
-      <!-- Header Custom (TCP) – clients/servers/errors are 2D arrays of groups -->
-      <template v-if="mask.type === 'header-custom'">
-        <!-- Clients -->
-        <a-form-item label="Clients">
-          <a-button
-            icon="plus"
-            type="primary"
-            size="small"
-            @click="mask.settings.clients.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"
-          ></a-button>
+        <a-form-item label="Domain" v-if="mask.type === 'header-dns'">
+          <a-input v-model.trim="mask.settings.domain" placeholder="e.g., www.example.com"></a-input>
         </a-form-item>
-        <template v-for="(group, gi) in mask.settings.clients" :key="'cg'+gi">
-          <a-divider :style="{ margin: '0' }">
-            Clients Group [[ gi + 1 ]]
-            <a-icon
-              type="delete"
-              @click="mask.settings.clients.splice(gi, 1)"
-              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"
-            ></a-icon>
-          </a-divider>
-          <template v-for="(item, ii) in group" :key="'ci'+ii">
+        <template v-if="mask.type === 'xdns'">
+          <a-form-item label="Domains">
+            <a-select mode="tags" v-model="mask.settings.domains" :style="{ width: '100%' }" :token-separators="[',']"
+              placeholder="e.g., www.example.com"></a-select>
+          </a-form-item>
+        </template>
+        <template v-if="mask.type === 'noise'">
+          <a-form-item label="Reset">
+            <a-input-number v-model.number="mask.settings.reset" :min="0" />
+          </a-form-item>
+          <a-form-item label="Noise">
+            <a-button icon="plus" type="primary" size="small"
+              @click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20'})"></a-button>
+          </a-form-item>
+          <template v-for="(n, index) in mask.settings.noise" :key="index">
+            <a-divider :style="{ margin: '0' }">
+              Noise [[ index + 1 ]]
+              <a-icon type="delete" @click="() => mask.settings.noise.splice(index, 1)"
+                :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+            </a-divider>
             <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-              <a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
-                @change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
+              <a-select v-model="n.type" :dropdown-class-name="themeSwitcher.currentTheme"
+                @change="t => { if(t === 'base64') n.packet = RandomUtil.randomBase64(); else if(t === 'array') n.packet = []; else n.packet = ''; }">
                 <a-select-option value="array">Array</a-select-option>
                 <a-select-option value="str">String</a-select-option>
                 <a-select-option value="hex">Hex</a-select-option>
                 <a-select-option value="base64">Base64</a-select-option>
               </a-select>
             </a-form-item>
-            <a-form-item label="Delay (ms)">
-              <a-input-number v-model.number="item.delay" :min="0" />
-            </a-form-item>
-            <template v-if="item.type === 'array'">
+            <template v-if="n.type === 'array'">
               <a-form-item label="Rand">
-                <a-input-number v-model.number="item.rand" :min="0" />
+                <a-input v-model.trim="n.rand" placeholder="0 or 1-8192" />
               </a-form-item>
               <a-form-item label="Rand Range">
-                <a-input v-model.trim="item.randRange" placeholder="0-255" />
+                <a-input v-model.trim="n.randRange" placeholder="0-255" />
               </a-form-item>
             </template>
             <a-form-item v-else label="Packet">
-              <a-input-group compact v-if="item.type === 'base64'">
-                <a-input v-model.trim="item.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
-                <a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
+              <a-input-group compact v-if="n.type === 'base64'">
+                <a-input v-model.trim="n.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
+                <a-button icon="reload" @click="n.packet = RandomUtil.randomBase64()" />
               </a-input-group>
-              <a-input v-else v-model.trim="item.packet" placeholder="binary data" />
+              <a-input v-else v-model.trim="n.packet" placeholder="binary data" />
+            </a-form-item>
+            <a-form-item label="Delay">
+              <a-input v-model.trim="n.delay" placeholder="10-20" />
             </a-form-item>
           </template>
         </template>
-
-        <!-- Servers -->
-        <a-form-item label="Servers">
-          <a-button
-            icon="plus"
-            type="primary"
-            size="small"
-            @click="mask.settings.servers.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"
-          ></a-button>
-        </a-form-item>
-        <template v-for="(group, gi) in mask.settings.servers" :key="'sg'+gi">
-          <a-divider :style="{ margin: '0' }">
-            Servers Group [[ gi + 1 ]]
-            <a-icon
-              type="delete"
-              @click="mask.settings.servers.splice(gi, 1)"
-              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"
-            ></a-icon>
-          </a-divider>
-          <template v-for="(item, ii) in group" :key="'si'+ii">
+        <template v-if="mask.type === 'header-custom'">
+          <a-form-item label="Client">
+            <a-button icon="plus" type="primary" size="small"
+              @click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"></a-button>
+          </a-form-item>
+          <template v-for="(c, index) in mask.settings.client" :key="index">
+            <a-divider :style="{ margin: '0' }">
+              Client [[ index + 1 ]]
+              <a-icon type="delete" @click="mask.settings.client.splice(index, 1)"
+                :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+            </a-divider>
             <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-              <a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
-                @change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
+              <a-select v-model="c.type" :dropdown-class-name="themeSwitcher.currentTheme"
+                @change="t => { if(t === 'base64') c.packet = RandomUtil.randomBase64(); else if(t === 'array') c.packet = []; else c.packet = ''; }">
                 <a-select-option value="array">Array</a-select-option>
                 <a-select-option value="str">String</a-select-option>
                 <a-select-option value="hex">Hex</a-select-option>
                 <a-select-option value="base64">Base64</a-select-option>
               </a-select>
             </a-form-item>
-            <a-form-item label="Delay (ms)">
-              <a-input-number v-model.number="item.delay" :min="0" />
-            </a-form-item>
-            <template v-if="item.type === 'array'">
+            <template v-if="c.type === 'array'">
               <a-form-item label="Rand">
-                <a-input-number v-model.number="item.rand" :min="0" />
+                <a-input-number v-model.number="c.rand" />
               </a-form-item>
               <a-form-item label="Rand Range">
-                <a-input v-model.trim="item.randRange" placeholder="0-255" />
+                <a-input v-model.trim="c.randRange" placeholder="0-255" />
               </a-form-item>
             </template>
             <a-form-item v-else label="Packet">
-              <a-input-group compact v-if="item.type === 'base64'">
-                <a-input v-model.trim="item.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
-                <a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
+              <a-input-group compact v-if="c.type === 'base64'">
+                <a-input v-model.trim="c.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
+                <a-button icon="reload" @click="c.packet = RandomUtil.randomBase64()" />
               </a-input-group>
-              <a-input v-else v-model.trim="item.packet" placeholder="binary data" />
-            </a-form-item>
-          </template>
-        </template>
-      </template>
-
-    </a-form>
-  </template>
-  </template>
-
-  <template v-if="inbound.protocol == Protocols.HYSTERIA || inbound.stream.network == 'kcp'">
-  <a-form-item label="UDP Masks">
-    <a-button
-      icon="plus"
-      type="primary"
-      size="small"
-      @click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')"
-    ></a-button>
-  </a-form-item>
-  <template
-    v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0"
-  >
-    <a-form
-      v-for="(mask, index) in inbound.stream.finalmask.udp"
-      :key="index"
-      :colon="false"
-      :label-col="{ md: {span:8} }"
-      :wrapper-col="{ md: {span:14} }"
-    >
-      <a-divider :style="{ margin: '0' }">
-        UDP Mask [[ index + 1 ]]
-        <a-icon
-          type="delete"
-          @click="() => inbound.stream.delUdpMask(index)"
-          :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
-        ></a-icon>
-      </a-divider>
-      <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-        <a-select
-          v-model="mask.type"
-          @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
-          :dropdown-class-name="themeSwitcher.currentTheme"
-        >
-          <template v-if="inbound.protocol === Protocols.HYSTERIA">
-            <a-select-option value="salamander"
-              >Salamander (Hysteria2)</a-select-option
-            >
-          </template>
-          <template v-else>
-            <a-select-option value="mkcp-aes128gcm"
-              >mKCP AES-128-GCM</a-select-option
-            >
-            <a-select-option value="header-dns">Header DNS</a-select-option>
-            <a-select-option value="header-dtls"
-              >Header DTLS 1.2</a-select-option
-            >
-            <a-select-option value="header-srtp">Header SRTP</a-select-option>
-            <a-select-option value="header-utp">Header uTP</a-select-option>
-            <a-select-option value="header-wechat"
-              >Header WeChat Video</a-select-option
-            >
-            <a-select-option value="header-wireguard"
-              >Header WireGuard</a-select-option
-            >
-            <a-select-option value="mkcp-original"
-              >mKCP Original</a-select-option
-            >
-            <a-select-option value="xdns">xDNS</a-select-option>
-            <a-select-option value="xicmp">xICMP</a-select-option>
-            <a-select-option value="header-custom">Header Custom</a-select-option>
-            <a-select-option value="noise">Noise</a-select-option>
-          </template>
-        </a-select>
-      </a-form-item>
-      <a-form-item
-        label="Password"
-        v-if="['mkcp-aes128gcm', 'salamander'].includes(mask.type)"
-      >
-        <a-input
-          v-model.trim="mask.settings.password"
-          placeholder="Obfuscation password"
-        ></a-input>
-      </a-form-item>
-      <a-form-item label="Domain" v-if="mask.type === 'header-dns'">
-        <a-input
-          v-model.trim="mask.settings.domain"
-          placeholder="e.g., www.example.com"
-        ></a-input>
-      </a-form-item>
-      <template v-if="mask.type === 'xdns'">
-        <a-form-item label="Domains">
-          <a-select
-            mode="tags"
-            v-model="mask.settings.domains"
-            :style="{ width: '100%' }"
-            :token-separators="[',']"
-            placeholder="e.g., www.example.com"
-          ></a-select>
-        </a-form-item>
-      </template>
-      <template v-if="mask.type === 'noise'">
-        <a-form-item label="Reset">
-          <a-input-number v-model.number="mask.settings.reset" :min="0" />
-        </a-form-item>
-        <a-form-item label="Noise">
-          <a-button
-            icon="plus"
-            type="primary"
-            size="small"
-            @click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20'})"
-          ></a-button>
-        </a-form-item>
-        <template v-for="(n, index) in mask.settings.noise" :key="index">
-          <a-divider :style="{ margin: '0' }">
-            Noise [[ index + 1 ]]
-            <a-icon
-              type="delete"
-              @click="() => mask.settings.noise.splice(index, 1)"
-              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
-            ></a-icon>
-          </a-divider>
-          <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-            <a-select
-              v-model="n.type"
-              :dropdown-class-name="themeSwitcher.currentTheme"
-              @change="t => { if(t === 'base64') n.packet = RandomUtil.randomBase64(); else if(t === 'array') n.packet = []; else n.packet = ''; }"
-            >
-              <a-select-option value="array">Array</a-select-option>
-              <a-select-option value="str">String</a-select-option>
-              <a-select-option value="hex">Hex</a-select-option>
-              <a-select-option value="base64">Base64</a-select-option>
-            </a-select>
-          </a-form-item>
-          <template v-if="n.type === 'array'">
-            <a-form-item label="Rand">
-              <a-input v-model.trim="n.rand" placeholder="0 or 1-8192" />
-            </a-form-item>
-            <a-form-item label="Rand Range">
-              <a-input v-model.trim="n.randRange" placeholder="0-255" />
+              <a-input v-else v-model.trim="c.packet" placeholder="binary data" />
             </a-form-item>
           </template>
-          <a-form-item v-else label="Packet">
-            <a-input-group compact v-if="n.type === 'base64'">
-              <a-input v-model.trim="n.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
-              <a-button icon="reload" @click="n.packet = RandomUtil.randomBase64()" />
-            </a-input-group>
-            <a-input v-else v-model.trim="n.packet" placeholder="binary data" />
+          <a-divider :style="{ margin: '0' }"></a-divider>
+          <a-form-item label="Server">
+            <a-button icon="plus" type="primary" size="small"
+              @click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"></a-button>
           </a-form-item>
-          <a-form-item label="Delay">
-            <a-input v-model.trim="n.delay" placeholder="10-20" />
-          </a-form-item>
-        </template>
-      </template>
-      <template v-if="mask.type === 'header-custom'">
-        <a-form-item label="Client">
-          <a-button
-            icon="plus"
-            type="primary"
-            size="small"
-            @click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
-          ></a-button>
-        </a-form-item>
-        <template v-for="(c, index) in mask.settings.client" :key="index">
-          <a-divider :style="{ margin: '0' }">
-            Client [[ index + 1 ]]
-            <a-icon
-              type="delete"
-              @click="mask.settings.client.splice(index, 1)"
-              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
-            ></a-icon>
-          </a-divider>
-          <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-            <a-select
-              v-model="c.type"
-              :dropdown-class-name="themeSwitcher.currentTheme"
-              @change="t => { if(t === 'base64') c.packet = RandomUtil.randomBase64(); else if(t === 'array') c.packet = []; else c.packet = ''; }"
-            >
-              <a-select-option value="array">Array</a-select-option>
-              <a-select-option value="str">String</a-select-option>
-              <a-select-option value="hex">Hex</a-select-option>
-              <a-select-option value="base64">Base64</a-select-option>
-            </a-select>
-          </a-form-item>
-          <template v-if="c.type === 'array'">
-            <a-form-item label="Rand">
-              <a-input-number v-model.number="c.rand" />
+          <template v-for="(s, index) in mask.settings.server" :key="index">
+            <a-divider :style="{ margin: '0' }">
+              Server [[ index + 1 ]]
+              <a-icon type="delete" @click="mask.settings.server.splice(index, 1)"
+                :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
+            </a-divider>
+            <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
+              <a-select v-model="s.type" :dropdown-class-name="themeSwitcher.currentTheme"
+                @change="t => { if(t === 'base64') s.packet = RandomUtil.randomBase64(); else if(t === 'array') s.packet = []; else s.packet = ''; }">
+                <a-select-option value="array">Array</a-select-option>
+                <a-select-option value="str">String</a-select-option>
+                <a-select-option value="hex">Hex</a-select-option>
+                <a-select-option value="base64">Base64</a-select-option>
+              </a-select>
             </a-form-item>
-            <a-form-item label="Rand Range">
-              <a-input v-model.trim="c.randRange" placeholder="0-255" />
+            <template v-if="s.type === 'array'">
+              <a-form-item label="Rand">
+                <a-input-number v-model.number="s.rand" />
+              </a-form-item>
+              <a-form-item label="Rand Range">
+                <a-input v-model.trim="s.randRange" placeholder="0-255" />
+              </a-form-item>
+            </template>
+            <a-form-item v-else label="Packet">
+              <a-input-group compact v-if="s.type === 'base64'">
+                <a-input v-model.trim="s.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
+                <a-button icon="reload" @click="s.packet = RandomUtil.randomBase64()" />
+              </a-input-group>
+              <a-input v-else v-model.trim="s.packet" placeholder="binary data" />
             </a-form-item>
           </template>
-          <a-form-item v-else label="Packet">
-            <a-input-group compact v-if="c.type === 'base64'">
-              <a-input v-model.trim="c.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
-              <a-button icon="reload" @click="c.packet = RandomUtil.randomBase64()" />
-            </a-input-group>
-            <a-input v-else v-model.trim="c.packet" placeholder="binary data" />
-          </a-form-item>
         </template>
-        <a-divider :style="{ margin: '0' }"></a-divider>
-        <a-form-item label="Server">
-          <a-button
-            icon="plus"
-            type="primary"
-            size="small"
-            @click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
-          ></a-button>
-        </a-form-item>
-        <template v-for="(s, index) in mask.settings.server" :key="index">
-          <a-divider :style="{ margin: '0' }">
-            Server [[ index + 1 ]]
-            <a-icon
-              type="delete"
-              @click="mask.settings.server.splice(index, 1)"
-              :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
-            ></a-icon>
-          </a-divider>
-          <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
-            <a-select
-              v-model="s.type"
-              :dropdown-class-name="themeSwitcher.currentTheme"
-              @change="t => { if(t === 'base64') s.packet = RandomUtil.randomBase64(); else if(t === 'array') s.packet = []; else s.packet = ''; }"
-            >
-              <a-select-option value="array">Array</a-select-option>
-              <a-select-option value="str">String</a-select-option>
-              <a-select-option value="hex">Hex</a-select-option>
-              <a-select-option value="base64">Base64</a-select-option>
-            </a-select>
+        <template v-if="mask.type === 'xicmp'">
+          <a-form-item label="IP">
+            <a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
           </a-form-item>
-          <template v-if="s.type === 'array'">
-            <a-form-item label="Rand">
-              <a-input-number v-model.number="s.rand" />
-            </a-form-item>
-            <a-form-item label="Rand Range">
-              <a-input v-model.trim="s.randRange" placeholder="0-255" />
-            </a-form-item>
-          </template>
-          <a-form-item v-else label="Packet">
-            <a-input-group compact v-if="s.type === 'base64'">
-              <a-input v-model.trim="s.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
-              <a-button icon="reload" @click="s.packet = RandomUtil.randomBase64()" />
-            </a-input-group>
-            <a-input v-else v-model.trim="s.packet" placeholder="binary data" />
+          <a-form-item label="ID">
+            <a-input-number v-model.number="mask.settings.id" :min="0" />
           </a-form-item>
         </template>
-      </template>
-      <template v-if="mask.type === 'xicmp'">
-        <a-form-item label="IP">
-          <a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
-        </a-form-item>
-        <a-form-item label="ID">
-          <a-input-number v-model.number="mask.settings.id" :min="0" />
-        </a-form-item>
-      </template>
-    </a-form>
-  </template>
+      </a-form>
+    </template>
   </template>
 
   <!-- quicParams – only for xhttp H3 and hysteria -->
@@ -450,10 +343,8 @@
     </a-form-item>
     <template v-if="inbound.stream.finalmask.enableQuicParams">
       <a-form-item label="Congestion">
-        <a-select
-          v-model="inbound.stream.finalmask.quicParams.congestion"
-          :dropdown-class-name="themeSwitcher.currentTheme"
-        >
+        <a-select v-model="inbound.stream.finalmask.quicParams.congestion"
+          :dropdown-class-name="themeSwitcher.currentTheme">
           <a-select-option value="reno">Reno</a-select-option>
           <a-select-option value="bbr">BBR</a-select-option>
           <a-select-option value="brutal">Brutal</a-select-option>
@@ -492,21 +383,26 @@
         <a-switch v-model="inbound.stream.finalmask.quicParams.disablePathMTUDiscovery"></a-switch>
       </a-form-item>
       <a-form-item label="Max Incoming Streams">
-        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIncomingStreams" :min="0" placeholder="0 = default" />
+        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIncomingStreams" :min="0"
+          placeholder="0 = default" />
       </a-form-item>
       <a-form-item label="Init Stream Window">
-        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="0" placeholder="0 = default" />
+        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="0"
+          placeholder="0 = default" />
       </a-form-item>
       <a-form-item label="Max Stream Window">
-        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="0" placeholder="0 = default" />
+        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="0"
+          placeholder="0 = default" />
       </a-form-item>
       <a-form-item label="Init Conn Window">
-        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="0" placeholder="0 = default" />
+        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="0"
+          placeholder="0 = default" />
       </a-form-item>
       <a-form-item label="Max Conn Window">
-        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="0" placeholder="0 = default" />
+        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="0"
+          placeholder="0 = default" />
       </a-form-item>
     </template>
   </template>
 </a-form>
-{{end}}
+{{end}}

+ 2 - 6
web/html/form/stream/stream_grpc.html

@@ -1,9 +1,5 @@
 {{define "form/streamGRPC"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="Service Name">
     <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
   </a-form-item>
@@ -14,4 +10,4 @@
     <a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 12 - 37
web/html/form/stream/stream_httpupgrade.html

@@ -1,13 +1,7 @@
 {{define "form/streamHTTPUpgrade"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="Proxy Protocol">
-    <a-switch
-      v-model="inbound.stream.httpupgrade.acceptProxyProtocol"
-    ></a-switch>
+    <a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
   </a-form-item>
   <a-form-item label='{{ i18n "host" }}'>
     <a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
@@ -16,39 +10,20 @@
     <a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button
-      icon="plus"
-      size="small"
-      @click="inbound.stream.httpupgrade.addHeader('', '')"
-    ></a-button>
+    <a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group
-      compact
-      v-for="(header, index) in inbound.stream.httpupgrade.headers"
-    >
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.name"
-        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
-      >
-        <template slot="addonBefore" :style="{ margin: '0' }"
-          >[[ index+1 ]]</template
-        >
+    <a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
+      <a-input :style="{ width: '50%' }" v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
+        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
       </a-input>
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.value"
-        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
-      >
-        <a-button
-          icon="minus"
-          slot="addonAfter"
-          size="small"
-          @click="inbound.stream.httpupgrade.removeHeader(index)"
-        ></a-button>
+      <a-input :style="{ width: '50%' }" v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+        <a-button icon="minus" slot="addonAfter" size="small"
+          @click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
       </a-input>
     </a-input-group>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 19 - 61
web/html/form/stream/stream_hysteria.html

@@ -1,14 +1,7 @@
 {{define "form/streamHysteria"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="UDP Idle Timeout">
-    <a-input-number
-      v-model.number="inbound.stream.hysteria.udpIdleTimeout"
-      :min="0"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.hysteria.udpIdleTimeout" :min="0"></a-input-number>
   </a-form-item>
   <a-form-item label="Masquerade">
     <a-switch v-model="inbound.stream.hysteria.masqueradeSwitch"></a-switch>
@@ -16,85 +9,50 @@
   <template v-if="inbound.stream.hysteria.masqueradeSwitch">
     <a-divider :style="{ margin: '5px 0 0' }">Masquerade</a-divider>
     <a-form-item label="Type">
-      <a-select
-        v-model="inbound.stream.hysteria.masquerade.type"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
+      <a-select v-model="inbound.stream.hysteria.masquerade.type" :dropdown-class-name="themeSwitcher.currentTheme">
         <a-select-option value="file">File</a-select-option>
         <a-select-option value="proxy">Proxy</a-select-option>
         <a-select-option value="string">String</a-select-option>
       </a-select>
     </a-form-item>
-    <a-form-item
-      label="Dir"
-      v-if="inbound.stream.hysteria.masquerade.type === 'file'"
-    >
+    <a-form-item label="Dir" v-if="inbound.stream.hysteria.masquerade.type === 'file'">
       <a-input v-model.trim="inbound.stream.hysteria.masquerade.dir"></a-input>
     </a-form-item>
     <template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
       <a-form-item label="URL">
-        <a-input
-          v-model.trim="inbound.stream.hysteria.masquerade.url"
-        ></a-input>
+        <a-input v-model.trim="inbound.stream.hysteria.masquerade.url"></a-input>
       </a-form-item>
       <a-form-item label="Rewrite Host">
-        <a-switch
-          v-model="inbound.stream.hysteria.masquerade.rewriteHost"
-        ></a-switch>
+        <a-switch v-model="inbound.stream.hysteria.masquerade.rewriteHost"></a-switch>
       </a-form-item>
       <a-form-item label="Insecure">
-        <a-switch
-          v-model="inbound.stream.hysteria.masquerade.insecure"
-        ></a-switch>
+        <a-switch v-model="inbound.stream.hysteria.masquerade.insecure"></a-switch>
       </a-form-item>
     </template>
     <template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
       <a-form-item label="Content">
-        <a-input
-          v-model.trim="inbound.stream.hysteria.masquerade.content"
-        ></a-input>
+        <a-input v-model.trim="inbound.stream.hysteria.masquerade.content"></a-input>
       </a-form-item>
       <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-        <a-button
-          size="small"
-          @click="inbound.stream.hysteria.masquerade.addHeader('', '')"
-          >+</a-button
-        >
+        <a-button size="small" @click="inbound.stream.hysteria.masquerade.addHeader('', '')">+</a-button>
       </a-form-item>
       <a-form-item :wrapper-col="{span:24}">
-        <a-input-group
-          compact
-          v-for="(header, index) in inbound.stream.hysteria.masquerade.headers"
-        >
-          <a-input
-            style="width: 50%"
-            v-model.trim="header.name"
-            placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
-          >
-            <template slot="addonBefore" style="margin: 0"
-              >[[ index+1 ]]</template
-            >
+        <a-input-group compact v-for="(header, index) in inbound.stream.hysteria.masquerade.headers">
+          <a-input style="width: 50%" v-model.trim="header.name"
+            placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
+            <template slot="addonBefore" style="margin: 0">[[ index+1 ]]</template>
           </a-input>
-          <a-input
-            style="width: 50%"
-            v-model.trim="header.value"
-            placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
-          >
-            <a-button
-              slot="addonAfter"
-              size="small"
-              @click="inbound.stream.hysteria.masquerade.removeHeader(index)"
-              >-</a-button
-            >
+          <a-input style="width: 50%" v-model.trim="header.value"
+            placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+            <a-button slot="addonAfter" size="small"
+              @click="inbound.stream.hysteria.masquerade.removeHeader(index)">-</a-button>
           </a-input>
         </a-input-group>
       </a-form-item>
       <a-form-item label="Status Code">
-        <a-input-number
-          v-model.number="inbound.stream.hysteria.masquerade.statusCode"
-        ></a-input-number>
+        <a-input-number v-model.number="inbound.stream.hysteria.masquerade.statusCode"></a-input-number>
       </a-form-item>
     </template>
   </template>
 </a-form>
-{{end}}
+{{end}}

+ 8 - 32
web/html/form/stream/stream_kcp.html

@@ -1,46 +1,22 @@
 {{define "form/streamKCP"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="MTU">
-    <a-input-number
-      v-model.number="inbound.stream.kcp.mtu"
-      :min="576"
-      :max="1460"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576" :max="1460"></a-input-number>
   </a-form-item>
   <a-form-item label="TTI (ms)">
-    <a-input-number
-      v-model.number="inbound.stream.kcp.tti"
-      :min="10"
-      :max="100"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.kcp.tti" :min="10" :max="100"></a-input-number>
   </a-form-item>
   <a-form-item label="Uplink (MB/s)">
-    <a-input-number
-      v-model.number="inbound.stream.kcp.upCap"
-      :min="0"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.kcp.upCap" :min="0"></a-input-number>
   </a-form-item>
   <a-form-item label="Downlink (MB/s)">
-    <a-input-number
-      v-model.number="inbound.stream.kcp.downCap"
-      :min="0"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.kcp.downCap" :min="0"></a-input-number>
   </a-form-item>
   <a-form-item label="CWND Multiplier">
-    <a-input-number
-      v-model.number="inbound.stream.kcp.cwndMultiplier"
-      :min="1"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.kcp.cwndMultiplier" :min="1"></a-input-number>
   </a-form-item>
   <a-form-item label="Max Sending Window">
-    <a-input-number
-      v-model.number="inbound.stream.kcp.maxSendingWindow"
-      :min="0"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.kcp.maxSendingWindow" :min="0"></a-input-number>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 14 - 22
web/html/form/stream/stream_settings.html

@@ -1,18 +1,10 @@
 {{define "form/streamSettings"}}
 <!-- select stream network -->
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
-  v-if="inbound.protocol != Protocols.HYSTERIA"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"
+  v-if="inbound.protocol != Protocols.HYSTERIA">
   <a-form-item label='{{ i18n "transmission" }}'>
-    <a-select
-      v-model="inbound.stream.network"
-      :style="{ width: '75%' }"
-      @change="streamNetworkChange"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+    <a-select v-model="inbound.stream.network" :style="{ width: '75%' }" @change="streamNetworkChange"
+      :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value="tcp">TCP (RAW)</a-select-option>
       <a-select-option value="kcp">mKCP</a-select-option>
       <a-select-option value="ws">WebSocket</a-select-option>
@@ -25,42 +17,42 @@
 
 <!-- tcp -->
 <template v-if="inbound.stream.network === 'tcp'">
-  {{template "form/streamTCP"}}
+  {{template "form/streamTCP" .}}
 </template>
 
 <!-- kcp -->
 <template v-if="inbound.stream.network === 'kcp'">
-  {{template "form/streamKCP"}}
+  {{template "form/streamKCP" .}}
 </template>
 
 <!-- ws -->
 <template v-if="inbound.stream.network === 'ws'">
-  {{template "form/streamWS"}}
+  {{template "form/streamWS" .}}
 </template>
 
 <!-- grpc -->
 <template v-if="inbound.stream.network === 'grpc'">
-  {{template "form/streamGRPC"}}
+  {{template "form/streamGRPC" .}}
 </template>
 
 <!-- hysteria -->
 <template v-if="inbound.stream.network === 'hysteria'">
-  {{template "form/streamHysteria"}}
+  {{template "form/streamHysteria" .}}
 </template>
 
 <!-- httpupgrade -->
 <template v-if="inbound.stream.network === 'httpupgrade'">
-  {{template "form/streamHTTPUpgrade"}}
+  {{template "form/streamHTTPUpgrade" .}}
 </template>
 
 <!-- xhttp -->
 <template v-if="inbound.stream.network === 'xhttp'">
-  {{template "form/streamXHTTP"}}
+  {{template "form/streamXHTTP" .}}
 </template>
 
 <!-- sockopt -->
-<template> {{template "form/streamSockopt"}} </template>
+<template> {{template "form/streamSockopt" .}} </template>
 
 <!-- finalmask -->
-<template> {{template "form/streamFinalMask"}} </template>
-{{end}}
+<template> {{template "form/streamFinalMask" .}} </template>
+{{end}}

+ 19 - 60
web/html/form/stream/stream_sockopt.html

@@ -1,49 +1,27 @@
 {{define "form/streamSockopt"}}
 <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="Sockopt">
     <a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
   </a-form-item>
   <template v-if="inbound.stream.sockoptSwitch">
     <a-form-item label="Route Mark">
-      <a-input-number
-        v-model.number="inbound.stream.sockopt.mark"
-        :min="0"
-      ></a-input-number>
+      <a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item label="TCP Keep Alive Interval">
-      <a-input-number
-        v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval"
-        :min="0"
-      ></a-input-number>
+      <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item label="TCP Keep Alive Idle">
-      <a-input-number
-        v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle"
-        :min="0"
-      ></a-input-number>
+      <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item label="TCP Max Seg">
-      <a-input-number
-        v-model.number="inbound.stream.sockopt.tcpMaxSeg"
-        :min="0"
-      ></a-input-number>
+      <a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item label="TCP User Timeout">
-      <a-input-number
-        v-model.number="inbound.stream.sockopt.tcpUserTimeout"
-        :min="0"
-      ></a-input-number>
+      <a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item label="TCP Window Clamp">
-      <a-input-number
-        v-model.number="inbound.stream.sockopt.tcpWindowClamp"
-        :min="0"
-      ></a-input-number>
+      <a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item label="Proxy Protocol">
       <a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
@@ -61,33 +39,20 @@
       <a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
     </a-form-item>
     <a-form-item label="Domain Strategy">
-      <a-select
-        v-model="inbound.stream.sockopt.domainStrategy"
-        :style="{ width: '50%' }"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
-        <a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key"
-          >[[ key ]]</a-select-option
-        >
+      <a-select v-model="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key">[[ key ]]</a-select-option>
       </a-select>
     </a-form-item>
     <a-form-item label="TCP Congestion">
-      <a-select
-        v-model="inbound.stream.sockopt.tcpcongestion"
-        :style="{ width: '50%' }"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
-        <a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key"
-          >[[ key ]]</a-select-option
-        >
+      <a-select v-model="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key">[[ key ]]</a-select-option>
       </a-select>
     </a-form-item>
     <a-form-item label="TProxy">
-      <a-select
-        v-model="inbound.stream.sockopt.tproxy"
-        :style="{ width: '50%' }"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
+      <a-select v-model="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme">
         <a-select-option value="off">Off</a-select-option>
         <a-select-option value="redirect">Redirect</a-select-option>
         <a-select-option value="tproxy">TProxy</a-select-option>
@@ -100,15 +65,9 @@
       <a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
     </a-form-item>
     <a-form-item label="Trusted X-Forwarded-For">
-      <a-select
-        mode="tags"
-        v-model="inbound.stream.sockopt.trustedXForwardedFor"
-        :style="{ width: '100%' }"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
-        <a-select-option value="CF-Connecting-IP"
-          >CF-Connecting-IP</a-select-option
-        >
+      <a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
+        :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
         <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
         <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
         <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
@@ -116,4 +75,4 @@
     </a-form-item>
   </template>
 </a-form>
-{{end}}
+{{end}}

+ 30 - 91
web/html/form/stream/stream_tcp.html

@@ -1,31 +1,19 @@
 {{define "form/streamTCP"}}
 <!-- tcp type -->
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
     <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
   </a-form-item>
   <a-form-item label='HTTP {{ i18n "camouflage" }}'>
-    <a-switch
-      :checked="inbound.stream.tcp.type === 'http'"
-      @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"
-    ></a-switch>
+    <a-switch :checked="inbound.stream.tcp.type === 'http'"
+      @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
   </a-form-item>
 </a-form>
 
-<a-form
-  v-if="inbound.stream.tcp.type === 'http'"
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
+  :wrapper-col="{ md: {span:14} }">
   <!-- tcp request -->
-  <a-divider :style="{ margin: '0' }"
-    >{{ i18n "pages.inbounds.stream.general.request" }}</a-divider
-  >
+  <a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
     <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
   </a-form-item>
@@ -33,66 +21,35 @@
     <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
   </a-form-item>
   <a-form-item>
-    <template slot="label"
-      >{{ i18n "pages.inbounds.stream.tcp.path" }}
-      <a-button
-        icon="plus"
-        size="small"
-        @click="inbound.stream.tcp.request.addPath('/')"
-      ></a-button>
+    <template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
+      <a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
     </template>
     <template v-for="(path, index) in inbound.stream.tcp.request.path">
       <a-input v-model.trim="inbound.stream.tcp.request.path[index]">
-        <a-button
-          icon="minus"
-          size="small"
-          slot="addonAfter"
-          @click="inbound.stream.tcp.request.removePath(index)"
-          v-if="inbound.stream.tcp.request.path.length>1"
-        ></a-button>
+        <a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
+          v-if="inbound.stream.tcp.request.path.length>1"></a-button>
       </a-input>
     </template>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button
-      icon="plus"
-      size="small"
-      @click="inbound.stream.tcp.request.addHeader('Host', '')"
-    ></a-button>
+    <a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group
-      compact
-      v-for="(header, index) in inbound.stream.tcp.request.headers"
-    >
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.name"
-        placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
-      >
-        <template slot="addonBefore" :style="{ margin: '0' }"
-          >[[ index+1 ]]</template
-        >
+    <a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
+      <a-input :style="{ width: '50%' }" v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
+        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
       </a-input>
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.value"
-        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
-      >
-        <a-button
-          icon="minus"
-          slot="addonAfter"
-          size="small"
-          @click="inbound.stream.tcp.request.removeHeader(index)"
-        ></a-button>
+      <a-input :style="{ width: '50%' }" v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+        <a-button icon="minus" slot="addonAfter" size="small"
+          @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
       </a-input>
     </a-input-group>
   </a-form-item>
 
   <!-- tcp response -->
-  <a-divider :style="{ margin: '0' }"
-    >{{ i18n "pages.inbounds.stream.general.response" }}</a-divider
-  >
+  <a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
     <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
   </a-form-item>
@@ -103,40 +60,22 @@
     <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
-    <a-button
-      icon="plus"
-      size="small"
-      @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"
-    ></a-button>
+    <a-button icon="plus" size="small"
+      @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group
-      compact
-      v-for="(header, index) in inbound.stream.tcp.response.headers"
-    >
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.name"
-        placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
-      >
-        <template slot="addonBefore" :style="{ margin: '0' }"
-          >[[ index+1 ]]</template
-        >
+    <a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
+      <a-input :style="{ width: '50%' }" v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
+        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
       </a-input>
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.value"
-        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
-      >
+      <a-input :style="{ width: '50%' }" v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
         <template slot="addonAfter">
-          <a-button
-            icon="minus"
-            size="small"
-            @click="inbound.stream.tcp.response.removeHeader(index)"
-          ></a-button>
+          <a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
         </template>
       </a-input>
     </a-input-group>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 10 - 34
web/html/form/stream/stream_ws.html

@@ -1,9 +1,5 @@
 {{define "form/streamWS"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label="Proxy Protocol">
     <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
   </a-form-item>
@@ -14,42 +10,22 @@
     <a-input v-model.trim="inbound.stream.ws.path"></a-input>
   </a-form-item>
   <a-form-item label="Heartbeat Period">
-    <a-input-number
-      v-model.number="inbound.stream.ws.heartbeatPeriod"
-      :min="0"
-    ></a-input-number>
+    <a-input-number v-model.number="inbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button
-      icon="plus"
-      size="small"
-      @click="inbound.stream.ws.addHeader('', '')"
-    ></a-button>
+    <a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('', '')"></a-button>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
     <a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.name"
-        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
-      >
-        <template slot="addonBefore" :style="{ margin: '0' }"
-          >[[ index+1 ]]</template
-        >
+      <a-input :style="{ width: '50%' }" v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
+        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
       </a-input>
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.value"
-        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
-      >
-        <a-button
-          icon="minus"
-          slot="addonAfter"
-          size="small"
-          @click="inbound.stream.ws.removeHeader(index)"
-        ></a-button>
+      <a-input :style="{ width: '50%' }" v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+        <a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
       </a-input>
     </a-input-group>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 43 - 129
web/html/form/stream/stream_xhttp.html

@@ -1,9 +1,5 @@
 {{define "form/streamXHTTP"}}
-<a-form
-  :colon="false"
-  :label-col="{ md: {span:8} }"
-  :wrapper-col="{ md: {span:14} }"
->
+<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <a-form-item label='{{ i18n "host" }}'>
     <a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
   </a-form-item>
@@ -11,69 +7,34 @@
     <a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
   </a-form-item>
   <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
-    <a-button
-      icon="plus"
-      size="small"
-      @click="inbound.stream.xhttp.addHeader('', '')"
-    ></a-button>
+    <a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
   </a-form-item>
   <a-form-item :wrapper-col="{span:24}">
-    <a-input-group
-      compact
-      v-for="(header, index) in inbound.stream.xhttp.headers"
-    >
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.name"
-        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
-      >
-        <template slot="addonBefore" :style="{ margin: '0' }"
-          >[[ index+1 ]]</template
-        >
+    <a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
+      <a-input :style="{ width: '50%' }" v-model.trim="header.name"
+        placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
+        <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
       </a-input>
-      <a-input
-        :style="{ width: '50%' }"
-        v-model.trim="header.value"
-        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
-      >
-        <a-button
-          icon="minus"
-          slot="addonAfter"
-          size="small"
-          @click="inbound.stream.xhttp.removeHeader(index)"
-        ></a-button>
+      <a-input :style="{ width: '50%' }" v-model.trim="header.value"
+        placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+        <a-button icon="minus" slot="addonAfter" size="small"
+          @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
       </a-input>
     </a-input-group>
   </a-form-item>
   <a-form-item label="Mode">
-    <a-select
-      v-model="inbound.stream.xhttp.mode"
-      :style="{ width: '50%' }"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
-      <a-select-option v-for="key in MODE_OPTION" :value="key"
-        >[[ key ]]</a-select-option
-      >
+    <a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
+      :dropdown-class-name="themeSwitcher.currentTheme">
+      <a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
     </a-select>
   </a-form-item>
-  <a-form-item
-    label="Max Buffered Upload"
-    v-if="inbound.stream.xhttp.mode === 'packet-up'"
-  >
-    <a-input-number
-      v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"
-    ></a-input-number>
-  </a-form-item>
-  <a-form-item
-    label="Max Upload Size (Byte)"
-    v-if="inbound.stream.xhttp.mode === 'packet-up'"
-  >
+  <a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
+    <a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
+  </a-form-item>
+  <a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
     <a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
   </a-form-item>
-  <a-form-item
-    label="Stream-Up Server"
-    v-if="inbound.stream.xhttp.mode === 'stream-up'"
-  >
+  <a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
     <a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
   </a-form-item>
   <a-form-item label="Padding Bytes">
@@ -84,22 +45,13 @@
   </a-form-item>
   <template v-if="inbound.stream.xhttp.xPaddingObfsMode">
     <a-form-item label="Padding Key">
-      <a-input
-        v-model.trim="inbound.stream.xhttp.xPaddingKey"
-        placeholder="x_padding"
-      ></a-input>
+      <a-input v-model.trim="inbound.stream.xhttp.xPaddingKey" placeholder="x_padding"></a-input>
     </a-form-item>
     <a-form-item label="Padding Header">
-      <a-input
-        v-model.trim="inbound.stream.xhttp.xPaddingHeader"
-        placeholder="X-Padding"
-      ></a-input>
+      <a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader" placeholder="X-Padding"></a-input>
     </a-form-item>
     <a-form-item label="Padding Placement">
-      <a-select
-        v-model="inbound.stream.xhttp.xPaddingPlacement"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
+      <a-select v-model="inbound.stream.xhttp.xPaddingPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
         <a-select-option value>Default (queryInHeader)</a-select-option>
         <a-select-option value="queryInHeader">queryInHeader</a-select-option>
         <a-select-option value="header">header</a-select-option>
@@ -108,10 +60,7 @@
       </a-select>
     </a-form-item>
     <a-form-item label="Padding Method">
-      <a-select
-        v-model="inbound.stream.xhttp.xPaddingMethod"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-      >
+      <a-select v-model="inbound.stream.xhttp.xPaddingMethod" :dropdown-class-name="themeSwitcher.currentTheme">
         <a-select-option value>Default (repeat-x)</a-select-option>
         <a-select-option value="repeat-x">repeat-x</a-select-option>
         <a-select-option value="tokenish">tokenish</a-select-option>
@@ -119,10 +68,7 @@
     </a-form-item>
   </template>
   <a-form-item label="Uplink HTTP Method">
-    <a-select
-      v-model="inbound.stream.xhttp.uplinkHTTPMethod"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+    <a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod" :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value>Default (POST)</a-select-option>
       <a-select-option value="POST">POST</a-select-option>
       <a-select-option value="PUT">PUT</a-select-option>
@@ -130,10 +76,7 @@
     </a-select>
   </a-form-item>
   <a-form-item label="Session Placement">
-    <a-select
-      v-model="inbound.stream.xhttp.sessionPlacement"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+    <a-select v-model="inbound.stream.xhttp.sessionPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value>Default (path)</a-select-option>
       <a-select-option value="path">path</a-select-option>
       <a-select-option value="header">header</a-select-option>
@@ -141,20 +84,12 @@
       <a-select-option value="query">query</a-select-option>
     </a-select>
   </a-form-item>
-  <a-form-item
-    label="Session Key"
-    v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
-  >
-    <a-input
-      v-model.trim="inbound.stream.xhttp.sessionKey"
-      placeholder="x_session"
-    ></a-input>
+  <a-form-item label="Session Key"
+    v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
+    <a-input v-model.trim="inbound.stream.xhttp.sessionKey" placeholder="x_session"></a-input>
   </a-form-item>
   <a-form-item label="Sequence Placement">
-    <a-select
-      v-model="inbound.stream.xhttp.seqPlacement"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+    <a-select v-model="inbound.stream.xhttp.seqPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value>Default (path)</a-select-option>
       <a-select-option value="path">path</a-select-option>
       <a-select-option value="header">header</a-select-option>
@@ -162,23 +97,12 @@
       <a-select-option value="query">query</a-select-option>
     </a-select>
   </a-form-item>
-  <a-form-item
-    label="Sequence Key"
-    v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
-  >
-    <a-input
-      v-model.trim="inbound.stream.xhttp.seqKey"
-      placeholder="x_seq"
-    ></a-input>
-  </a-form-item>
-  <a-form-item
-    label="Uplink Data Placement"
-    v-if="inbound.stream.xhttp.mode === 'packet-up'"
-  >
-    <a-select
-      v-model="inbound.stream.xhttp.uplinkDataPlacement"
-      :dropdown-class-name="themeSwitcher.currentTheme"
-    >
+  <a-form-item label="Sequence Key"
+    v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
+    <a-input v-model.trim="inbound.stream.xhttp.seqKey" placeholder="x_seq"></a-input>
+  </a-form-item>
+  <a-form-item label="Uplink Data Placement" v-if="inbound.stream.xhttp.mode === 'packet-up'">
+    <a-select v-model="inbound.stream.xhttp.uplinkDataPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
       <a-select-option value>Default (body)</a-select-option>
       <a-select-option value="body">body</a-select-option>
       <a-select-option value="header">header</a-select-option>
@@ -186,27 +110,17 @@
       <a-select-option value="query">query</a-select-option>
     </a-select>
   </a-form-item>
-  <a-form-item
-    label="Uplink Data Key"
-    v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
-  >
-    <a-input
-      v-model.trim="inbound.stream.xhttp.uplinkDataKey"
-      placeholder="x_data"
-    ></a-input>
-  </a-form-item>
-  <a-form-item
-    label="Uplink Chunk Size"
-    v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
-  >
-    <a-input-number
-      v-model.number="inbound.stream.xhttp.uplinkChunkSize"
-      :min="0"
-      placeholder="0 (unlimited)"
-    ></a-input-number>
+  <a-form-item label="Uplink Data Key"
+    v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
+    <a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data"></a-input>
+  </a-form-item>
+  <a-form-item label="Uplink Chunk Size"
+    v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
+    <a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize" :min="0"
+      placeholder="0 (unlimited)"></a-input-number>
   </a-form-item>
   <a-form-item label="No SSE Header">
     <a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
   </a-form-item>
 </a-form>
-{{end}}
+{{end}}

+ 12 - 31
web/html/form/tls_settings.html

@@ -1,14 +1,12 @@
 {{define "form/tlsSettings"}}
 <!-- tls enable -->
-<a-form v-if="inbound.canEnableTls()" :colon="false"
-  :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
   <template v-if="inbound.protocol !== Protocols.HYSTERIA">
     <a-divider :style="{ margin: '3px 0' }"></a-divider>
     <a-form-item label='{{ i18n "security" }}'>
       <a-radio-group v-model="inbound.stream.security" button-style="solid">
         <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
-        <a-radio-button v-if="inbound.canEnableReality()"
-          value="reality">Reality</a-radio-button>
+        <a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
         <a-radio-button value="tls">TLS</a-radio-button>
       </a-radio-group>
     </a-form-item>
@@ -21,8 +19,7 @@
       <a-input v-model.trim="inbound.stream.tls.sni"></a-input>
     </a-form-item>
     <a-form-item label="Cipher Suites">
-      <a-select v-model="inbound.stream.tls.cipherSuites"
-        :dropdown-class-name="themeSwitcher.currentTheme">
+      <a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
         <a-select-option value>Auto</a-select-option>
         <a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[
           value ]]</a-select-option>
@@ -30,14 +27,12 @@
     </a-form-item>
     <a-form-item label="Min/Max Version">
       <a-input-group compact>
-        <a-select v-model="inbound.stream.tls.minVersion"
-          :style="{ width: '50%' }"
+        <a-select v-model="inbound.stream.tls.minVersion" :style="{ width: '50%' }"
           :dropdown-class-name="themeSwitcher.currentTheme">
           <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
             ]]</a-select-option>
         </a-select>
-        <a-select v-model="inbound.stream.tls.maxVersion"
-          :style="{ width: '50%' }"
+        <a-select v-model="inbound.stream.tls.maxVersion" :style="{ width: '50%' }"
           :dropdown-class-name="themeSwitcher.currentTheme">
           <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
             ]]</a-select-option>
@@ -45,8 +40,7 @@
       </a-input-group>
     </a-form-item>
     <a-form-item label="uTLS">
-      <a-select v-model="inbound.stream.tls.settings.fingerprint"
-        :style="{ width: '100%' }"
+      <a-select v-model="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }"
         :dropdown-class-name="themeSwitcher.currentTheme">
         <a-select-option value>None</a-select-option>
         <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key
@@ -54,9 +48,7 @@
       </a-select>
     </a-form-item>
     <a-form-item label="ALPN">
-      <a-select mode="multiple"
-        :dropdown-class-name="themeSwitcher.currentTheme"
-        v-model="inbound.stream.tls.alpn">
+      <a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
         <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
           ]]</a-select-option>
       </a-select>
@@ -75,8 +67,7 @@
       <a-form-item label='{{ i18n "certificate" }}'>
         <a-radio-group v-model="cert.useFile" button-style="solid"
           :style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
-          <a-radio-button :value="true"
-            :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
+          <a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
             i18n "pages.inbounds.certificatePath" }}</a-radio-button>
           <a-radio-button :value="false"
             :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
@@ -87,8 +78,7 @@
         <a-space>
           <a-button icon="plus" v-if="index === 0" type="primary" size="small"
             @click="inbound.stream.tls.addCert()"></a-button>
-          <a-button icon="minus" v-if="inbound.stream.tls.certs.length>1"
-            type="primary" size="small"
+          <a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
             @click="inbound.stream.tls.removeCert(index)"></a-button>
         </a-space>
       </a-form-item>
@@ -100,8 +90,7 @@
           <a-input v-model.trim="cert.keyFile"></a-input>
         </a-form-item>
         <a-form-item label=" ">
-          <a-button type="primary" icon="import"
-            @click="setDefaultCertData(index)">
+          <a-button type="primary" icon="import" @click="setDefaultCertData(index)">
             {{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
         </a-form-item>
       </template>
@@ -117,8 +106,7 @@
         <a-switch v-model="cert.oneTimeLoading"></a-switch>
       </a-form-item>
       <a-form-item label='Usage Option'>
-        <a-select v-model="cert.usage" :style="{ width: '50%' }"
-          :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select v-model="cert.usage" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
           <a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key
             ]]</a-select-option>
         </a-select>
@@ -133,13 +121,6 @@
     <a-form-item label='ECH config'>
       <a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
     </a-form-item>
-    <a-form-item label='ECH force query'>
-      <a-select v-model="inbound.stream.tls.echForceQuery"
-        :dropdown-class-name="themeSwitcher.currentTheme">
-        <a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[
-          key ]]</a-select-option>
-      </a-select>
-    </a-form-item>
     <a-form-item label=" ">
       <a-space>
         <a-button type="primary" icon="import" @click="getNewEchCert">Get New
@@ -151,7 +132,7 @@
 
   <!-- reality settings -->
   <template v-if="inbound.stream.isReality">
-    {{template "form/realitySettings"}}
+    {{template "form/realitySettings" .}}
   </template>
 </a-form>
 {{end}}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 251 - 266
web/html/inbounds.html


+ 299 - 148
web/html/index.html

@@ -10,6 +10,7 @@
     padding: 2px 6px;
     border-radius: 3px;
   }
+
   html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code {
     color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.88));
     background: var(--dark-color-surface-700, #111929);
@@ -119,7 +120,8 @@
                           </a-row>
                         </span>
                         <template slot="content">
-                          <span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line ]]</span>
+                          <span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line
+                            ]]</span>
                         </template>
                         <a-badge :text="status.xray.stateMsg" :color="status.xray.color"
                           :class="status.xray.color === 'red' ? 'xray-error-animation' : ''" />
@@ -170,10 +172,11 @@
               <a-col :sm="24" :lg="12">
                 <a-card title='3X-UI' hoverable>
                   <template v-if="panelUpdateModal.info.updateAvailable" #extra>
-                    <a-tooltip :overlay-class-name="themeSwitcher.currentTheme" :title='`{{ i18n "pages.index.updatePanel" }}: ${panelUpdateModal.info.latestVersion}`'>
+                    <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"
+                      :title='`{{ i18n "pages.index.updatePanel" }}: ${panelUpdateModal.info.latestVersion}`'>
                       <a-tag color="orange" style="cursor:pointer;margin:0" @click="openPanelUpdate">
                         <a-icon type="cloud-download"></a-icon>[[ panelUpdateModal.info.latestVersion ]]
-                      <span v-if="!isMobile">{{ i18n "pages.index.updatePanel" }}</span>
+                        <span v-if="!isMobile">{{ i18n "pages.index.updatePanel" }}</span>
                       </a-tag>
                     </a-tooltip>
                   </template>
@@ -327,8 +330,7 @@
   </a-layout>
   <a-modal id="panel-update-modal" v-model="panelUpdateModal.visible" title='{{ i18n "pages.index.updatePanel" }}'
     :closable="true" @ok="() => panelUpdateModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
-    <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.panelUpdateDesc" }}'
-      show-icon></a-alert>
+    <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.panelUpdateDesc" }}' show-icon></a-alert>
     <a-list class="ant-version-list w-100" bordered>
       <a-list-item class="ant-version-list-item">
         <span>{{ i18n "pages.index.currentPanelVersion" }}</span>
@@ -379,57 +381,62 @@
       </a-collapse-panel>
       <a-collapse-panel key="3" header='{{ i18n "pages.index.customGeoTitle" }}'>
         <div class="custom-geo-section">
-        <a-alert type="info" show-icon class="mb-10"
-          message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
-        <div class="mb-10">
-          <a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
-            {{ i18n "pages.index.customGeoAdd" }}
-          </a-button>
-          <a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n
+          <a-alert type="info" show-icon class="mb-10"
+            message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
+          <div class="mb-10">
+            <a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
+              {{ i18n "pages.index.customGeoAdd" }}
+            </a-button>
+            <a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n
             "pages.index.geofilesUpdateAll" }}</a-button>
-        </div>
-        <a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
-          :loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
-          <template slot="extDat" slot-scope="text, record">
-            <code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
-          </template>
-          <template slot="lastUpdatedAt" slot-scope="text, record">
-            <span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
-            <span v-else>—</span>
-          </template>
-          <template slot="action" slot-scope="text, record">
-            <a-space size="small">
-              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
-                <template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template>
-                <a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button>
-              </a-tooltip>
-              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
-                <template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template>
-                <a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)" :loading="customGeoActionId === record.id"></a-button>
-              </a-tooltip>
-              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
-                <template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template>
-                <a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button>
-              </a-tooltip>
-            </a-space>
-          </template>
-        </a-table>
+          </div>
+          <a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
+            :loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
+            <template slot="extDat" slot-scope="text, record">
+              <code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
+            </template>
+            <template slot="lastUpdatedAt" slot-scope="text, record">
+              <span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
+              <span v-else>—</span>
+            </template>
+            <template slot="action" slot-scope="text, record">
+              <a-space size="small">
+                <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                  <template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template>
+                  <a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button>
+                </a-tooltip>
+                <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                  <template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template>
+                  <a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)"
+                    :loading="customGeoActionId === record.id"></a-button>
+                </a-tooltip>
+                <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
+                  <template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template>
+                  <a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button>
+                </a-tooltip>
+              </a-space>
+            </template>
+          </a-table>
         </div>
       </a-collapse-panel>
     </a-collapse>
   </a-modal>
-  <a-modal v-model="customGeoModal.visible" :title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'"
-    :confirm-loading="customGeoModal.saving" @ok="submitCustomGeo" :ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'"
+  <a-modal v-model="customGeoModal.visible"
+    :title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'"
+    :confirm-loading="customGeoModal.saving" @ok="submitCustomGeo"
+    :ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'"
     :class="themeSwitcher.currentTheme">
     <a-form layout="vertical">
       <a-form-item label='{{ i18n "pages.index.customGeoType" }}'>
-        <a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId" :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId"
+          :dropdown-class-name="themeSwitcher.currentTheme">
           <a-select-option value="geosite">geosite</a-select-option>
           <a-select-option value="geoip">geoip</a-select-option>
         </a-select>
       </a-form-item>
       <a-form-item label='{{ i18n "pages.index.customGeoAlias" }}'>
-        <a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId" placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input>
+        <a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId"
+          placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input>
       </a-form-item>
       <a-form-item label='{{ i18n "pages.index.customGeoUrl" }}'>
         <a-input v-model.trim="customGeoModal.form.url" placeholder="https://"></a-input>
@@ -469,7 +476,8 @@
         <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
       </a-form-item>
       <a-form-item style="float: right;">
-        <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
+        <a-button type="primary" icon="download"
+          @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
       </a-form-item>
     </a-form>
     <div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
@@ -547,7 +555,8 @@
       <sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
         :stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
         :max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
-      <div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div>
+      <div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point
+        (total [[ cpuHistoryLong.length ]] points)</div>
     </div>
   </a-modal>
 </a-layout>
@@ -555,33 +564,93 @@
 {{template "component/aSidebar" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aCustomStatistic" .}}
-{{template "modals/textModal"}}
+{{template "modals/textModal" .}}
 <script>
   // Tiny Sparkline component using an inline SVG polyline
   Vue.component('sparkline', {
     props: {
-      data: { type: Array, required: true },
+      data: {
+        type: Array,
+        required: true
+      },
       // viewBox width for drawing space; SVG width will be 100% of container
-      vbWidth: { type: Number, default: 320 },
-      height: { type: Number, default: 80 },
-      stroke: { type: String, default: '#008771' },
-      strokeWidth: { type: Number, default: 2 },
-      maxPoints: { type: Number, default: 120 },
-      showGrid: { type: Boolean, default: true },
-      gridColor: { type: String, default: 'rgba(0,0,0,0.1)' },
-      fillOpacity: { type: Number, default: 0.15 },
-      showMarker: { type: Boolean, default: true },
-      markerRadius: { type: Number, default: 2.8 },
+      vbWidth: {
+        type: Number,
+        default: 320
+      },
+      height: {
+        type: Number,
+        default: 80
+      },
+      stroke: {
+        type: String,
+        default: '#008771'
+      },
+      strokeWidth: {
+        type: Number,
+        default: 2
+      },
+      maxPoints: {
+        type: Number,
+        default: 120
+      },
+      showGrid: {
+        type: Boolean,
+        default: true
+      },
+      gridColor: {
+        type: String,
+        default: 'rgba(0,0,0,0.1)'
+      },
+      fillOpacity: {
+        type: Number,
+        default: 0.15
+      },
+      showMarker: {
+        type: Boolean,
+        default: true
+      },
+      markerRadius: {
+        type: Number,
+        default: 2.8
+      },
       // New opts for axes/labels/tooltip
-      labels: { type: Array, default: () => [] }, // same length as data for x labels (e.g., timestamps)
-      showAxes: { type: Boolean, default: false },
-      yTickStep: { type: Number, default: 25 }, // percent ticks
-      tickCountX: { type: Number, default: 4 },
-      paddingLeft: { type: Number, default: 32 },
-      paddingRight: { type: Number, default: 6 },
-      paddingTop: { type: Number, default: 6 },
-      paddingBottom: { type: Number, default: 20 },
-      showTooltip: { type: Boolean, default: false },
+      labels: {
+        type: Array,
+        default: () => []
+      }, // same length as data for x labels (e.g., timestamps)
+      showAxes: {
+        type: Boolean,
+        default: false
+      },
+      yTickStep: {
+        type: Number,
+        default: 25
+      }, // percent ticks
+      tickCountX: {
+        type: Number,
+        default: 4
+      },
+      paddingLeft: {
+        type: Number,
+        default: 32
+      },
+      paddingRight: {
+        type: Number,
+        default: 6
+      },
+      paddingTop: {
+        type: Number,
+        default: 6
+      },
+      paddingBottom: {
+        type: Number,
+        default: 20
+      },
+      showTooltip: {
+        type: Boolean,
+        default: false
+      },
     },
     data() {
       return {
@@ -644,7 +713,12 @@
         // draw at 25%, 50%, 75%
         return [0, 0.25, 0.5, 0.75, 1]
           .map(r => Math.round(this.paddingTop + h * r))
-          .map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
+          .map(y => ({
+            x1: this.paddingLeft,
+            y1: y,
+            x2: this.paddingLeft + w,
+            y2: y
+          }))
       },
       lastPoint() {
         if (this.pointsArr.length === 0) return null
@@ -656,7 +730,10 @@
         const ticks = []
         for (let p = 0; p <= 100; p += step) {
           const y = Math.round(this.paddingTop + (this.drawHeight - (p / 100) * this.drawHeight))
-          ticks.push({ y, label: `${p}%` })
+          ticks.push({
+            y,
+            label: `${p}%`
+          })
         }
         return ticks
       },
@@ -677,7 +754,10 @@
         positions.forEach(idx => {
           const label = labels[idx] != null ? String(labels[idx]) : String(idx)
           const x = Math.round(this.paddingLeft + idx * dx)
-          ticks.push({ x, label })
+          ticks.push({
+            x,
+            label
+          })
         })
         return ticks
       },
@@ -778,17 +858,36 @@
       this.disk = new CurTotal(0, 0);
       this.loads = [0, 0, 0];
       this.mem = new CurTotal(0, 0);
-      this.netIO = { up: 0, down: 0 };
-      this.netTraffic = { sent: 0, recv: 0 };
-      this.publicIP = { ipv4: 0, ipv6: 0 };
+      this.netIO = {
+        up: 0,
+        down: 0
+      };
+      this.netTraffic = {
+        sent: 0,
+        recv: 0
+      };
+      this.publicIP = {
+        ipv4: 0,
+        ipv6: 0
+      };
       this.swap = new CurTotal(0, 0);
       this.tcpCount = 0;
       this.udpCount = 0;
       this.uptime = 0;
       this.appUptime = 0;
-      this.appStats = { threads: 0, mem: 0, uptime: 0 };
+      this.appStats = {
+        threads: 0,
+        mem: 0,
+        uptime: 0
+      };
 
-      this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" };
+      this.xray = {
+        state: 'stop',
+        stateMsg: "",
+        errorMsg: "",
+        version: "",
+        color: ""
+      };
 
       if (data == null) {
         return;
@@ -864,6 +963,18 @@
     },
   };
 
+  const escapeHtml = (value) => {
+    if (value === null || value === undefined) {
+      return '';
+    }
+    return String(value)
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&#39;');
+  };
+
   const logModal = {
     visible: false,
     logs: [],
@@ -887,24 +998,28 @@
         if (index > 0) formattedLogs += '<br>';
 
         if (parts.length === 3) {
-          const d = parts[0];
-          const t = parts[1];
-          const level = parts[2];
-          const levelIndex = levels.indexOf(level, levels) || 5;
+          const d = escapeHtml(parts[0]);
+          const t = escapeHtml(parts[1]);
+          const levelRaw = parts[2];
+          const level = escapeHtml(levelRaw);
+          const idx = levels.indexOf(levelRaw);
+          const levelIndex = idx >= 0 ? idx : 5;
 
           //formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
           formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
           formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
         } else {
-          const levelIndex = levels.indexOf(data, levels) || 5;
-          formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
+          const idx = levels.indexOf(data);
+          const levelIndex = idx >= 0 ? idx : 5;
+          formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${escapeHtml(data)}</span>`;
         }
 
         if (message) {
-          if (message.startsWith("XRAY:"))
-            message = "<b>XRAY: </b>" + message.substring(5);
-          else
-            message = "<b>X-UI: </b>" + message;
+          if (message.startsWith("XRAY:")) {
+            message = "<b>XRAY: </b>" + escapeHtml(message.substring(5));
+          } else {
+            message = "<b>X-UI: </b>" + escapeHtml(message);
+          }
         }
 
         formattedLogs += message ? ' - ' + message : '';
@@ -918,20 +1033,20 @@
   };
 
   const xraylogModal = {
-      visible: false,
-      logs: [],
-      rows: 20,
-      showDirect: true,
-      showBlocked: true,
-      showProxy: true,
-      loading: false,
-      show(logs) {
-        this.visible = true;
-        this.logs = logs;
-        this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
-      },
-      formatLogs(logs) {
-        let formattedLogs = `
+    visible: false,
+    logs: [],
+    rows: 20,
+    showDirect: true,
+    showBlocked: true,
+    showProxy: true,
+    loading: false,
+    show(logs) {
+      this.visible = true;
+      this.logs = logs;
+      this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
+    },
+    formatLogs(logs) {
+      let formattedLogs = `
 <style>
   table {
     border-collapse: collapse;
@@ -954,38 +1069,37 @@
     </tr>
 `;
 
-        logs.reverse().forEach((log, index) => {
-          let outboundColor = '';
-          if (log.Event === 1) {
-            outboundColor = ' style="color: #e04141;"'; //red for blocked
-          }
-          else if (log.Event === 2) {
-            outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
-          }
+      logs.reverse().forEach((log, index) => {
+        let outboundColor = '';
+        if (log.Event === 1) {
+          outboundColor = ' style="color: #e04141;"'; //red for blocked
+        } else if (log.Event === 2) {
+          outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
+        }
 
-          let text = ``;
-          if (log.Email !== "") {
-            text = `<td>${log.Email}</td>`;
-          }
+        let text = ``;
+        if (log.Email !== "") {
+          text = `<td>${escapeHtml(log.Email)}</td>`;
+        }
 
-          formattedLogs += `
+        formattedLogs += `
 <tr ${outboundColor}>
-    <td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
-    <td>${log.FromAddress}</td>
-    <td>${log.ToAddress}</td>
-    <td>${log.Inbound}</td>
-    <td>${log.Outbound}</td>
+    <td><b>${escapeHtml(IntlUtil.formatDate(log.DateTime))}</b></td>
+    <td>${escapeHtml(log.FromAddress)}</td>
+    <td>${escapeHtml(log.ToAddress)}</td>
+    <td>${escapeHtml(log.Inbound)}</td>
+    <td>${escapeHtml(log.Outbound)}</td>
     ${text}
 </tr>
 `;
-        });
+      });
 
-        return formattedLogs += "</table>";
-      },
-      hide() {
-        this.visible = false;
-      },
-    };
+      return formattedLogs += "</table>";
+    },
+    hide() {
+      this.visible = false;
+    },
+  };
   const backupModal = {
     visible: false,
     show() {
@@ -996,10 +1110,31 @@
     },
   };
 
-  const customGeoColumns = [
-    { title: '{{ i18n "pages.index.customGeoExtColumn" }}', key: 'extDat', scopedSlots: { customRender: 'extDat' }, ellipsis: true },
-    { title: '{{ i18n "pages.index.customGeoLastUpdated" }}', key: 'lastUpdatedAt', scopedSlots: { customRender: 'lastUpdatedAt' }, width: 160 },
-    { title: '{{ i18n "pages.index.customGeoActions" }}', key: 'action', scopedSlots: { customRender: 'action' }, width: 120, fixed: 'right' },
+  const customGeoColumns = [{
+      title: '{{ i18n "pages.index.customGeoExtColumn" }}',
+      key: 'extDat',
+      scopedSlots: {
+        customRender: 'extDat'
+      },
+      ellipsis: true
+    },
+    {
+      title: '{{ i18n "pages.index.customGeoLastUpdated" }}',
+      key: 'lastUpdatedAt',
+      scopedSlots: {
+        customRender: 'lastUpdatedAt'
+      },
+      width: 160
+    },
+    {
+      title: '{{ i18n "pages.index.customGeoActions" }}',
+      key: 'action',
+      scopedSlots: {
+        customRender: 'action'
+      },
+      width: 120,
+      fixed: 'right'
+    },
   ];
 
   const app = new Vue({
@@ -1016,7 +1151,10 @@
       cpuHistory: [], // small live widget history
       cpuHistoryLong: [], // aggregated points from backend
       cpuHistoryLabels: [],
-      cpuHistoryModal: { visible: false, bucket: 2 },
+      cpuHistoryModal: {
+        visible: false,
+        bucket: 2
+      },
       versionModal,
       panelUpdateModal,
       logModal,
@@ -1092,16 +1230,16 @@
             const labels = []
             for (const p of msg.obj) {
               const d = new Date(p.t * 1000)
-              const hh = String(d.getHours()).padStart(2,'0')
-              const mm = String(d.getMinutes()).padStart(2,'0')
-              const ss = String(d.getSeconds()).padStart(2,'0')
-              labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
+              const hh = String(d.getHours()).padStart(2, '0')
+              const mm = String(d.getMinutes()).padStart(2, '0')
+              const ss = String(d.getSeconds()).padStart(2, '0')
+              labels.push(bucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
               vals.push(Math.max(0, Math.min(100, p.cpu)))
             }
             this.cpuHistoryLabels = labels
             this.cpuHistoryLong = vals
           }
-        } catch(e) {
+        } catch (e) {
           console.error('Failed to fetch bucketed cpu history', e)
         }
       },
@@ -1129,9 +1267,9 @@
         return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts);
       },
       customGeoExtDisplay(record) {
-        const fn = record.type === 'geoip'
-          ? `geoip_${record.alias}.dat`
-          : `geosite_${record.alias}.dat`;
+        const fn = record.type === 'geoip' ?
+          `geoip_${record.alias}.dat` :
+          `geosite_${record.alias}.dat`;
         return `ext:${fn}:tag`;
       },
       async loadCustomGeo() {
@@ -1285,18 +1423,18 @@
         const isSingleFile = !!fileName;
         this.$confirm({
           title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
-          content: isSingleFile
-            ? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
-            : '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
+          content: isSingleFile ?
+            '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName) :
+            '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
           okText: '{{ i18n "confirm"}}',
           class: themeSwitcher.currentTheme,
           cancelText: '{{ i18n "cancel"}}',
           onOk: async () => {
             versionModal.hide();
             this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
-            const url = isSingleFile
-              ? `/panel/api/server/updateGeofile/${fileName}`
-              : `/panel/api/server/updateGeofile`;
+            const url = isSingleFile ?
+              `/panel/api/server/updateGeofile/${fileName}` :
+              `/panel/api/server/updateGeofile`;
             await HttpUtil.post(url);
             this.loading(false);
           },
@@ -1320,7 +1458,10 @@
       },
       async openLogs() {
         logModal.loading = true;
-        const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog });
+        const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, {
+          level: logModal.level,
+          syslog: logModal.syslog
+        });
         if (!msg.success) {
           return;
         }
@@ -1330,7 +1471,12 @@
       },
       async openXrayLogs() {
         xraylogModal.loading = true;
-        const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy });
+        const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, {
+          filter: xraylogModal.filter,
+          showDirect: xraylogModal.showDirect,
+          showBlocked: xraylogModal.showBlocked,
+          showProxy: xraylogModal.showProxy
+        });
         if (!msg.success) {
           return;
         }
@@ -1347,10 +1493,15 @@
           try {
             const dt = l.DateTime ? new Date(l.DateTime) : null;
             const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
-            const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
+            const eventMap = {
+              0: 'DIRECT',
+              1: 'BLOCKED',
+              2: 'PROXY'
+            };
             const eventText = eventMap[l.Event] || String(l.Event ?? '');
             const emailPart = l.Email ? ` Email=${l.Email}` : '';
-            return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
+            return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`
+              .trim();
           } catch (e) {
             return JSON.stringify(l);
           }
@@ -1442,7 +1593,7 @@
       // Setup WebSocket for real-time updates
       if (window.wsClient) {
         window.wsClient.connect();
-        
+
         // Listen for status updates
         window.wsClient.on('status', (payload) => {
           this.setStatus(payload);
@@ -1491,4 +1642,4 @@
     },
   });
 </script>
-{{ template "page/body_end" .}}
+{{ template "page/body_end" .}}

+ 22 - 6
web/html/login.html

@@ -108,8 +108,15 @@
     el: '#app',
     data: {
       themeSwitcher,
-      loadingStates: { fetched: false, spinning: false },
-      user: { username: "", password: "", twoFactorCode: "" },
+      loadingStates: {
+        fetched: false,
+        spinning: false
+      },
+      user: {
+        username: "",
+        password: "",
+        twoFactorCode: ""
+      },
       twoFactorEnable: false,
       lang: "",
       animationStarted: false
@@ -143,7 +150,11 @@
       },
       initHeadline() {
         const animationDelay = 2000;
-        const headlines = this.$el.querySelectorAll('.headline');
+        const rootEl = this.$el instanceof Element ? this.$el : document.getElementById('app');
+        if (!rootEl || typeof rootEl.querySelectorAll !== 'function') {
+          return;
+        }
+        const headlines = rootEl.querySelectorAll('.headline');
         headlines.forEach((headline) => {
           const first = headline.querySelector('.is-visible');
           if (!first) return;
@@ -233,13 +244,18 @@
         }
       }
     });
-    pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });
+    pm_wait_for_forms.observe(pm_host, {
+      childList: true,
+      subtree: true
+    });
   }
 
   if (document.readyState === 'loading') {
-    document.addEventListener('DOMContentLoaded', pm_init, { once: true });
+    document.addEventListener('DOMContentLoaded', pm_init, {
+      once: true
+    });
   } else {
     pm_init();
   }
 </script>
-{{ template "page/body_end" .}}
+{{ template "page/body_end" .}}

+ 46 - 70
web/html/modals/client_bulk_modal.html

@@ -1,58 +1,41 @@
 {{define "modals/clientsBulkModal"}}
-<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible"
-    :title="clientsBulkModal.title"
-    @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading"
-    :closable="true" :mask-closable="false"
-    :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'
-    :class="themeSwitcher.currentTheme">
-    <a-form :colon="false" :label-col="{ md: {span:8} }"
-        :wrapper-col="{ md: {span:14} }">
+<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title"
+    @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
+    :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
+    <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
         <a-form-item label='{{ i18n "pages.client.method" }}'>
             <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
                 :dropdown-class-name="themeSwitcher.currentTheme">
                 <a-select-option :value="0">Random</a-select-option>
                 <a-select-option :value="1">Random+Prefix</a-select-option>
                 <a-select-option :value="2">Random+Prefix+Num</a-select-option>
-                <a-select-option
-                    :value="3">Random+Prefix+Num+Postfix</a-select-option>
+                <a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
                 <a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
             </a-select>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.first" }}'
-            v-if="clientsBulkModal.emailMethod>1">
-            <a-input-number v-model.number="clientsBulkModal.firstNum"
-                :min="1"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
+            <a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.last" }}'
-            v-if="clientsBulkModal.emailMethod>1">
-            <a-input-number v-model.number="clientsBulkModal.lastNum"
-                :min="clientsBulkModal.firstNum"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
+            <a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.prefix" }}'
-            v-if="clientsBulkModal.emailMethod>0">
+        <a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
             <a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.postfix" }}'
-            v-if="clientsBulkModal.emailMethod>2">
+        <a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
             <a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.clientCount" }}'
-            v-if="clientsBulkModal.emailMethod < 2">
-            <a-input-number v-model.number="clientsBulkModal.quantity" :min="1"
-                :max="500"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
+            <a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="500"></a-input-number>
         </a-form-item>
-        <a-form-item label='{{ i18n "security" }}'
-            v-if="inbound.protocol === Protocols.VMESS">
-            <a-select v-model="clientsBulkModal.security"
-                :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
+            <a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
                 <a-select-option v-for="key in USERS_SECURITY" :value="key">[[
                     key ]]</a-select-option>
             </a-select>
         </a-form-item>
-        <a-form-item label='Flow'
-            v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
-            <a-select v-model="clientsBulkModal.flow"
-                :dropdown-class-name="themeSwitcher.currentTheme">
+        <a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
+            <a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
                 <a-select-option value selected>{{ i18n "none"
                     }}</a-select-option>
                 <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
@@ -67,9 +50,7 @@
                             }}</span>
                     </template>
                     Subscription
-                    <a-icon
-                        @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)"
-                        type="sync"></a-icon>
+                    <a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
                 </a-tooltip>
             </template>
             <a-input v-model.trim="clientsBulkModal.subId"></a-input>
@@ -84,8 +65,7 @@
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
             </template>
-            <a-input-number :style="{ width: '50%' }"
-                v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
+            <a-input-number :style="{ width: '50%' }" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
         </a-form-item>
         <a-form-item v-if="app.ipLimitEnable">
             <template slot="label">
@@ -97,8 +77,7 @@
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
             </template>
-            <a-input-number v-model.number="clientsBulkModal.limitIp"
-                min="0"></a-input-number>
+            <a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
         </a-form-item>
         <a-form-item>
             <template slot="label">
@@ -110,17 +89,13 @@
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
             </template>
-            <a-input-number v-model.number="clientsBulkModal.totalGB"
-                :min="0"></a-input-number>
+            <a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
         </a-form-item>
         <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-            <a-switch v-model="clientsBulkModal.delayedStart"
-                @click="clientsBulkModal.expiryTime=0"></a-switch>
+            <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
         </a-form-item>
-        <a-form-item label='{{ i18n "pages.client.expireDays" }}'
-            v-if="clientsBulkModal.delayedStart">
-            <a-input-number v-model.number="delayedExpireDays"
-                :min="0"></a-input-number>
+        <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
+            <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
         </a-form-item>
         <a-form-item v-else>
             <template slot="label">
@@ -133,15 +108,11 @@
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
             </template>
-            <a-date-picker v-if="datepicker == 'gregorian'"
-                :show-time="{ format: 'HH:mm:ss' }"
-                format="YYYY-MM-DD HH:mm:ss"
-                :dropdown-class-name="themeSwitcher.currentTheme"
+            <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
+                format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
                 v-model="clientsBulkModal.expiryTime"></a-date-picker>
-            <a-persian-datepicker v-else
-                placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
-                value="clientsBulkModal.expiryTime"
-                v-model="clientsBulkModal.expiryTime">
+            <a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
+                value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
             </a-persian-datepicker>
         </a-form-item>
         <a-form-item v-if="clientsBulkModal.expiryTime != 0">
@@ -154,13 +125,11 @@
                     <a-icon type="question-circle"></a-icon>
                 </a-tooltip>
             </template>
-            <a-input-number v-model.number="clientsBulkModal.reset"
-                :min="0"></a-input-number>
+            <a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
         </a-form-item>
     </a-form>
 </a-modal>
 <script>
-
     const clientsBulkModal = {
         visible: false,
         confirmLoading: false,
@@ -219,7 +188,7 @@
             title = '',
             okText = '{{ i18n "sure" }}',
             dbInbound = null,
-            confirm = (inbound, dbInbound) => { }
+            confirm = (inbound, dbInbound) => {}
         }) {
             this.visible = true;
             this.title = title;
@@ -245,12 +214,19 @@
         },
         newClient(protocol) {
             switch (protocol) {
-                case Protocols.VMESS: return new Inbound.VmessSettings.VMESS();
-                case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
-                case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
-                case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
-                case Protocols.HYSTERIA: return new Inbound.HysteriaSettings.Hysteria();
-                default: return null;
+                case Protocols.VMESS:
+                    return new Inbound.VmessSettings.VMESS();
+                case Protocols.VLESS:
+                    return new Inbound.VLESSSettings.VLESS();
+                case Protocols.TROJAN:
+                    return new Inbound.TrojanSettings.Trojan();
+                case Protocols.SHADOWSOCKS:
+                    return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings
+                        .shadowsockses[0].method);
+                case Protocols.HYSTERIA:
+                    return new Inbound.HysteriaSettings.Hysteria();
+                default:
+                    return null;
             }
         },
         close() {
@@ -271,7 +247,8 @@
                 return this.clientsBulkModal.inbound;
             },
             get delayedExpireDays() {
-                return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
+                return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 :
+                    0;
             },
             get datepicker() {
                 return app.datepicker;
@@ -281,6 +258,5 @@
             },
         },
     });
-
 </script>
-{{end}}
+{{end}}

+ 43 - 28
web/html/modals/client_modal.html

@@ -1,19 +1,15 @@
 {{define "modals/clientsModal"}}
-<a-modal id="client-modal" v-model="clientModal.visible"
-    :title="clientModal.title" @ok="clientModal.ok"
-    :confirm-loading="clientModal.confirmLoading" :closable="true"
-    :mask-closable="false"
-    :class="themeSwitcher.currentTheme"
-    :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
+<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
+    :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
+    :class="themeSwitcher.currentTheme" :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
     <template v-if="isEdit">
         <a-tag v-if="isExpiry || isTrafficExhausted" color="red"
             :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
             is (Expired|Traffic Ended) And Disabled</a-tag>
     </template>
-    {{template "form/client"}}
+    {{template "form/client" .}}
 </a-modal>
 <script>
-
     const clientModal = {
         visible: false,
         confirmLoading: false,
@@ -30,12 +26,20 @@
         delayedStart: false,
         ok() {
             if (clientModal.isEdit) {
-                ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
+                ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal
+                    .oldClientId);
             } else {
                 ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
             }
         },
-        show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
+        show({
+            title = '',
+            okText = '{{ i18n "sure" }}',
+            index = null,
+            dbInbound = null,
+            confirm = () => {},
+            isEdit = false
+        }) {
             this.visible = true;
             this.title = title;
             this.okText = okText;
@@ -55,30 +59,41 @@
             }
             this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
             this.confirm = confirm;
-        },  
+        },
         getClientId(protocol, client) {
             switch (protocol) {
-                case Protocols.TROJAN: return client.password;
-                case Protocols.SHADOWSOCKS: return client.email;
-                case Protocols.HYSTERIA: return client.auth;
-                default: return client.id;
+                case Protocols.TROJAN:
+                    return client.password;
+                case Protocols.SHADOWSOCKS:
+                    return client.email;
+                case Protocols.HYSTERIA:
+                    return client.auth;
+                default:
+                    return client.id;
             }
         },
         addClient(inbound, clients) {
             switch (inbound.protocol) {
-                case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
-                case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
-                case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
-                case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
-                case Protocols.HYSTERIA: return clients.push(new Inbound.HysteriaSettings.Hysteria());
-                default: return null;
+                case Protocols.VMESS:
+                    return clients.push(new Inbound.VmessSettings.VMESS());
+                case Protocols.VLESS:
+                    return clients.push(new Inbound.VLESSSettings.VLESS());
+                case Protocols.TROJAN:
+                    return clients.push(new Inbound.TrojanSettings.Trojan());
+                case Protocols.SHADOWSOCKS:
+                    return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil
+                        .randomShadowsocksPassword(inbound.settings.method)));
+                case Protocols.HYSTERIA:
+                    return clients.push(new Inbound.HysteriaSettings.Hysteria());
+                default:
+                    return null;
             }
         },
         close() {
             clientModal.visible = false;
             clientModal.loading(false);
         },
-        loading(loading=true) {
+        loading(loading = true) {
             clientModal.confirmLoading = loading;
         },
     };
@@ -110,7 +125,8 @@
                 return true
             },
             get isExpiry() {
-                return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
+                return this.clientModal.isEdit && this.client.expiryTime > 0 ? (this.client.expiryTime <
+                    new Date().getTime()) : false;
             },
             get delayedStart() {
                 return this.clientModal.delayedStart;
@@ -150,8 +166,7 @@
                         return;
                     }
                     document.getElementById("clientIPs").value = "";
-                } catch (error) {
-                }
+                } catch (error) {}
             },
             resetClientTraffic(email, dbInboundId, iconElement) {
                 this.$confirm({
@@ -162,7 +177,8 @@
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: async () => {
                         iconElement.disabled = true;
-                        const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
+                        const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' +
+                            dbInboundId + '/resetClientTraffic/' + email);
                         if (msg.success) {
                             this.clientModal.clientStats.up = 0;
                             this.clientModal.clientStats.down = 0;
@@ -173,6 +189,5 @@
             },
         },
     });
-
 </script>
-{{end}}
+{{end}}

+ 10 - 5
web/html/modals/dns_presets_modal.html

@@ -5,10 +5,12 @@
     <a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
       <div class="ant-dns-presets-line">
         <a-space direction="horizontal" size="small" align="center">
-          <a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
+          <a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}'
+            : 'DNS' ]]</a-tag>
           <span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
         </a-space>
-        <a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
+        <a-button class="ant-dns-presets-install" type="primary"
+          @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
       </div>
     </a-list-item>
   </a-list>
@@ -36,8 +38,7 @@
 </style>
 
 <script>
-  const dnsPresetsDatabase = [
-    {
+  const dnsPresetsDatabase = [{
       name: 'Google DNS',
       family: false,
       data: [
@@ -96,7 +97,11 @@
     install(selectedPreset) {
       return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset);
     },
-    show({ title = '', selected = (selectedPreset) => { }, isEdit = false }) {
+    show({
+      title = '',
+      selected = (selectedPreset) => {},
+      isEdit = false
+    }) {
       this.title = title;
       this.selected = selected;
       this.visible = true;

+ 31 - 10
web/html/modals/inbound_info_modal.html

@@ -521,7 +521,8 @@
                       @click="copy(infoModal.wireguardLinks[index])"></a-button>
                   </a-tooltip>
                 </tr-info-title>
-                <code :style="{ display: 'block', whiteSpace: 'normal', wordBreak: 'break-all' }">[[ infoModal.wireguardLinks[index] ]]</code>
+                <code :style="{ display: 'block', whiteSpace: 'normal', wordBreak: 'break-all' }">[[
+                  infoModal.wireguardLinks[index] ]]</code>
               </tr-info-row>
             </td>
           </tr>
@@ -534,7 +535,10 @@
   function refreshIPs(email) {
     return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
       if (!msg.success) {
-        return { text: 'No IP Record', array: [] };
+        return {
+          text: 'No IP Record',
+          array: []
+        };
       }
 
       const formatIpRecord = (record) => {
@@ -574,7 +578,10 @@
           try {
             ips = JSON.parse(ips);
           } catch (e) {
-            return { text: String(ips), array: [String(ips)] };
+            return {
+              text: String(ips),
+              array: [String(ips)]
+            };
           }
         }
 
@@ -586,20 +593,32 @@
         // New format or object array
         if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
           const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
-          return { text: result.join(' | '), array: result };
+          return {
+            text: result.join(' | '),
+            array: result
+          };
         }
 
         // Old format - simple array of IPs
         if (Array.isArray(ips) && ips.length > 0) {
           const result = ips.map((ip) => String(ip));
-          return { text: result.join(', '), array: result };
+          return {
+            text: result.join(', '),
+            array: result
+          };
         }
 
         // Fallback for any other format
-        return { text: String(ips), array: [String(ips)] };
+        return {
+          text: String(ips),
+          array: [String(ips)]
+        };
 
       } catch (e) {
-        return { text: 'Error loading IPs', array: [] };
+        return {
+          text: 'Error loading IPs',
+          array: []
+        };
       }
     });
   }
@@ -626,7 +645,8 @@
       this.dbInbound = new DBInbound(dbInbound);
       this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
       this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
-      this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null;
+      this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this
+        .clientSettings.email) || null) : null;
 
       if (
         [
@@ -752,7 +772,8 @@
         return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
       },
       getRemStats() {
-        remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
+        remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats
+          .down;
         return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
       },
       refreshIPs() {
@@ -775,7 +796,7 @@
             this.infoModal.clientIps = 'No IP Record';
             this.infoModal.clientIpsArray = [];
           })
-          .catch(() => { });
+          .catch(() => {});
       },
     },
   });

+ 16 - 18
web/html/modals/inbound_modal.html

@@ -2,7 +2,7 @@
 <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
     @ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
     :class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
-    {{template "form/inbound"}}
+    {{template "form/inbound" .}}
 </a-modal>
 <script>
     // Make inModal globally available to ensure it works with any base path
@@ -23,7 +23,7 @@
             okText = '{{ i18n "sure" }}',
             inbound = null,
             dbInbound = null,
-            confirm = (inbound, dbInbound) => { },
+            confirm = (inbound, dbInbound) => {},
             isEdit = false,
         }) {
             this.title = title;
@@ -127,17 +127,17 @@
             get client() {
                 return inModal.inbound &&
                     inModal.inbound.clients &&
-                    inModal.inbound.clients.length > 0
-                    ? inModal.inbound.clients[0]
-                    : null;
+                    inModal.inbound.clients.length > 0 ?
+                    inModal.inbound.clients[0] :
+                    null;
             },
             get datepicker() {
                 return app.datepicker;
             },
             get delayedExpireDays() {
-                return this.client && this.client.expiryTime < 0
-                    ? this.client.expiryTime / -86400000
-                    : 0;
+                return this.client && this.client.expiryTime < 0 ?
+                    this.client.expiryTime / -86400000 :
+                    0;
             },
             set delayedExpireDays(days) {
                 this.client.expiryTime = -86400000 * days;
@@ -147,14 +147,12 @@
             },
             set externalProxy(value) {
                 if (value) {
-                    inModal.inbound.stream.externalProxy = [
-                        {
-                            forceTls: "same",
-                            dest: window.location.hostname,
-                            port: inModal.inbound.port,
-                            remark: "",
-                        },
-                    ];
+                    inModal.inbound.stream.externalProxy = [{
+                        forceTls: "same",
+                        dest: window.location.hostname,
+                        port: inModal.inbound.port,
+                        remark: "",
+                    }, ];
                 } else {
                     inModal.inbound.stream.externalProxy = [];
                 }
@@ -182,8 +180,8 @@
                     ) {
                         const hasVisionFlow = inModal.inbound.settings.vlesses.some(
                             (c) =>
-                                c.flow === "xtls-rprx-vision" ||
-                                c.flow === "xtls-rprx-vision-udp443",
+                            c.flow === "xtls-rprx-vision" ||
+                            c.flow === "xtls-rprx-vision-udp443",
                         );
                         if (
                             hasVisionFlow &&

+ 45 - 24
web/html/modals/nord_modal.html

@@ -1,25 +1,31 @@
 {{define "modals/nordModal"}}
 <a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx"
-         :confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true"
-         :footer="null" :class="themeSwitcher.currentTheme">
+    :confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true" :footer="null"
+    :class="themeSwitcher.currentTheme">
     <template v-if="nordModal.nordData == null">
         <a-tabs default-active-key="token" :class="themeSwitcher.currentTheme">
             <a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'>
-                <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
+                <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
+                    :style="{ marginTop: '20px' }">
                     <a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'>
-                        <a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
+                        <a-input v-model="nordModal.token"
+                            placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
                         <div :style="{ marginTop: '10px' }">
-                            <a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
+                            <a-button type="primary" icon="login" @click="login()"
+                                :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
                         </div>
                     </a-form-item>
                 </a-form>
             </a-tab-pane>
             <a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'>
-                <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
+                <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
+                    :style="{ marginTop: '20px' }">
                     <a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'>
-                        <a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
+                        <a-input v-model="nordModal.manualKey"
+                            placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
                         <div :style="{ marginTop: '10px' }">
-                            <a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
+                            <a-button type="primary" icon="save" @click="saveKey()"
+                                :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
                         </div>
                     </a-form-item>
                 </a-form>
@@ -39,7 +45,8 @@
         </table>
         <a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button>
         <a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
-        <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }">
+        <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
+            :style="{ marginTop: '10px' }">
             <a-form-item label='{{ i18n "pages.xray.outbound.country" }}'>
                 <a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label">
                     <a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name">
@@ -69,11 +76,13 @@
         <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
             <template v-if="nordOutboundIndex>=0">
                 <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
-                <a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
+                <a-button @click="resetOutbound" :loading="nordModal.confirmLoading"
+                    type="danger">{{ i18n "reset" }}</a-button>
             </template>
             <template v-else>
                 <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
-                <a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
+                <a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading"
+                    type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
             </template>
         </a-form>
     </template>
@@ -115,7 +124,9 @@
         },
         async login() {
             this.loading(true);
-            const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token });
+            const msg = await HttpUtil.post('/panel/xray/nord/reg', {
+                token: this.token
+            });
             if (msg.success) {
                 this.nordData = JSON.parse(msg.obj);
                 await this.fetchCountries();
@@ -124,7 +135,9 @@
         },
         async saveKey() {
             this.loading(true);
-            const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey });
+            const msg = await HttpUtil.post('/panel/xray/nord/setKey', {
+                key: this.manualKey
+            });
             if (msg.success) {
                 this.nordData = JSON.parse(msg.obj);
                 await this.fetchCountries();
@@ -160,7 +173,9 @@
             this.cities = [];
             this.serverId = null;
             this.cityId = null;
-            const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId });
+            const msg = await HttpUtil.post('/panel/xray/nord/servers', {
+                countryId: this.countryId
+            });
             if (msg.success) {
                 const data = JSON.parse(msg.obj);
                 const locations = data.locations || [];
@@ -173,7 +188,7 @@
                     }
                 });
                 this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name));
-                
+
                 this.servers = (data.servers || []).map(s => {
                     const firstLocId = (s.location_ids || [])[0];
                     const city = locToCity[firstLocId];
@@ -195,7 +210,7 @@
         addOutbound() {
             const server = this.servers.find(s => s.id === this.serverId);
             if (!server) return;
-            
+
             const tech = server.technologies.find(t => t.id === 35);
             const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
 
@@ -221,7 +236,7 @@
         resetOutbound(index) {
             const server = this.servers.find(s => s.id === this.serverId);
             if (!server || index === -1) return;
-            
+
             const tech = server.technologies.find(t => t.id === 35);
             const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
 
@@ -242,7 +257,7 @@
                 }
             };
             app.templateSettings.outbounds[index] = outbound;
-            
+
             // Sync routing rules
             app.templateSettings.routing.rules.forEach(r => {
                 if (r.outboundTag === oldTag) {
@@ -262,7 +277,8 @@
         },
         delRouting() {
             if (app.templateSettings && app.templateSettings.routing) {
-                app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-"));
+                app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag
+                    .startsWith("nord-"));
             }
         }
     };
@@ -276,10 +292,14 @@
         methods: {
             login: () => nordModal.login(),
             saveKey: () => nordModal.saveKey(),
-            logout() { nordModal.logout(this.nordOutboundIndex) },
+            logout() {
+                nordModal.logout(this.nordOutboundIndex)
+            },
             fetchServers: () => nordModal.fetchServers(),
             addOutbound: () => nordModal.addOutbound(),
-            resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) },
+            resetOutbound() {
+                nordModal.resetOutbound(this.nordOutboundIndex)
+            },
             onCityChange() {
                 if (this.filteredServers.length > 0) {
                     this.nordModal.serverId = this.filteredServers[0].id;
@@ -290,8 +310,9 @@
         },
         computed: {
             nordOutboundIndex: {
-                get: function () {
-                    return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1;
+                get: function() {
+                    return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag
+                        .startsWith("nord-")) : -1;
                 }
             },
             filteredServers: function() {
@@ -303,4 +324,4 @@
         }
     });
 </script>
-{{end}}
+{{end}}

+ 7 - 12
web/html/modals/prompt_modal.html

@@ -1,17 +1,13 @@
 {{define "modals/promptModal"}}
-<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
-         :closable="true" @ok="promptModal.ok" :mask-closable="false"
-         :confirm-loading="promptModal.confirmLoading"
-         :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
-    <a-input id="prompt-modal-input" :type="promptModal.type"
-             v-model="promptModal.value"
-             :autosize="{minRows: 10, maxRows: 20}"
-             @keydown.enter.native="promptModal.keyEnter"
-             @keydown.ctrl.83="promptModal.ctrlS"></a-input>
+<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" :closable="true"
+    @ok="promptModal.ok" :mask-closable="false" :confirm-loading="promptModal.confirmLoading"
+    :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
+    <a-input id="prompt-modal-input" :type="promptModal.type" v-model="promptModal.value"
+        :autosize="{minRows: 10, maxRows: 20}" @keydown.enter.native="promptModal.keyEnter"
+        @keydown.ctrl.83="promptModal.ctrlS"></a-input>
 </a-modal>
 
 <script>
-
     const promptModal = {
         title: '',
         type: '',
@@ -55,7 +51,7 @@
         close() {
             this.visible = false;
         },
-        loading(loading=true) {
+        loading(loading = true) {
             this.confirmLoading = loading;
         },
     };
@@ -66,6 +62,5 @@
             promptModal: promptModal,
         },
     });
-
 </script>
 {{end}}

+ 22 - 17
web/html/modals/qrcode_modal.html

@@ -57,39 +57,44 @@
     border-radius: 1rem;
     overflow-x: hidden;
   }
-  
+
   /* QR code transition effects */
   .qr-cv {
     transition: all 0.3s ease-in-out;
   }
-  
-  .qr-transition-enter-active, .qr-transition-leave-active {
+
+  .qr-transition-enter-active,
+  .qr-transition-leave-active {
     transition: opacity 0.3s, transform 0.3s;
   }
-  
-  .qr-transition-enter, .qr-transition-leave-to {
+
+  .qr-transition-enter,
+  .qr-transition-leave-to {
     opacity: 0;
     transform: scale(0.9);
   }
-  
-  .qr-transition-enter-to, .qr-transition-leave {
+
+  .qr-transition-enter-to,
+  .qr-transition-leave {
     opacity: 1;
     transform: scale(1);
   }
-  
+
   .qr-flash {
     animation: qr-flash-animation 0.6s;
   }
-  
+
   @keyframes qr-flash-animation {
     0% {
       opacity: 1;
       transform: scale(1);
     }
+
     50% {
       opacity: 0.5;
       transform: scale(0.95);
     }
+
     100% {
       opacity: 1;
       transform: scale(1);
@@ -105,7 +110,7 @@
     qrcodes: [],
     visible: false,
     subId: '',
-    show: function (title = '', dbInbound, client) {
+    show: function(title = '', dbInbound, client) {
       this.title = title;
       this.dbInbound = dbInbound;
       this.inbound = dbInbound.toInbound();
@@ -135,7 +140,7 @@
       }
       this.visible = true;
     },
-    close: function () {
+    close: function() {
       this.visible = false;
     },
   };
@@ -159,7 +164,7 @@
           console.error("Failed to get status:", e);
         }
       },
-      
+
       toggleIPv4(index) {
         const row = qrModal.qrcodes[index];
         row.useIPv4 = !row.useIPv4;
@@ -170,13 +175,13 @@
         if (!this.serverStatus || !this.serverStatus.publicIP) {
           return;
         }
-        
+
         if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
           // Replace the hostname or IP in the link with the IPv4 address
           const originalLink = row.originalLink;
           const url = new URL(originalLink);
           const ipv4 = this.serverStatus.publicIP.ipv4;
-          
+
           if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
             // Special handling for WireGuard config
             const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
@@ -196,19 +201,19 @@
           // Restore original link
           row.link = row.originalLink;
         }
-        
+
         // Update QR code with transition effect
         const canvasElement = document.querySelector('#qrCode-' + index);
         if (canvasElement) {
           // Add flash animation class
           canvasElement.classList.add('qr-flash');
-          
+
           // Remove the class after animation completes
           setTimeout(() => {
             canvasElement.classList.remove('qr-flash');
           }, 600);
         }
-        
+
         this.setQrCode("qrCode-" + index, row.link);
       },
       copy(content) {

+ 3 - 3
web/html/modals/text_modal.html

@@ -21,13 +21,13 @@
         fileName: '',
         qrcode: null,
         visible: false,
-        show: function (title = '', content = '', fileName = '') {
+        show: function(title = '', content = '', fileName = '') {
             this.title = title;
             this.content = content;
             this.fileName = fileName;
             this.visible = true;
         },
-        copy: function (content = '') {
+        copy: function(content = '') {
             ClipboardManager
                 .copyText(content)
                 .then(() => {
@@ -35,7 +35,7 @@
                     this.close();
                 })
         },
-        close: function () {
+        close: function() {
             this.visible = false;
         },
     };

+ 29 - 29
web/html/modals/two_factor_modal.html

@@ -55,12 +55,12 @@
 
             twoFactorModal.close()
         },
-        show: function ({
+        show: function({
             title = '',
             description = '',
             token = '',
             type = 'set',
-            confirm = (success) => { }
+            confirm = (success) => {}
         }) {
             this.title = title;
             this.description = description;
@@ -78,7 +78,7 @@
                 secret: twoFactorModal.token,
             });
         },
-        close: function () {
+        close: function() {
             twoFactorModal.enteredCode = "";
             twoFactorModal.visible = false;
         },
@@ -91,34 +91,34 @@
             twoFactorModal: twoFactorModal,
         },
         updated() {
-          if (
-            this.twoFactorModal.visible &&
-            this.twoFactorModal.type === 'set' &&
-            document.getElementById('twofactor-qrcode')
-          ) {
-            this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());
-          }
+            if (
+                this.twoFactorModal.visible &&
+                this.twoFactorModal.type === 'set' &&
+                document.getElementById('twofactor-qrcode')
+            ) {
+                this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());
+            }
         },
         methods: {
-          setQrCode(elementId, content) {
-            new QRious({
-              element: document.getElementById(elementId),
-              size: 200,
-              value: content,
-              background: 'white',
-              backgroundAlpha: 0,
-              foreground: 'black',
-              padding: 2,
-              level: 'L'
-            });
-          },
-          copy(content) {
-            ClipboardManager
-              .copyText(content)
-              .then(() => {
-                app.$message.success('{{ i18n "copied" }}')
-              })
-          },
+            setQrCode(elementId, content) {
+                new QRious({
+                    element: document.getElementById(elementId),
+                    size: 200,
+                    value: content,
+                    background: 'white',
+                    backgroundAlpha: 0,
+                    foreground: 'black',
+                    padding: 2,
+                    level: 'L'
+                });
+            },
+            copy(content) {
+                ClipboardManager
+                    .copyText(content)
+                    .then(() => {
+                        app.$message.success('{{ i18n "copied" }}')
+                    })
+            },
         }
     });
 </script>

+ 12 - 10
web/html/modals/warp_modal.html

@@ -1,7 +1,6 @@
 {{define "modals/warpModal"}}
-<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP"
-         :confirm-loading="warpModal.confirmLoading" :closable="true" :mask-closable="true"
-         :footer="null" :class="themeSwitcher.currentTheme">
+<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP" :confirm-loading="warpModal.confirmLoading"
+    :closable="true" :mask-closable="true" :footer="null" :class="themeSwitcher.currentTheme">
     <template v-if="ObjectUtil.isEmpty(warpModal.warpData)">
         <a-button icon="api" @click="register" :loading="warpModal.confirmLoading">{{ i18n "create" }}</a-button>
     </template>
@@ -81,11 +80,13 @@
             <a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
                 <template v-if="warpOutboundIndex>=0">
                     <a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
-                    <a-button @click="resetOutbound" :loading="warpModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
+                    <a-button @click="resetOutbound" :loading="warpModal.confirmLoading"
+                        type="danger">{{ i18n "reset" }}</a-button>
                 </template>
                 <template v-else>
                     <a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
-                    <a-button @click="addOutbound" :loading="warpModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
+                    <a-button @click="addOutbound" :loading="warpModal.confirmLoading"
+                        type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
                 </template>
                 </a-form-item>
             </a-form>
@@ -93,7 +94,6 @@
     </template>
 </a-modal>
 <script>
-
     const warpModal = {
         visible: false,
         confirmLoading: false,
@@ -188,7 +188,9 @@
             },
             async updateLicense(l) {
                 warpModal.loading(true);
-                const msg = await HttpUtil.post('/panel/xray/warp/license', { license: l });
+                const msg = await HttpUtil.post('/panel/xray/warp/license', {
+                    license: l
+                });
                 if (msg.success) {
                     warpModal.warpData = JSON.parse(msg.obj);
                     warpModal.warpConfig = null;
@@ -235,12 +237,12 @@
         },
         computed: {
             warpOutboundIndex: {
-                get: function () {
-                    return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1;
+                get: function() {
+                    return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag ==
+                        'warp') : -1;
                 }
             }
         }
     });
-
 </script>
 {{end}}

+ 18 - 19
web/html/modals/xray_balancer_modal.html

@@ -1,15 +1,7 @@
 {{define "modals/balancerModal"}}
-<a-modal 
-    id="balancer-modal"
-    v-model="balancerModal.visible"
-    :title="balancerModal.title"
-    @ok="balancerModal.ok"
-    :confirm-loading="balancerModal.confirmLoading"
-    :ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
-    :closable="true"
-    :mask-closable="false"
-    :ok-text="balancerModal.okText"
-    cancel-text='{{ i18n "close" }}'
+<a-modal id="balancer-modal" v-model="balancerModal.visible" :title="balancerModal.title" @ok="balancerModal.ok"
+    :confirm-loading="balancerModal.confirmLoading" :ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
+    :closable="true" :mask-closable="false" :ok-text="balancerModal.okText" cancel-text='{{ i18n "close" }}'
     :class="themeSwitcher.currentTheme">
     <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
         <a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
@@ -35,7 +27,8 @@
         <a-form-item label="Fallback">
             <a-select v-model="balancerModal.balancer.fallbackTag" clearable
                 :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag ]]</a-select-option>
+                <a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag
+                    ]]</a-select-option>
             </a-select>
         </a-form-item>
         </table>
@@ -58,7 +51,7 @@
             fallbackTag: ''
         },
         outboundTags: [],
-        balancerTags:[],
+        balancerTags: [],
         ok() {
             if (balancerModal.balancer.selector.length == 0) {
                 balancerModal.emptySelector = true;
@@ -67,7 +60,14 @@
             balancerModal.emptySelector = false;
             ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
         },
-        show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
+        show({
+            title = '',
+            okText = '{{ i18n "sure" }}',
+            balancerTags = [],
+            balancer,
+            confirm = (balancer) => {},
+            isEdit = false
+        }) {
             this.title = title;
             this.okText = okText;
             this.confirm = confirm;
@@ -83,7 +83,8 @@
                 };
             }
             this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
-            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
+            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
+                obj.tag);
             this.isEdit = isEdit;
             this.check();
             this.checkSelector();
@@ -92,7 +93,7 @@
             this.visible = false;
             this.loading(false);
         },
-        loading(loading=true) {
+        loading(loading = true) {
             this.confirmLoading = loading;
         },
         check() {
@@ -115,9 +116,7 @@
         data: {
             balancerModal: balancerModal
         },
-        methods: {
-        }
+        methods: {}
     });
-
 </script>
 {{end}}

+ 13 - 5
web/html/modals/xray_dns_modal.html

@@ -75,15 +75,19 @@
     okText: '{{ i18n "confirm" }}',
     isEdit: false,
     confirm: null,
-    dnsServer: { ...defaultDnsObject },
+    dnsServer: {
+      ...defaultDnsObject
+    },
     ok() {
-      ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
+      ObjectUtil.execute(dnsModal.confirm, {
+        ...dnsModal.dnsServer
+      });
     },
     show({
       title = '',
       okText = '{{ i18n "confirm" }}',
       dnsServer,
-      confirm = (dnsServer) => { },
+      confirm = (dnsServer) => {},
       isEdit = false
     }) {
       this.title = title;
@@ -95,7 +99,9 @@
       if (isEdit) {
         switch (typeof dnsServer) {
           case 'string':
-            const dnsObj = { ...defaultDnsObject };
+            const dnsObj = {
+              ...defaultDnsObject
+            };
 
             dnsObj.address = dnsServer;
 
@@ -106,7 +112,9 @@
             break;
         }
       } else {
-        this.dnsServer = { ...defaultDnsObject };
+        this.dnsServer = {
+          ...defaultDnsObject
+        };
 
         this.dnsServer.domains = [];
         this.dnsServer.expectIPs = [];

+ 13 - 4
web/html/modals/xray_fakedns_modal.html

@@ -23,11 +23,19 @@
     okText: '{{ i18n "confirm" }}',
     isEdit: false,
     confirm: null,
-    fakeDns: { ...fakednsDefaultData },
+    fakeDns: {
+      ...fakednsDefaultData
+    },
     ok() {
       ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
     },
-    show({ title = '', okText = '{{ i18n "confirm" }}', fakeDns, confirm = (fakeDns) => { }, isEdit = false }) {
+    show({
+      title = '',
+      okText = '{{ i18n "confirm" }}',
+      fakeDns,
+      confirm = (fakeDns) => {},
+      isEdit = false
+    }) {
       this.title = title;
       this.okText = okText;
       this.confirm = confirm;
@@ -35,7 +43,9 @@
       if (isEdit) {
         this.fakeDns = fakeDns;
       } else {
-        this.fakeDns = { ...fakednsDefaultData }
+        this.fakeDns = {
+          ...fakednsDefaultData
+        }
       }
       this.isEdit = isEdit;
     },
@@ -51,6 +61,5 @@
       fakednsModal: fakednsModal,
     }
   });
-
 </script>
 {{end}}

+ 30 - 24
web/html/modals/xray_outbound_modal.html

@@ -1,12 +1,11 @@
 {{define "modals/outModal"}}
 <a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
-         :confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
-         :ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
-         :ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
-         {{template "form/outbound"}}
+    :confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
+    :ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
+    :ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
+    {{template "form/outbound" .}}
 </a-modal>
 <script>
-
     const outModal = {
         title: '',
         visible: false,
@@ -25,7 +24,14 @@
         ok() {
             ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
         },
-        show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[]  }) {
+        show({
+            title = '',
+            okText = '{{ i18n "sure" }}',
+            outbound,
+            confirm = (outbound) => {},
+            isEdit = false,
+            tags = []
+        }) {
             this.title = title;
             this.okText = okText;
             this.confirm = confirm;
@@ -42,11 +48,11 @@
             outModal.visible = false;
             outModal.loading(false);
         },
-        loading(loading=true) {
+        loading(loading = true) {
             outModal.confirmLoading = loading;
         },
-        check(){
-            if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){
+        check() {
+            if (outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)) {
                 this.duplicateTag = true;
                 this.isValid = false;
             } else {
@@ -56,25 +62,25 @@
         },
         toggleJson(jsonTab) {
             textAreaObj = document.getElementById('outboundJson');
-            if(jsonTab){
-                if(this.cm != null) {
-                        this.cm.toTextArea();
-                        this.cm=null;
+            if (jsonTab) {
+                if (this.cm != null) {
+                    this.cm.toTextArea();
+                    this.cm = null;
                 }
                 textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
                 this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
-                this.cm.on('change',editor => {
+                this.cm.on('change', editor => {
                     value = editor.getValue();
-                    if(this.isJsonString(value)){
+                    if (this.isJsonString(value)) {
                         this.outbound = Outbound.fromJson(JSON.parse(value));
                         this.check();
                     }
                 });
                 this.activeKey = '2';
             } else {
-                if(this.cm != null) {
-                        this.cm.toTextArea();
-                        this.cm=null;
+                if (this.cm != null) {
+                    this.cm.toTextArea();
+                    this.cm = null;
                 }
                 this.activeKey = '1';
             }
@@ -100,20 +106,21 @@
         },
         methods: {
             streamNetworkChange() {
-                if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {
+                if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound
+                .canEnableTlsFlow()) {
                     delete this.outModal.outbound.settings.flow;
                 }
             },
             canEnableTls() {
                 return this.outModal.outbound.canEnableTls();
             },
-            convertLink(){
+            convertLink() {
                 newOutbound = Outbound.fromLink(outModal.link);
-                if(newOutbound){
+                if (newOutbound) {
                     this.outModal.outbound = newOutbound;
                     this.outModal.toggleJson(true);
                     this.outModal.check();
-                    this.$message.success('Link imported successfully...');      
+                    this.$message.success('Link imported successfully...');
                     outModal.link = '';
                 } else {
                     this.$message.error('Wrong Link!');
@@ -122,6 +129,5 @@
             },
         },
     });
-
 </script>
-{{end}}
+{{end}}

+ 60 - 34
web/html/modals/xray_reverse_modal.html

@@ -1,7 +1,7 @@
 {{define "modals/reverseModal"}}
 <a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
-         :confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
-         :ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
+    :confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
+    :ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
     <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
         <a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
             <a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -15,26 +15,24 @@
             <a-input v-model.trim="reverseModal.reverse.domain"></a-input>
         </a-form-item>
         <template v-if="reverseModal.reverse.type=='bridge'">
-        <a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
-            <a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
-            </a-select>
-        </a-form-item>
-        <a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
-            <a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
-                <a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
-            </a-select>
-        </a-form-item>
+            <a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
+                <a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
+                    <a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
+                </a-select>
+            </a-form-item>
+            <a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
+                <a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
+                    <a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
+                </a-select>
+            </a-form-item>
         </template>
         <template v-else>
             <a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
-                <a-checkbox-group
-                    v-model="reverseModal.rules[0].inboundTag"
+                <a-checkbox-group v-model="reverseModal.rules[0].inboundTag"
                     :options="reverseModal.inboundTags"></a-checkbox-group>
             </a-form-item>
             <a-form-item label='{{ i18n "pages.xray.rules.inbound" }}'>
-                <a-checkbox-group
-                    v-model="reverseModal.rules[1].inboundTag"
+                <a-checkbox-group v-model="reverseModal.rules[1].inboundTag"
                     :options="reverseModal.inboundTags"></a-checkbox-group>
             </a-form-item>
         </template>
@@ -53,9 +51,14 @@
             type: "",
             domain: ""
         },
-        rules: [
-            { outboundTag: '', inboundTag: []},
-            { outboundTag: '', inboundTag: []}
+        rules: [{
+                outboundTag: '',
+                inboundTag: []
+            },
+            {
+                outboundTag: '',
+                inboundTag: []
+            }
         ],
         inboundTags: [],
         outboundTags: [],
@@ -64,7 +67,7 @@
             reverseModal.rules[0].type = 'field';
             reverseModal.rules[1].type = 'field';
 
-            if(reverseModal.reverse.type == 'bridge'){
+            if (reverseModal.reverse.type == 'bridge') {
                 reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];
                 reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];
             } else {
@@ -73,22 +76,36 @@
             }
             ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
         },
-        show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false  }) {
+        show({
+            title = '',
+            okText = '{{ i18n "sure" }}',
+            reverse,
+            rules,
+            confirm = (reverse, rules) => {},
+            isEdit = false
+        }) {
             this.title = title;
             this.okText = okText;
             this.confirm = confirm;
             this.visible = true;
-            if(isEdit) {
+            if (isEdit) {
                 this.reverse = {
                     tag: reverse.tag,
                     type: reverse.type,
                     domain: reverse.domain,
                 };
-                    reverse;
+                reverse;
                 rules0 = rules.filter(r => r.domain != null);
-                if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: ["full:" + this.reverse.domain], inboundTag: []}];
+                if (rules0.length == 0) rules0 = [{
+                    outboundTag: '',
+                    domain: ["full:" + this.reverse.domain],
+                    inboundTag: []
+                }];
                 rules1 = rules.filter(r => r.domain == null);
-                if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}];
+                if (rules1.length == 0) rules1 = [{
+                    outboundTag: '',
+                    inboundTag: []
+                }];
                 this.rules = [];
                 this.rules.push({
                     domain: rules0[0].domain,
@@ -105,22 +122,29 @@
                     type: "bridge",
                     domain: "reverse.xui"
                 }
-                this.rules = [
-                    { outboundTag: '', inboundTag: []},
-                    { outboundTag: '', inboundTag: []}
+                this.rules = [{
+                        outboundTag: '',
+                        inboundTag: []
+                    },
+                    {
+                        outboundTag: '',
+                        inboundTag: []
+                    }
                 ]
             }
             this.isEdit = isEdit;
-            this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
+            this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj =>
+                obj.tag);
             this.inboundTags.push(...app.inboundTags);
             if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
-            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
+            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
+                obj.tag);
         },
         close() {
             reverseModal.visible = false;
             reverseModal.loading(false);
         },
-        loading(loading=true) {
+        loading(loading = true) {
             reverseModal.confirmLoading = loading;
         },
     };
@@ -130,9 +154,11 @@
         el: '#reverse-modal',
         data: {
             reverseModal: reverseModal,
-            reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
+            reverseTypes: {
+                bridge: '{{ i18n "pages.xray.outbound.bridge" }}',
+                portal: '{{ i18n "pages.xray.outbound.portal" }}'
+            },
         },
     });
-
 </script>
-{{end}}
+{{end}}

+ 21 - 10
web/html/modals/xray_rule_modal.html

@@ -1,5 +1,7 @@
 {{define "modals/ruleModal"}}
-<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
+<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
+  :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText"
+  cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
   <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
     <a-form-item>
       <template slot="label">
@@ -42,15 +44,19 @@
       </a-select>
     </a-form-item>
     <a-form-item label='Attributes'>
-      <a-button icon="plus" size="small" :style="{ marginLeft: '10px' }" @click="ruleModal.rule.attrs.push(['', ''])"></a-button>
+      <a-button icon="plus" size="small" :style="{ marginLeft: '10px' }"
+        @click="ruleModal.rule.attrs.push(['', ''])"></a-button>
     </a-form-item>
     <a-form-item :wrapper-col="{span: 24}">
       <a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
-        <a-input :style="{ width: '50%' }" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
+        <a-input :style="{ width: '50%' }" v-model="attr[0]"
+          placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
           <template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
         </a-input>
-        <a-input :style="{ width: '50%' }" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
-          <a-button icon="minus" slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)"></a-button>
+        <a-input :style="{ width: '50%' }" v-model="attr[1]"
+          placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
+          <a-button icon="minus" slot="addonAfter" size="small"
+            @click="ruleModal.rule.attrs.splice(index,1)"></a-button>
         </a-input>
       </a-input-group>
     </a-form-item>
@@ -196,16 +202,20 @@
       this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
       this.inboundTags.push(...app.inboundTags);
       if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
-      this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
+      this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
+        obj.tag)];
       if (app.templateSettings.reverse) {
         if (app.templateSettings.reverse.bridges) {
           this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
         }
-        if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
+        if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(
+          b => b.tag));
       }
       this.balancerTags = [""];
       if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
-        this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
+        this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag))
+          .map(obj => obj.tag)
+        ];
       }
     },
     close() {
@@ -234,7 +244,8 @@
       rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
       rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
       for (const [key, value] of Object.entries(rule)) {
-        if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
+        if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(
+            typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
           newRule[key] = value;
         }
       }
@@ -249,4 +260,4 @@
     }
   });
 </script>
-{{end}}
+{{end}}

+ 174 - 65
web/html/settings.html

@@ -79,7 +79,8 @@
                     </template>
                     {{ template "settings/panel/subscription/general" . }}
                   </a-tab-pane>
-                  <a-tab-pane key="5" v-if="allSetting.subJsonEnable || allSetting.subClashEnable" :style="{ paddingTop: '20px' }">
+                  <a-tab-pane key="5" v-if="allSetting.subJsonEnable || allSetting.subClashEnable"
+                    :style="{ paddingTop: '20px' }">
                     <template #tab>
                       <a-icon type="code"></a-icon>
                       <span>{{ i18n "pages.settings.subSettings" }} (Formats)</span>
@@ -102,7 +103,7 @@
 {{template "component/aSidebar" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aSettingListItem" .}}
-{{template "modals/twoFactorModal"}}
+{{template "modals/twoFactorModal" .}}
 <script>
   const app = new Vue({
     delimiters: ['[[', ']]'],
@@ -124,9 +125,19 @@
       user: {},
       lang: LanguageManager.getLanguage(),
       inboundOptions: [],
-      remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },
+      remarkModels: {
+        i: 'Inbound',
+        e: 'Email',
+        o: 'Other'
+      },
       remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
-      datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],
+      datepickerList: [{
+        name: 'Gregorian (Standard)',
+        value: 'gregorian'
+      }, {
+        name: 'Jalalian (شمسی)',
+        value: 'jalalian'
+      }],
       remarkSample: '',
       defaultFragment: {
         packets: "tlshello",
@@ -134,17 +145,19 @@
         interval: "10-20",
         maxSplit: "300-400"
       },
-      defaultNoises: [
-        { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" }
-      ],
+      defaultNoises: [{
+        type: "rand",
+        packet: "10-20",
+        delay: "10-16",
+        applyTo: "ip"
+      }],
       defaultMux: {
         enabled: true,
         concurrency: 8,
         xudpConcurrency: 16,
         xudpProxyUDP443: "reject"
       },
-      defaultRules: [
-        {
+      defaultRules: [{
           type: "field",
           outboundTag: "direct",
           domain: [
@@ -160,26 +173,75 @@
           ]
         },
       ],
-      directIPsOptions: [
-        { label: 'Private IP', value: 'geoip:private' },
-        { label: '🇮🇷 Iran', value: 'geoip:ir' },
-        { label: '🇨🇳 China', value: 'geoip:cn' },
-        { label: '🇷🇺 Russia', value: 'geoip:ru' },
-        { label: '🇻🇳 Vietnam', value: 'geoip:vn' },
-        { label: '🇪🇸 Spain', value: 'geoip:es' },
-        { label: '🇮🇩 Indonesia', value: 'geoip:id' },
-        { label: '🇺🇦 Ukraine', value: 'geoip:ua' },
-        { label: '🇹🇷 Türkiye', value: 'geoip:tr' },
-        { label: '🇧🇷 Brazil', value: 'geoip:br' },
+      directIPsOptions: [{
+          label: 'Private IP',
+          value: 'geoip:private'
+        },
+        {
+          label: '🇮🇷 Iran',
+          value: 'geoip:ir'
+        },
+        {
+          label: '🇨🇳 China',
+          value: 'geoip:cn'
+        },
+        {
+          label: '🇷🇺 Russia',
+          value: 'geoip:ru'
+        },
+        {
+          label: '🇻🇳 Vietnam',
+          value: 'geoip:vn'
+        },
+        {
+          label: '🇪🇸 Spain',
+          value: 'geoip:es'
+        },
+        {
+          label: '🇮🇩 Indonesia',
+          value: 'geoip:id'
+        },
+        {
+          label: '🇺🇦 Ukraine',
+          value: 'geoip:ua'
+        },
+        {
+          label: '🇹🇷 Türkiye',
+          value: 'geoip:tr'
+        },
+        {
+          label: '🇧🇷 Brazil',
+          value: 'geoip:br'
+        },
       ],
-      diretDomainsOptions: [
-        { label: 'Private DNS', value: 'geosite:private' },
-        { label: '🇮🇷 Iran', value: 'geosite:category-ir' },
-        { label: '🇨🇳 China', value: 'geosite:cn' },
-        { label: '🇷🇺 Russia', value: 'geosite:category-ru' },
-        { label: 'Apple', value: 'geosite:apple' },
-        { label: 'Meta', value: 'geosite:meta' },
-        { label: 'Google', value: 'geosite:google' },
+      diretDomainsOptions: [{
+          label: 'Private DNS',
+          value: 'geosite:private'
+        },
+        {
+          label: '🇮🇷 Iran',
+          value: 'geosite:category-ir'
+        },
+        {
+          label: '🇨🇳 China',
+          value: 'geosite:cn'
+        },
+        {
+          label: '🇷🇺 Russia',
+          value: 'geosite:category-ru'
+        },
+        {
+          label: 'Apple',
+          value: 'geosite:apple'
+        },
+        {
+          label: 'Meta',
+          value: 'geosite:meta'
+        },
+        {
+          label: 'Google',
+          value: 'geosite:google'
+        },
       ],
       get remarkModel() {
         rm = this.allSetting.remarkModel;
@@ -317,7 +379,13 @@
         this.loading(true);
         await PromiseUtil.sleep(5000);
 
-        const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting;
+        const {
+          webDomain,
+          webPort,
+          webBasePath,
+          webCertFile,
+          webKeyFile
+        } = this.allSetting;
         const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:";
 
         let base = webBasePath ? webBasePath.replace(/^\//, "") : "";
@@ -358,7 +426,8 @@
             type: 'set',
             confirm: (success) => {
               if (success) {
-                Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
+                Vue.prototype.$message['success'](
+                  '{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
 
                 this.allSetting.twoFactorToken = newTwoFactorToken
               }
@@ -374,7 +443,8 @@
             type: 'confirm',
             confirm: (success) => {
               if (success) {
-                Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
+                Vue.prototype.$message['success'](
+                  '{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
 
                 this.allSetting.twoFactorEnable = false
                 this.allSetting.twoFactorToken = ""
@@ -384,7 +454,12 @@
         }
       },
       addNoise() {
-        const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
+        const newNoise = {
+          type: "rand",
+          packet: "10-20",
+          delay: "10-16",
+          applyTo: "ip"
+        };
         this.noisesArray = [...this.noisesArray, newNoise];
       },
       removeNoise(index) {
@@ -394,44 +469,60 @@
       },
       updateNoiseType(index, value) {
         const updatedNoises = [...this.noisesArray];
-        updatedNoises[index] = { ...updatedNoises[index], type: value };
+        updatedNoises[index] = {
+          ...updatedNoises[index],
+          type: value
+        };
         this.noisesArray = updatedNoises;
       },
       updateNoisePacket(index, value) {
         const updatedNoises = [...this.noisesArray];
-        updatedNoises[index] = { ...updatedNoises[index], packet: value };
+        updatedNoises[index] = {
+          ...updatedNoises[index],
+          packet: value
+        };
         this.noisesArray = updatedNoises;
       },
       updateNoiseDelay(index, value) {
         const updatedNoises = [...this.noisesArray];
-        updatedNoises[index] = { ...updatedNoises[index], delay: value };
+        updatedNoises[index] = {
+          ...updatedNoises[index],
+          delay: value
+        };
         this.noisesArray = updatedNoises;
       },
       updateNoiseApplyTo(index, value) {
         const updatedNoises = [...this.noisesArray];
-        updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
+        updatedNoises[index] = {
+          ...updatedNoises[index],
+          applyTo: value
+        };
         this.noisesArray = updatedNoises;
       },
     },
     computed: {
       ldapInboundTagList: {
-        get: function () {
+        get: function() {
           const csv = this.allSetting.ldapInboundTags || "";
           return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
         },
-        set: function (list) {
+        set: function(list) {
           this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
         }
       },
       fragment: {
-        get: function () { return this.allSetting?.subJsonFragment != ""; },
-        set: function (v) {
+        get: function() {
+          return this.allSetting?.subJsonFragment != "";
+        },
+        set: function(v) {
           this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
         }
       },
       fragmentPackets: {
-        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).packets : ""; },
-        set: function (v) {
+        get: function() {
+          return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).packets : "";
+        },
+        set: function(v) {
           if (v != "") {
             newFragment = JSON.parse(this.allSetting.subJsonFragment);
             newFragment.packets = v;
@@ -440,8 +531,10 @@
         }
       },
       fragmentLength: {
-        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).length : ""; },
-        set: function (v) {
+        get: function() {
+          return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).length : "";
+        },
+        set: function(v) {
           if (v != "") {
             newFragment = JSON.parse(this.allSetting.subJsonFragment);
             newFragment.length = v;
@@ -450,8 +543,10 @@
         }
       },
       fragmentInterval: {
-        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).interval : ""; },
-        set: function (v) {
+        get: function() {
+          return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).interval : "";
+        },
+        set: function(v) {
           if (v != "") {
             newFragment = JSON.parse(this.allSetting.subJsonFragment);
             newFragment.interval = v;
@@ -460,8 +555,10 @@
         }
       },
       fragmentMaxSplit: {
-        get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).maxSplit : ""; },
-        set: function (v) {
+        get: function() {
+          return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).maxSplit : "";
+        },
+        set: function(v) {
           if (v != "") {
             newFragment = JSON.parse(this.allSetting.subJsonFragment);
             newFragment.maxSplit = v;
@@ -492,50 +589,60 @@
         }
       },
       enableMux: {
-        get: function () { return this.allSetting?.subJsonMux != ""; },
-        set: function (v) {
+        get: function() {
+          return this.allSetting?.subJsonMux != "";
+        },
+        set: function(v) {
           this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : "";
         }
       },
       muxConcurrency: {
-        get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; },
-        set: function (v) {
+        get: function() {
+          return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1;
+        },
+        set: function(v) {
           newMux = JSON.parse(this.allSetting.subJsonMux);
           newMux.concurrency = v;
           this.allSetting.subJsonMux = JSON.stringify(newMux);
         }
       },
       muxXudpConcurrency: {
-        get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; },
-        set: function (v) {
+        get: function() {
+          return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1;
+        },
+        set: function(v) {
           newMux = JSON.parse(this.allSetting.subJsonMux);
           newMux.xudpConcurrency = v;
           this.allSetting.subJsonMux = JSON.stringify(newMux);
         }
       },
       muxXudpProxyUDP443: {
-        get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject"; },
-        set: function (v) {
+        get: function() {
+          return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject";
+        },
+        set: function(v) {
           newMux = JSON.parse(this.allSetting.subJsonMux);
           newMux.xudpProxyUDP443 = v;
           this.allSetting.subJsonMux = JSON.stringify(newMux);
         }
       },
       enableDirect: {
-        get: function () { return this.allSetting?.subJsonRules != ""; },
-        set: function (v) {
+        get: function() {
+          return this.allSetting?.subJsonRules != "";
+        },
+        set: function(v) {
           this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
         }
       },
       directIPs: {
-        get: function () {
+        get: function() {
           if (!this.enableDirect) return [];
           const rules = JSON.parse(this.allSetting.subJsonRules);
           if (!Array.isArray(rules)) return [];
           const ipRule = rules.find(r => r.ip);
           return ipRule?.ip ?? [];
         },
-        set: function (v) {
+        set: function(v) {
           let rules = JSON.parse(this.allSetting.subJsonRules);
           if (!Array.isArray(rules)) return;
 
@@ -554,14 +661,14 @@
         }
       },
       directDomains: {
-        get: function () {
+        get: function() {
           if (!this.enableDirect) return [];
           const rules = JSON.parse(this.allSetting.subJsonRules);
           if (!Array.isArray(rules)) return [];
           const domainRule = rules.find(r => r.domain);
           return domainRule?.domain ?? [];
         },
-        set: function (v) {
+        set: function(v) {
           let rules = JSON.parse(this.allSetting.subJsonRules);
           if (!Array.isArray(rules)) return;
           if (v.length == 0) {
@@ -576,7 +683,7 @@
         }
       },
       confAlerts: {
-        get: function () {
+        get: function() {
           if (!this.allSetting) return [];
           var alerts = []
           if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}');
@@ -584,11 +691,13 @@
           panelPath = window.location.pathname.split('/').length < 4
           if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
           if (this.allSetting.subEnable) {
-            subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
+            subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this
+              .allSetting.subPath;
             if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
           }
           if (this.allSetting.subJsonEnable) {
-            subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
+            subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname :
+              this.allSetting.subJsonPath;
             if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
           }
           return alerts

+ 14 - 7
web/html/settings/panel/general.html

@@ -162,7 +162,8 @@
         <a-setting-list-item paddings="small">
             <template #title>LDAP Port</template>
             <template #control>
-                <a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort" :style="{ width: '100%' }"></a-input-number>
+                <a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort"
+                    :style="{ width: '100%' }"></a-input-number>
             </template>
         </a-setting-list-item>
         <a-setting-list-item paddings="small">
@@ -239,10 +240,13 @@
             <template #title>Inbound tags</template>
             <template #description>Select inbounds to manage (auto create/delete)</template>
             <template #control>
-                <a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }" v-model="ldapInboundTagList">
-                    <a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label ]]</a-select-option>
+                <a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }"
+                    v-model="ldapInboundTagList">
+                    <a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label
+                        ]]</a-select-option>
                 </a-select>
-                <div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create one in Inbounds.</div>
+                <div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create
+                    one in Inbounds.</div>
             </template>
         </a-setting-list-item>
         <a-setting-list-item paddings="small">
@@ -260,19 +264,22 @@
         <a-setting-list-item paddings="small">
             <template #title>Default total (GB)</template>
             <template #control>
-                <a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB" :style="{ width: '100%' }"></a-input-number>
+                <a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB"
+                    :style="{ width: '100%' }"></a-input-number>
             </template>
         </a-setting-list-item>
         <a-setting-list-item paddings="small">
             <template #title>Default expiry (days)</template>
             <template #control>
-                <a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays" :style="{ width: '100%' }"></a-input-number>
+                <a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays"
+                    :style="{ width: '100%' }"></a-input-number>
             </template>
         </a-setting-list-item>
         <a-setting-list-item paddings="small">
             <template #title>Default Limit IP</template>
             <template #control>
-                <a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP" :style="{ width: '100%' }"></a-input-number>
+                <a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP"
+                    :style="{ width: '100%' }"></a-input-number>
             </template>
         </a-setting-list-item>
     </a-collapse-panel>

+ 6 - 12
web/html/settings/panel/subscription/general.html

@@ -46,8 +46,7 @@
             <template #description>{{ i18n
                 "pages.settings.subPortDesc"}}</template>
             <template #control>
-                <a-input-number v-model="allSetting.subPort" :min="1"
-                    :min="65535"
+                <a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
                     :style="{ width: '100%' }"></a-input-number>
             </template>
         </a-setting-list-item>
@@ -67,8 +66,7 @@
             <template #description>{{ i18n
                 "pages.settings.subURIDesc"}}</template>
             <template #control>
-                <a-input type="text"
-                    placeholder="(http|https)://domain[:port]/path/"
+                <a-input type="text" placeholder="(http|https)://domain[:port]/path/"
                     v-model="allSetting.subURI"></a-input>
             </template>
         </a-setting-list-item>
@@ -104,8 +102,7 @@
             <template #description>{{ i18n
                 "pages.settings.subSupportUrlDesc"}}</template>
             <template #control>
-                <a-input type="text" v-model="allSetting.subSupportUrl"
-                    placeholder="https://example.com"></a-input>
+                <a-input type="text" v-model="allSetting.subSupportUrl" placeholder="https://example.com"></a-input>
             </template>
         </a-setting-list-item>
         <a-setting-list-item paddings="small">
@@ -113,8 +110,7 @@
             <template #description>{{ i18n
                 "pages.settings.subProfileUrlDesc"}}</template>
             <template #control>
-                <a-input type="text" v-model="allSetting.subProfileUrl"
-                    placeholder="https://example.com"></a-input>
+                <a-input type="text" v-model="allSetting.subProfileUrl" placeholder="https://example.com"></a-input>
             </template>
         </a-setting-list-item>
         <a-setting-list-item paddings="small">
@@ -141,8 +137,7 @@
             <template #description>{{ i18n
                 "pages.settings.subRoutingRulesDesc"}}</template>
             <template #control>
-                <a-textarea v-model="allSetting.subRoutingRules"
-                    placeholder="happ://routing/add/..."></a-textarea>
+                <a-textarea v-model="allSetting.subRoutingRules" placeholder="happ://routing/add/..."></a-textarea>
             </template>
         </a-setting-list-item>
     </a-collapse-panel>
@@ -170,8 +165,7 @@
             <template #description>{{ i18n
                 "pages.settings.subUpdatesDesc"}}</template>
             <template #control>
-                <a-input-number :min="1" v-model="allSetting.subUpdates"
-                    :style="{ width: '100%' }"></a-input-number>
+                <a-input-number :min="1" v-model="allSetting.subUpdates" :style="{ width: '100%' }"></a-input-number>
             </template>
         </a-setting-list-item>
     </a-collapse-panel>

+ 7 - 6
web/html/settings/panel/subscription/subpage.html

@@ -100,7 +100,8 @@
                         <a-form-item>
                             <a-space direction="vertical" align="center">
                                 <a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%">
-                                    <a-col :xs="24" :sm="app.subJsonUrl || app.subClashUrl ? 12 : 24" style="text-align:center;">
+                                    <a-col :xs="24" :sm="app.subJsonUrl || app.subClashUrl ? 12 : 24"
+                                        style="text-align:center;">
                                         <tr-qr-box class="qr-box">
                                             <a-tag color="purple" class="qr-tag">
                                                 <span>{{ i18n
@@ -270,11 +271,11 @@
 </a-layout>
 
 <!-- Bootstrap data for external JS -->
-<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}" data-subclash-url="{{ .subClashUrl }}"
-    data-download="{{ .download }}" data-upload="{{ .upload }}" data-used="{{ .used }}" data-total="{{ .total }}"
-    data-remained="{{ .remained }}" data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
-    data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
-    data-datepicker="{{ .datepicker }}"></template>
+<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
+    data-subclash-url="{{ .subClashUrl }}" data-download="{{ .download }}" data-upload="{{ .upload }}"
+    data-used="{{ .used }}" data-total="{{ .total }}" data-remained="{{ .remained }}" data-expire="{{ .expire }}"
+    data-lastonline="{{ .lastOnline }}" data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}"
+    data-totalbyte="{{ .totalByte }}" data-datepicker="{{ .datepicker }}"></template>
 <textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }}
 {{ end }}</textarea>
 

+ 5 - 3
web/html/settings/xray/balancers.html

@@ -5,7 +5,8 @@
             <span>{{ i18n "pages.xray.balancer.addBalancer"}}</span>
         </a-button>
         <a-table :columns="balancerColumns" bordered :row-key="r => r.key" :data-source="balancersData"
-            :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0" :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
+            :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
+            :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
             <template slot="action" slot-scope="text, balancer, index">
                 <span>[[ index+1 ]]</span>
                 <a-dropdown :trigger="['click']">
@@ -18,7 +19,7 @@
                         </a-menu-item>
                         <a-menu-item @click="deleteBalancer(index)">
                             <span :style="{ color: '#FF4D4F' }">
-                                <a-icon type="delete"></a-icon> 
+                                <a-icon type="delete"></a-icon>
                                 <span>{{ i18n "delete"}}</span>
                             </span>
                         </a-menu-item>
@@ -32,7 +33,8 @@
                 <a-tag :style="{ margin: '0' }" v-if="balancer.strategy=='leastPing'" color="green">Least Ping</a-tag>
             </template>
             <template slot="selector" slot-scope="text, balancer, index">
-                <a-tag class="info-large-tag" :style="{ margin: '1' }" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
+                <a-tag class="info-large-tag" :style="{ margin: '1' }" v-for="sel in balancer.selector">[[ sel
+                    ]]</a-tag>
             </template>
         </a-table>
         <a-radio-group v-if="observatoryEnable || burstObservatoryEnable" v-model="obsSettings" @change="changeObsCode"

+ 35 - 70
web/html/settings/xray/basics.html

@@ -4,8 +4,7 @@
         <a-row :xs="24" :sm="24" :lg="12">
             <a-alert type="warning" :style="{ textAlign: 'center' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     <span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
                 </template>
             </a-alert>
@@ -15,11 +14,9 @@
             <template #description>{{ i18n "pages.xray.FreedomStrategyDesc"
                 }}</template>
             <template #control>
-                <a-select v-model="freedomStrategy"
-                    :dropdown-class-name="themeSwitcher.currentTheme"
+                <a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
                     :style="{ width: '100%' }">
-                    <a-select-option v-for="s in OutboundDomainStrategies"
-                        :value="s">
+                    <a-select-option v-for="s in OutboundDomainStrategies" :value="s">
                         <span>[[ s ]]</span>
                     </a-select-option>
                 </a-select>
@@ -30,11 +27,9 @@
             <template #description>{{ i18n "pages.xray.RoutingStrategyDesc"
                 }}</template>
             <template #control>
-                <a-select v-model="routingStrategy"
-                    :dropdown-class-name="themeSwitcher.currentTheme"
+                <a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
                     :style="{ width: '100%' }">
-                    <a-select-option v-for="s in routingDomainStrategies"
-                        :value="s">
+                    <a-select-option v-for="s in routingDomainStrategies" :value="s">
                         <span>[[ s ]]</span>
                     </a-select-option>
                 </a-select>
@@ -45,8 +40,7 @@
             <template #description>{{ i18n "pages.xray.outboundTestUrlDesc"
                 }}</template>
             <template #control>
-                <a-input v-model="outboundTestUrl"
-                    :placeholder="'https://www.google.com/generate_204'"
+                <a-input v-model="outboundTestUrl" :placeholder="'https://www.google.com/generate_204'"
                     :style="{ width: '100%' }"></a-input>
             </template>
         </a-setting-list-item>
@@ -93,8 +87,7 @@
         <a-row :xs="24" :sm="24" :lg="12">
             <a-alert type="warning" :style="{ textAlign: 'center' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     <span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
                 </template>
             </a-alert>
@@ -104,8 +97,7 @@
             <template #description>{{ i18n "pages.xray.logLevelDesc"
                 }}</template>
             <template #control>
-                <a-select v-model="logLevel"
-                    :dropdown-class-name="themeSwitcher.currentTheme"
+                <a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme"
                     :style="{ width: '100%' }">
                     <a-select-option v-for="s in log.loglevel" :value="s">
                         <span>[[ s ]]</span>
@@ -118,8 +110,7 @@
             <template #description>{{ i18n "pages.xray.accessLogDesc"
                 }}</template>
             <template #control>
-                <a-select v-model="accessLog"
-                    :dropdown-class-name="themeSwitcher.currentTheme"
+                <a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme"
                     :style="{ width: '100%' }">
                     <a-select-option value>
                         <span>Empty</span>
@@ -135,8 +126,7 @@
             <template #description>{{ i18n "pages.xray.errorLogDesc"
                 }}</template>
             <template #control>
-                <a-select v-model="errorLog"
-                    :dropdown-class-name="themeSwitcher.currentTheme"
+                <a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme"
                     :style="{ width: '100%' }">
                     <a-select-option value>
                         <span>Empty</span>
@@ -152,8 +142,7 @@
             <template #description>{{ i18n "pages.xray.maskAddressDesc"
                 }}</template>
             <template #control>
-                <a-select v-model="maskAddressLog"
-                    :dropdown-class-name="themeSwitcher.currentTheme"
+                <a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme"
                     :style="{ width: '100%' }">
                     <a-select-option value>
                         <span>Empty</span>
@@ -176,8 +165,7 @@
         <a-row :xs="24" :sm="24" :lg="12">
             <a-alert type="warning" :style="{ textAlign: 'center' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     <span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
                 </template>
             </a-alert>
@@ -191,8 +179,7 @@
         <a-row :xs="24" :sm="24" :lg="12">
             <a-alert type="warning" :style="{ textAlign: 'center' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     <span>{{ i18n "pages.xray.blockConnectionsConfigsDesc"
                         }}</span>
                 </template>
@@ -201,11 +188,9 @@
         <a-setting-list-item paddings="small">
             <template #title>{{ i18n "pages.xray.blockips" }}</template>
             <template #control>
-                <a-select mode="tags" v-model="blockedIPs"
-                    :style="{ width: '100%' }"
+                <a-select mode="tags" v-model="blockedIPs" :style="{ width: '100%' }"
                     :dropdown-class-name="themeSwitcher.currentTheme">
-                    <a-select-option :value="p.value" :label="p.label"
-                        v-for="p in settingsData.IPsOptions">
+                    <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
                         <span>[[ p.label ]]</span>
                     </a-select-option>
                 </a-select>
@@ -214,22 +199,18 @@
         <a-setting-list-item paddings="small">
             <template #title>{{ i18n "pages.xray.blockdomains" }}</template>
             <template #control>
-                <a-select mode="tags" v-model="blockedDomains"
-                    :style="{ width: '100%' }"
+                <a-select mode="tags" v-model="blockedDomains" :style="{ width: '100%' }"
                     :dropdown-class-name="themeSwitcher.currentTheme">
-                    <a-select-option :value="p.value" :label="p.label"
-                        v-for="p in settingsData.BlockDomainsOptions">
+                    <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.BlockDomainsOptions">
                         <span>[[ p.label ]]</span>
                     </a-select-option>
                 </a-select>
             </template>
         </a-setting-list-item>
         <a-row :xs="24" :sm="24" :lg="12">
-            <a-alert type="warning"
-                :style="{ textAlign: 'center', marginTop: '20px' }">
+            <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     <span>{{ i18n "pages.xray.directConnectionsConfigsDesc"
                         }}</span>
                 </template>
@@ -238,11 +219,9 @@
         <a-setting-list-item paddings="small">
             <template #title>{{ i18n "pages.xray.directips" }}</template>
             <template #control>
-                <a-select mode="tags" :style="{ width: '100%' }"
-                    v-model="directIPs"
+                <a-select mode="tags" :style="{ width: '100%' }" v-model="directIPs"
                     :dropdown-class-name="themeSwitcher.currentTheme">
-                    <a-select-option :value="p.value" :label="p.label"
-                        v-for="p in settingsData.IPsOptions">
+                    <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
                         <span>[[ p.label ]]</span>
                     </a-select-option>
                 </a-select>
@@ -251,22 +230,18 @@
         <a-setting-list-item paddings="small">
             <template #title>{{ i18n "pages.xray.directdomains" }}</template>
             <template #control>
-                <a-select mode="tags" :style="{ width: '100%' }"
-                    v-model="directDomains"
+                <a-select mode="tags" :style="{ width: '100%' }" v-model="directDomains"
                     :dropdown-class-name="themeSwitcher.currentTheme">
-                    <a-select-option :value="p.value" :label="p.label"
-                        v-for="p in settingsData.DomainsOptions">
+                    <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.DomainsOptions">
                         <span>[[ p.label ]]</span>
                     </a-select-option>
                 </a-select>
             </template>
         </a-setting-list-item>
         <a-row :xs="24" :sm="24" :lg="12">
-            <a-alert type="warning"
-                :style="{ textAlign: 'center', marginTop: '20px' }">
+            <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     <span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
                 </template>
             </a-alert>
@@ -274,22 +249,18 @@
         <a-setting-list-item paddings="small">
             <template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
             <template #control>
-                <a-select mode="tags" :style="{ width: '100%' }"
-                    v-model="ipv4Domains"
+                <a-select mode="tags" :style="{ width: '100%' }" v-model="ipv4Domains"
                     :dropdown-class-name="themeSwitcher.currentTheme">
-                    <a-select-option :value="p.value" :label="p.label"
-                        v-for="p in settingsData.ServicesOptions">
+                    <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
                         <span>[[ p.label ]]</span>
                     </a-select-option>
                 </a-select>
             </template>
         </a-setting-list-item>
         <a-row :xs="24" :sm="24" :lg="12">
-            <a-alert type="warning"
-                :style="{ textAlign: 'center', marginTop: '20px' }">
+            <a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
                 <template slot="message">
-                    <a-icon type="exclamation-circle" theme="filled"
-                        :style="{ color: '#FFA031' }"></a-icon>
+                    <a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
                     {{ i18n "pages.xray.warpRoutingDesc" }}
                 </template>
             </a-alert>
@@ -298,18 +269,15 @@
             <template #title>{{ i18n "pages.xray.warpRouting" }}</template>
             <template #control>
                 <template v-if="WarpExist">
-                    <a-select mode="tags" :style="{ width: '100%' }"
-                        v-model="warpDomains"
+                    <a-select mode="tags" :style="{ width: '100%' }" v-model="warpDomains"
                         :dropdown-class-name="themeSwitcher.currentTheme">
-                        <a-select-option :value="p.value" :label="p.label"
-                            v-for="p in settingsData.ServicesOptions">
+                        <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
                             <span>[[ p.label ]]</span>
                         </a-select-option>
                     </a-select>
                 </template>
                 <template v-else>
-                    <a-button type="primary" icon="cloud"
-                        @click="showWarp()">WARP</a-button>
+                    <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
                 </template>
             </template>
         </a-setting-list-item>
@@ -317,11 +285,9 @@
             <template #title>{{ i18n "pages.xray.nordRouting" }}</template>
             <template #control>
                 <template v-if="NordExist">
-                    <a-select mode="tags" :style="{ width: '100%' }"
-                        v-model="nordDomains"
+                    <a-select mode="tags" :style="{ width: '100%' }" v-model="nordDomains"
                         :dropdown-class-name="themeSwitcher.currentTheme">
-                        <a-select-option :value="p.value" :label="p.label"
-                            v-for="p in settingsData.ServicesOptions">
+                        <a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
                             <span>[[ p.label ]]</span>
                         </a-select-option>
                     </a-select>
@@ -333,8 +299,7 @@
             </template>
         </a-setting-list-item>
     </a-collapse-panel>
-    <a-collapse-panel key="6"
-        header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
+    <a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
         <a-space direction="horizontal" :style="{ padding: '0 20px' }">
             <a-button type="danger" @click="resetXrayConfigToDefault">
                 <span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>

+ 2 - 1
web/html/settings/xray/dns.html

@@ -29,7 +29,8 @@
                 <template #control>
                     <a-select v-model="dnsStrategy" :style="{ width: '100%' }"
                         :dropdown-class-name="themeSwitcher.currentTheme">
-                        <a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']">
+                        <a-select-option :value="l" :label="l"
+                            v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']">
                             <span>[[ l ]]</span>
                         </a-select-option>
                     </a-select>

+ 16 - 35
web/html/settings/xray/outbounds.html

@@ -7,21 +7,16 @@
                     <span v-if="!isMobile">{{ i18n
                         "pages.xray.outbound.addOutbound" }}</span>
                 </a-button>
-                <a-button type="primary" icon="cloud"
-                    @click="showWarp()">WARP</a-button>
-                <a-button type="primary" icon="api"
-                    @click="showNord()">NordVPN</a-button>
+                <a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
+                <a-button type="primary" icon="api" @click="showNord()">NordVPN</a-button>
             </a-space>
         </a-col>
         <a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
             <a-button-group>
-                <a-button icon="sync" @click="refreshOutboundTraffic()"
-                    :loading="refreshing"></a-button>
-                <a-popconfirm placement="topRight"
-                    @confirm="resetOutboundTraffic(-1)"
+                <a-button icon="sync" @click="refreshOutboundTraffic()" :loading="refreshing"></a-button>
+                <a-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
                     title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
-                    :overlay-class-name="themeSwitcher.currentTheme"
-                    ok-text='{{ i18n "reset"}}'
+                    :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}'
                     cancel-text='{{ i18n "cancel"}}'>
                     <a-icon slot="icon" type="question-circle-o"
                         :style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
@@ -30,10 +25,8 @@
             </a-button-group>
         </a-col>
     </a-row>
-    <a-table :columns="outboundColumns" bordered :row-key="r => r.key"
-        :data-source="outboundData"
-        :scroll="isMobile ? {} : { x: 800 }" :pagination="false"
-        :indent-size="0"
+    <a-table :columns="outboundColumns" bordered :row-key="r => r.key" :data-source="outboundData"
+        :scroll="isMobile ? {} : { x: 800 }" :pagination="false" :indent-size="0"
         :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
         <template slot="action" slot-scope="text, outbound, index">
             <span>[[ index+1 ]]</span>
@@ -41,8 +34,7 @@
                 <a-icon @click="e => e.preventDefault()" type="more"
                     :style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
                 <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
-                    <a-menu-item v-if="index>0"
-                        @click="setFirstOutbound(index)">
+                    <a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
                         <a-icon type="vertical-align-top"></a-icon>
                         <span>{{ i18n "pages.xray.rules.first"}}</span>
                     </a-menu-item>
@@ -66,8 +58,7 @@
             </a-dropdown>
         </template>
         <template slot="address" slot-scope="text, outbound, index">
-            <p :style="{ margin: '0 5px' }"
-                v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
+            <p :style="{ margin: '0 5px' }" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
         </template>
         <template slot="protocol" slot-scope="text, outbound, index">
             <a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol
@@ -76,11 +67,8 @@
                 v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
                 <a-tag :style="{ margin: '0' }" color="blue">[[
                     outbound.streamSettings.network ]]</a-tag>
-                <a-tag :style="{ margin: '0' }"
-                    v-if="outbound.streamSettings.security=='tls'"
-                    color="green">tls</a-tag>
-                <a-tag :style="{ margin: '0' }"
-                    v-if="outbound.streamSettings.security=='reality'"
+                <a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
+                <a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='reality'"
                     color="green">reality</a-tag>
             </template>
         </template>
@@ -91,10 +79,7 @@
             <a-tooltip>
                 <template slot="title">{{ i18n "pages.xray.outbound.test"
                     }}</template>
-                <a-button
-                    type="primary"
-                    shape="circle"
-                    icon="thunderbolt"
+                <a-button type="primary" shape="circle" icon="thunderbolt"
                     :loading="outboundTestStates[index] && outboundTestStates[index].testing"
                     @click="testOutbound(index)"
                     :disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)">
@@ -102,24 +87,20 @@
             </a-tooltip>
         </template>
         <template slot="testResult" slot-scope="text, outbound, index">
-            <div
-                v-if="outboundTestStates[index] && outboundTestStates[index].result">
-                <a-tag v-if="outboundTestStates[index].result.success"
-                    color="green">
+            <div v-if="outboundTestStates[index] && outboundTestStates[index].result">
+                <a-tag v-if="outboundTestStates[index].result.success" color="green">
                     [[ outboundTestStates[index].result.delay ]]ms
                     <span v-if="outboundTestStates[index].result.statusCode">
                         ([[ outboundTestStates[index].result.statusCode
                         ]])</span>
                 </a-tag>
-                <a-tooltip v-else
-                    :title="outboundTestStates[index].result.error">
+                <a-tooltip v-else :title="outboundTestStates[index].result.error">
                     <a-tag color="red">
                         Failed
                     </a-tag>
                 </a-tooltip>
             </div>
-            <span
-                v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
+            <span v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
                 <a-icon type="loading" />
             </span>
             <span v-else>-</span>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 620 - 207
web/html/xray.html


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 351 - 342
x-ui.sh


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů