8 次代码提交 2b83dc047b ... 77d94b25d0

作者 SHA1 备注 提交日期
  MHSanaei 77d94b25d0 Add 'active' filter option to inbounds 1 天之前
  MHSanaei 32b7ada549 subpage: enabled state 1 天之前
  MHSanaei 6099a07ff0 feat: add configurable auto-restart on client auto-disable 1 天之前
  MHSanaei e9806832ec reality: remove apple, icloud 1 天之前
  MHSanaei 15ebf3df10 fix: client count for Hysteria 1 天之前
  MHSanaei d44b70682c Update QUIC params defaults and UI validations 1 天之前
  MHSanaei fb75e3d7c7 Check scanner error in GetXrayLogs 1 天之前
  MHSanaei e9979b6774 API: Check client existence 1 天之前

+ 1 - 1
sub/subClashService.go

@@ -55,7 +55,7 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
 			}
 		}
 		for _, client := range clients {
-			if client.Enable && client.SubID == subId {
+			if client.SubID == subId {
 				clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
 				proxies = append(proxies, s.getProxies(inbound, client, host)...)
 			}

+ 1 - 0
sub/subController.go

@@ -138,6 +138,7 @@ func (a *SUBController) subs(c *gin.Context) {
 				"host":         page.Host,
 				"base_path":    page.BasePath,
 				"sId":          page.SId,
+				"enabled":      page.Enabled,
 				"download":     page.Download,
 				"upload":       page.Upload,
 				"total":        page.Total,

+ 1 - 1
sub/subJsonService.go

@@ -116,7 +116,7 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
 		}
 
 		for _, client := range clients {
-			if client.Enable && client.SubID == subId {
+			if client.SubID == subId {
 				clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
 				newConfigs := s.getConfig(inbound, client, host)
 				configArray = append(configArray, newConfigs...)

+ 8 - 1
sub/subService.go

@@ -46,6 +46,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
 	var result []string
 	var traffic xray.ClientTraffic
 	var lastOnline int64
+	var hasEnabledClient bool
 	var clientTraffics []xray.ClientTraffic
 	inbounds, err := s.getInboundsBySubId(subId)
 	if err != nil {
@@ -77,7 +78,10 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
 			}
 		}
 		for _, client := range clients {
-			if client.Enable && client.SubID == subId {
+			if client.SubID == subId {
+				if client.Enable {
+					hasEnabledClient = true
+				}
 				link := s.getLink(inbound, client.Email)
 				result = append(result, link)
 				ct := s.getClientTraffics(inbound.ClientStats, client.Email)
@@ -111,6 +115,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
 			}
 		}
 	}
+	traffic.Enable = hasEnabledClient
 	return result, lastOnline, traffic, nil
 }
 
@@ -1304,6 +1309,7 @@ type PageData struct {
 	Host         string
 	BasePath     string
 	SId          string
+	Enabled      bool
 	Download     string
 	Upload       string
 	Total        string
@@ -1453,6 +1459,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
 		Host:         hostHeader,
 		BasePath:     basePath,
 		SId:          subId,
+		Enabled:      traffic.Enable,
 		Download:     download,
 		Upload:       upload,
 		Total:        total,

+ 8 - 6
web/assets/js/model/inbound.js

@@ -871,7 +871,7 @@ class RealityStreamSettings extends XrayCommonClass {
         if (!target && !serverNames) {
             const randomTarget = typeof getRandomRealityTarget !== 'undefined'
                 ? getRandomRealityTarget()
-                : { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };
+                : { target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' };
             target = randomTarget.target;
             serverNames = randomTarget.sni;
         }
@@ -1207,15 +1207,15 @@ class QuicParams extends XrayCommonClass {
     constructor(
         congestion = 'bbr',
         debug = false,
-        brutalUp = '',
-        brutalDown = '',
+        brutalUp = 65537,
+        brutalDown = 65537,
         udpHop = undefined,
         initStreamReceiveWindow = 8388608,
         maxStreamReceiveWindow = 8388608,
         initConnectionReceiveWindow = 20971520,
         maxConnectionReceiveWindow = 20971520,
         maxIdleTimeout = 30,
-        keepAlivePeriod = 0,
+        keepAlivePeriod = 5,
         disablePathMTUDiscovery = false,
         maxIncomingStreams = 1024,
     ) {
@@ -1265,8 +1265,10 @@ class QuicParams extends XrayCommonClass {
     toJson() {
         const result = { congestion: this.congestion };
         if (this.debug) result.debug = this.debug;
-        if (this.brutalUp) result.brutalUp = this.brutalUp;
-        if (this.brutalDown) result.brutalDown = this.brutalDown;
+        if (['brutal', 'force-brutal'].includes(this.congestion)) {
+            if (this.brutalUp) result.brutalUp = this.brutalUp;
+            if (this.brutalDown) result.brutalDown = this.brutalDown;
+        }
         if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
         if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
         if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;

+ 41 - 7
web/assets/js/model/outbound.js

@@ -6,10 +6,10 @@ const Protocols = {
     VLESS: "vless",
     Trojan: "trojan",
     Shadowsocks: "shadowsocks",
+    Wireguard: "wireguard",
+    Hysteria: "hysteria",
     Socks: "socks",
     HTTP: "http",
-    Wireguard: "wireguard",
-    Hysteria: "hysteria"
 };
 
 const SSMethods = {
@@ -500,7 +500,7 @@ class HysteriaStreamSettings extends CommonClass {
         initConnectionReceiveWindow = 20971520,
         maxConnectionReceiveWindow = 20971520,
         maxIdleTimeout = 30,
-        keepAlivePeriod = 0,
+        keepAlivePeriod = 2,
         disablePathMTUDiscovery = false
     ) {
         super();
@@ -789,9 +789,17 @@ class QuicParams extends CommonClass {
     constructor(
         congestion = 'bbr',
         debug = false,
-        brutalUp = '',
-        brutalDown = '',
+        brutalUp = 65537,
+        brutalDown = 65537,
         udpHop = undefined,
+        initStreamReceiveWindow = 8388608,
+        maxStreamReceiveWindow = 8388608,
+        initConnectionReceiveWindow = 20971520,
+        maxConnectionReceiveWindow = 20971520,
+        maxIdleTimeout = 30,
+        keepAlivePeriod = 5,
+        disablePathMTUDiscovery = false,
+        maxIncomingStreams = 1024,
     ) {
         super();
         this.congestion = congestion;
@@ -799,6 +807,14 @@ class QuicParams extends CommonClass {
         this.brutalUp = brutalUp;
         this.brutalDown = brutalDown;
         this.udpHop = udpHop;
+        this.initStreamReceiveWindow = initStreamReceiveWindow;
+        this.maxStreamReceiveWindow = maxStreamReceiveWindow;
+        this.initConnectionReceiveWindow = initConnectionReceiveWindow;
+        this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
+        this.maxIdleTimeout = maxIdleTimeout;
+        this.keepAlivePeriod = keepAlivePeriod;
+        this.disablePathMTUDiscovery = disablePathMTUDiscovery;
+        this.maxIncomingStreams = maxIncomingStreams;
     }
 
     get hasUdpHop() {
@@ -817,15 +833,33 @@ class QuicParams extends CommonClass {
             json.brutalUp,
             json.brutalDown,
             json.udpHop ? { ports: json.udpHop.ports, interval: json.udpHop.interval } : undefined,
+            json.initStreamReceiveWindow,
+            json.maxStreamReceiveWindow,
+            json.initConnectionReceiveWindow,
+            json.maxConnectionReceiveWindow,
+            json.maxIdleTimeout,
+            json.keepAlivePeriod,
+            json.disablePathMTUDiscovery,
+            json.maxIncomingStreams,
         );
     }
 
     toJson() {
         const result = { congestion: this.congestion };
         if (this.debug) result.debug = this.debug;
-        if (this.brutalUp) result.brutalUp = this.brutalUp;
-        if (this.brutalDown) result.brutalDown = this.brutalDown;
+        if (['brutal', 'force-brutal'].includes(this.congestion)) {
+            if (this.brutalUp) result.brutalUp = this.brutalUp;
+            if (this.brutalDown) result.brutalDown = this.brutalDown;
+        }
         if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
+        if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
+        if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;
+        if (this.initConnectionReceiveWindow > 0) result.initConnectionReceiveWindow = this.initConnectionReceiveWindow;
+        if (this.maxConnectionReceiveWindow > 0) result.maxConnectionReceiveWindow = this.maxConnectionReceiveWindow;
+        if (this.maxIdleTimeout !== 30 && this.maxIdleTimeout > 0) result.maxIdleTimeout = this.maxIdleTimeout;
+        if (this.keepAlivePeriod > 0) result.keepAlivePeriod = this.keepAlivePeriod;
+        if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery;
+        if (this.maxIncomingStreams > 0) result.maxIncomingStreams = this.maxIncomingStreams;
         return result;
     }
 }

+ 0 - 2
web/assets/js/model/reality_targets.js

@@ -1,7 +1,5 @@
 // List of popular services for VLESS Reality Target/SNI randomization
 const REALITY_TARGETS = [
-    { target: 'www.apple.com:443', sni: 'www.apple.com' },
-    { target: 'www.icloud.com:443', sni: 'www.icloud.com' },
     { target: 'www.amazon.com:443', sni: 'www.amazon.com' },
     { target: 'aws.amazon.com:443', sni: 'aws.amazon.com' },
     { target: 'www.oracle.com:443', sni: 'www.oracle.com' },

+ 1 - 0
web/assets/js/model/setting.js

@@ -43,6 +43,7 @@ class AllSetting {
         this.subDomain = "";
         this.externalTrafficInformEnable = false;
         this.externalTrafficInformURI = "";
+        this.restartXrayOnClientDisable = true;
         this.subCertFile = "";
         this.subKeyFile = "";
         this.subUpdates = 12;

+ 3 - 1
web/assets/js/subscription.js

@@ -7,6 +7,7 @@
 
   const data = {
     sId: el.getAttribute('data-sid') || '',
+    enabled: (el.getAttribute('data-enabled') || '').toLowerCase() === 'true',
     subUrl: el.getAttribute('data-sub-url') || '',
     subJsonUrl: el.getAttribute('data-subjson-url') || '',
     subClashUrl: el.getAttribute('data-subclash-url') || '',
@@ -128,9 +129,10 @@
       },
       isActive() {
         const now = Date.now();
+        const enabledOk = this.app.enabled;
         const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
         const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
-        return expiryOk && trafficOk;
+        return enabledOk && expiryOk && trafficOk;
       },
       shadowrocketUrl() {
         const rawUrl = this.app.subUrl + '?flag=shadowrocket';

+ 1 - 0
web/entity/entity.go

@@ -71,6 +71,7 @@ type AllSetting struct {
 	SubUpdates                  int    `json:"subUpdates" form:"subUpdates"`                                   // Subscription update interval in minutes
 	ExternalTrafficInformEnable bool   `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
 	ExternalTrafficInformURI    string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`       // URI for external traffic reporting
+	RestartXrayOnClientDisable  bool   `json:"restartXrayOnClientDisable" form:"restartXrayOnClientDisable"`   // Restart Xray when clients are auto-disabled by expiry/traffic limit
 	SubEncrypt                  bool   `json:"subEncrypt" form:"subEncrypt"`                                   // Encrypt subscription responses
 	SubShowInfo                 bool   `json:"subShowInfo" form:"subShowInfo"`                                 // Show client information in subscriptions
 	SubURI                      string `json:"subURI" form:"subURI"`                                           // Subscription server URI

+ 17 - 17
web/html/form/outbound.html

@@ -579,23 +579,23 @@
             <a-input-number v-model.number="outbound.stream.hysteria.udphopIntervalMax" :min="5"></a-input-number>
           </a-form-item>
           <a-form-item label="Init Stream Receive">
-            <a-input-number v-model.number="outbound.stream.hysteria.initStreamReceiveWindow"></a-input-number>
+            <a-input-number v-model.number="outbound.stream.hysteria.initStreamReceiveWindow" :min="16384"></a-input-number>
           </a-form-item>
           <a-form-item label="Max Stream Receive">
-            <a-input-number v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow"></a-input-number>
+            <a-input-number v-model.number="outbound.stream.hysteria.maxStreamReceiveWindow" :min="16384"></a-input-number>
           </a-form-item>
           <a-form-item label="Init Connection Receive">
-            <a-input-number v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow"></a-input-number>
+            <a-input-number v-model.number="outbound.stream.hysteria.initConnectionReceiveWindow" :min="16384"></a-input-number>
           </a-form-item>
           <a-form-item label="Max Connection Receive">
-            <a-input-number v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow"></a-input-number>
+            <a-input-number v-model.number="outbound.stream.hysteria.maxConnectionReceiveWindow" :min="16384"></a-input-number>
           </a-form-item>
           <a-form-item label="Max Idle Timeout (s)">
             <a-input-number v-model.number="outbound.stream.hysteria.maxIdleTimeout" :min="4"
               :max="120"></a-input-number>
           </a-form-item>
           <a-form-item label="Keep Alive Period (s)">
-            <a-input-number v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="0"
+            <a-input-number v-model.number="outbound.stream.hysteria.keepAlivePeriod" :min="2"
               :max="60"></a-input-number>
           </a-form-item>
           <a-form-item label="Disable Path MTU Dis">
@@ -934,10 +934,10 @@
             </a-form-item>
             <template v-if="['brutal','force-brutal'].includes(outbound.stream.finalmask.quicParams.congestion)">
               <a-form-item label="Brutal Up">
-                <a-input v-model.trim="outbound.stream.finalmask.quicParams.brutalUp" placeholder="e.g. 60 mbps" />
+                <a-input v-model.trim="outbound.stream.finalmask.quicParams.brutalUp" :min="65537"placeholder="65537" />
               </a-form-item>
               <a-form-item label="Brutal Down">
-                <a-input v-model.trim="outbound.stream.finalmask.quicParams.brutalDown" placeholder="e.g. 60 mbps" />
+                <a-input v-model.trim="outbound.stream.finalmask.quicParams.brutalDown" :min="65537" placeholder="65537" />
               </a-form-item>
             </template>
             <a-form-item label="UDP Hop">
@@ -964,24 +964,24 @@
               <a-switch v-model="outbound.stream.finalmask.quicParams.disablePathMTUDiscovery"></a-switch>
             </a-form-item>
             <a-form-item label="Max Incoming Streams">
-              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.maxIncomingStreams" :min="0"
-                placeholder="0 = default" />
+              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.maxIncomingStreams" :min="8"
+                placeholder="8 = default" />
             </a-form-item>
             <a-form-item label="Init Stream Window">
-              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="0"
-                placeholder="0 = default" />
+              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="16384"
+                placeholder="8388608 = default" />
             </a-form-item>
             <a-form-item label="Max Stream Window">
-              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="0"
-                placeholder="0 = default" />
+              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="16384"
+                placeholder="8388608 = default" />
             </a-form-item>
             <a-form-item label="Init Conn Window">
-              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="0"
-                placeholder="0 = default" />
+              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="16384"
+                placeholder="20971520 = default" />
             </a-form-item>
             <a-form-item label="Max Conn Window">
-              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="0"
-                placeholder="0 = default" />
+              <a-input-number v-model.number="outbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="16384"
+                placeholder="20971520 = default" />
             </a-form-item>
           </template>
         </template>

+ 13 - 13
web/html/form/stream/stream_finalmask.html

@@ -356,10 +356,10 @@
       </a-form-item>
       <template v-if="['brutal','force-brutal'].includes(inbound.stream.finalmask.quicParams.congestion)">
         <a-form-item label="Brutal Up">
-          <a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalUp" placeholder="e.g. 60 mbps" />
+          <a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalUp" :min="65537" placeholder="65537" />
         </a-form-item>
         <a-form-item label="Brutal Down">
-          <a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalDown" placeholder="e.g. 60 mbps" />
+          <a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalDown" :min="65537" placeholder="65537" />
         </a-form-item>
       </template>
       <a-form-item label="UDP Hop">
@@ -377,30 +377,30 @@
         <a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIdleTimeout" :min="4" :max="120" />
       </a-form-item>
       <a-form-item label="Keep Alive Period (s)">
-        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.keepAlivePeriod" :min="0" :max="60" />
+        <a-input-number v-model.number="inbound.stream.finalmask.quicParams.keepAlivePeriod" :min="2" :max="60" />
       </a-form-item>
       <a-form-item label="Disable Path MTU Dis">
         <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="8"
+          placeholder="1024 = 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="16384"
+          placeholder="8388608 = 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="16384"
+          placeholder="8388608 = 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="16384"
+          placeholder="20971520 = 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="16384"
+          placeholder="20971520 = default" />
       </a-form-item>
     </template>
   </template>

+ 3 - 1
web/html/inbounds.html

@@ -184,6 +184,8 @@
                     <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"
                       :size="isMobile ? 'small' : ''">
                       <a-radio-button value>{{ i18n "none" }}</a-radio-button>
+                      <a-radio-button value="active">{{ i18n "subscription.active"
+                        }}</a-radio-button>
                       <a-radio-button value="deactive">{{ i18n "disabled"
                         }}</a-radio-button>
                       <a-radio-button value="depleted">{{ i18n "depleted"
@@ -1169,7 +1171,7 @@
           to_inbound = dbInbound.toInbound()
           this.inbounds.push(to_inbound);
           this.dbInbounds.push(dbInbound);
-          if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound
+          if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA].includes(inbound
             .protocol)) {
             if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
               continue;

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

@@ -124,6 +124,13 @@
                     v-model="allSetting.externalTrafficInformURI"></a-input>
             </template>
         </a-setting-list-item>
+        <a-setting-list-item paddings="small">
+            <template #title>{{ i18n "pages.settings.restartXrayOnClientDisable"}}</template>
+            <template #description>{{ i18n "pages.settings.restartXrayOnClientDisableDesc"}}</template>
+            <template #control>
+                <a-switch v-model="allSetting.restartXrayOnClientDisable"></a-switch>
+            </template>
+        </a-setting-list-item>
     </a-collapse-panel>
     <a-collapse-panel key="5" header='{{ i18n "pages.settings.dateAndTime" }}'>
         <a-setting-list-item paddings="small">

+ 5 - 2
web/html/settings/panel/subscription/subpage.html

@@ -153,7 +153,10 @@
                                     app.sId
                                     ]]</a-descriptions-item>
                                 <a-descriptions-item label='{{ i18n "subscription.status" }}'>
-                                    <template v-if="isUnlimited">
+                                    <template v-if="!app.enabled">
+                                        <a-tag color="red">{{ i18n "subscription.inactive" }}</a-tag>
+                                    </template>
+                                    <template v-else-if="isUnlimited">
                                         <a-tag color="purple">{{ i18n
                                             "subscription.unlimited" }}</a-tag>
                                     </template>
@@ -275,7 +278,7 @@
     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>
+    data-totalbyte="{{ .totalByte }}" data-datepicker="{{ .datepicker }}" data-enabled="{{ .enabled }}"></template>
 <textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }}
 {{ end }}</textarea>
 

+ 3 - 0
web/job/check_client_ip_job.go

@@ -199,6 +199,9 @@ func (j *CheckClientIpJob) processLogFile() bool {
 			inboundClientIps[email][ip] = timestamp
 		}
 	}
+	if err := scanner.Err(); err != nil {
+		j.checkError(err)
+	}
 
 	shouldCleanLog := false
 	for email, ipTimestamps := range inboundClientIps {

+ 13 - 1
web/job/xray_traffic_job.go

@@ -33,7 +33,7 @@ func (j *XrayTrafficJob) Run() {
 	if err != nil {
 		return
 	}
-	err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)
+	err, needRestart0, clientsDisabled := j.inboundService.AddTraffic(traffics, clientTraffics)
 	if err != nil {
 		logger.Warning("add inbound traffic failed:", err)
 	}
@@ -41,6 +41,18 @@ func (j *XrayTrafficJob) Run() {
 	if err != nil {
 		logger.Warning("add outbound traffic failed:", err)
 	}
+	if clientsDisabled {
+		restartOnDisable, settingErr := j.settingService.GetRestartXrayOnClientDisable()
+		if settingErr != nil {
+			logger.Warning("get RestartXrayOnClientDisable failed:", settingErr)
+		}
+		if restartOnDisable {
+			if err := j.xrayService.RestartXray(true); err != nil {
+				logger.Warning("restart xray after disabling clients failed:", err)
+				j.xrayService.SetToNeedRestart()
+			}
+		}
+	}
 	if ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable {
 		j.informTrafficToExternalAPI(traffics, clientTraffics)
 	} else if err != nil {

+ 96 - 29
web/service/inbound.go

@@ -979,10 +979,12 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
 	interfaceClients := settings["clients"].([]any)
 	var newClients []any
 	needApiDel := false
+	clientFound := false
 	for _, client := range interfaceClients {
 		c := client.(map[string]any)
 		c_id := c[client_key].(string)
 		if c_id == clientId {
+			clientFound = true
 			email, _ = c["email"].(string)
 			needApiDel, _ = c["enable"].(bool)
 		} else {
@@ -990,6 +992,10 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
 		}
 	}
 
+	if !clientFound {
+		return false, common.NewError("Client Not Found In Inbound For ID:", clientId)
+	}
+
 	if len(newClients) == 0 {
 		return false, common.NewError("no client remained in Inbound")
 	}
@@ -1222,7 +1228,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 	return needRestart, tx.Save(oldInbound).Error
 }
 
-func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
+func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool, bool) {
 	var err error
 	db := database.GetDB()
 	tx := db.Begin()
@@ -1236,11 +1242,11 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
 	}()
 	err = s.addInboundTraffic(tx, inboundTraffics)
 	if err != nil {
-		return err, false
+		return err, false, false
 	}
 	err = s.addClientTraffic(tx, clientTraffics)
 	if err != nil {
-		return err, false
+		return err, false, false
 	}
 
 	needRestart0, count, err := s.autoRenewClients(tx)
@@ -1250,11 +1256,13 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
 		logger.Debugf("%v clients renewed", count)
 	}
 
+	disabledClientsCount := int64(0)
 	needRestart1, count, err := s.disableInvalidClients(tx)
 	if err != nil {
 		logger.Warning("Error in disabling invalid clients:", err)
 	} else if count > 0 {
 		logger.Debugf("%v clients disabled", count)
+		disabledClientsCount = count
 	}
 
 	needRestart2, count, err := s.disableInvalidInbounds(tx)
@@ -1263,7 +1271,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
 	} else if count > 0 {
 		logger.Debugf("%v inbounds disabled", count)
 	}
-	return nil, (needRestart0 || needRestart1 || needRestart2)
+	return nil, (needRestart0 || needRestart1 || needRestart2), disabledClientsCount > 0
 }
 
 func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@@ -1540,46 +1548,105 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
 	now := time.Now().Unix() * 1000
 	needRestart := false
 
-	if p != nil {
-		var results []struct {
-			Tag   string
-			Email string
-		}
+	var clientsToDisable []struct {
+		InboundId int
+		Tag       string
+		Email     string
+	}
 
-		err := tx.Table("inbounds").
-			Select("inbounds.tag, client_traffics.email").
-			Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
-			Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
-			Scan(&results).Error
-		if err != nil {
-			return false, 0, err
-		}
+	err := tx.Table("inbounds").
+		Select("inbounds.id as inbound_id, inbounds.tag, client_traffics.email").
+		Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
+		Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
+		Scan(&clientsToDisable).Error
+	if err != nil {
+		return false, 0, err
+	}
+
+	if p != nil {
 		s.xrayApi.Init(p.GetAPIPort())
-		for _, result := range results {
-			err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
+		for _, client := range clientsToDisable {
+			err1 := s.xrayApi.RemoveUser(client.Tag, client.Email)
 			if err1 == nil {
-				logger.Debug("Client disabled by api:", result.Email)
+				logger.Debug("Client disabled by api:", client.Email)
 			} else {
-				if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
+				if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", client.Email)) {
 					logger.Debug("User is already disabled. Nothing to do more...")
 				} else {
-					if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
-						logger.Debug("User is already disabled. Nothing to do more...")
-					} else {
-						logger.Debug("Error in disabling client by api:", err1)
-						needRestart = true
-					}
+					logger.Debug("Error in disabling client by api:", err1)
+					needRestart = true
 				}
 			}
 		}
 		s.xrayApi.Close()
 	}
+
 	result := tx.Model(xray.ClientTraffic{}).
 		Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
 		Update("enable", false)
-	err := result.Error
+	err = result.Error
 	count := result.RowsAffected
-	return needRestart, count, err
+	if err != nil {
+		return needRestart, count, err
+	}
+
+	// Also set enable=false in inbounds.settings JSON so clients are visibly disabled
+	if len(clientsToDisable) > 0 {
+		inboundEmailMap := make(map[int]map[string]struct{})
+		for _, c := range clientsToDisable {
+			if inboundEmailMap[c.InboundId] == nil {
+				inboundEmailMap[c.InboundId] = make(map[string]struct{})
+			}
+			inboundEmailMap[c.InboundId][c.Email] = struct{}{}
+		}
+		inboundIds := make([]int, 0, len(inboundEmailMap))
+		for id := range inboundEmailMap {
+			inboundIds = append(inboundIds, id)
+		}
+		var inbounds []*model.Inbound
+		if err = tx.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error; err != nil {
+			logger.Warning("disableInvalidClients fetch inbounds:", err)
+			return needRestart, count, nil
+		}
+		for _, inbound := range inbounds {
+			settings := map[string]any{}
+			if jsonErr := json.Unmarshal([]byte(inbound.Settings), &settings); jsonErr != nil {
+				continue
+			}
+			clients, ok := settings["clients"].([]any)
+			if !ok {
+				continue
+			}
+			emailSet := inboundEmailMap[inbound.Id]
+			changed := false
+			for i := range clients {
+				c, ok := clients[i].(map[string]any)
+				if !ok {
+					continue
+				}
+				email, _ := c["email"].(string)
+				if _, shouldDisable := emailSet[email]; shouldDisable {
+					c["enable"] = false
+					c["updated_at"] = time.Now().Unix() * 1000
+					clients[i] = c
+					changed = true
+				}
+			}
+			if changed {
+				settings["clients"] = clients
+				modifiedSettings, jsonErr := json.MarshalIndent(settings, "", "  ")
+				if jsonErr != nil {
+					continue
+				}
+				inbound.Settings = string(modifiedSettings)
+			}
+		}
+		if err = tx.Save(inbounds).Error; err != nil {
+			logger.Warning("disableInvalidClients update inbound settings:", err)
+		}
+	}
+
+	return needRestart, count, nil
 }
 
 func (s *InboundService) GetInboundTags() (string, error) {

+ 4 - 0
web/service/server.go

@@ -846,6 +846,10 @@ func (s *ServerService) GetXrayLogs(
 		entries = append(entries, entry)
 	}
 
+	if err := scanner.Err(); err != nil {
+		return nil
+	}
+
 	if len(entries) > countInt {
 		entries = entries[len(entries)-countInt:]
 	}

+ 9 - 0
web/service/setting.go

@@ -83,6 +83,7 @@ var defaultValueMap = map[string]string{
 	"nord":                        "",
 	"externalTrafficInformEnable": "false",
 	"externalTrafficInformURI":    "",
+	"restartXrayOnClientDisable":  "true",
 	"xrayOutboundTestUrl":         "https://www.google.com/generate_204",
 
 	// LDAP defaults
@@ -628,6 +629,14 @@ func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error {
 	return s.setString("externalTrafficInformURI", InformURI)
 }
 
+func (s *SettingService) GetRestartXrayOnClientDisable() (bool, error) {
+	return s.getBool("restartXrayOnClientDisable")
+}
+
+func (s *SettingService) SetRestartXrayOnClientDisable(value bool) error {
+	return s.setBool("restartXrayOnClientDisable", value)
+}
+
 func (s *SettingService) GetIpLimitEnable() (bool, error) {
 	accessLogPath, err := xray.GetAccessLogPath()
 	if err != nil {

+ 1 - 1
web/service/xray.go

@@ -103,7 +103,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 		return nil, err
 	}
 
-	s.inboundService.AddTraffic(nil, nil)
+	_, _, _ = s.inboundService.AddTraffic(nil, nil)
 
 	inbounds, err := s.inboundService.GetAllInbounds()
 	if err != nil {

+ 2 - 0
web/translation/translate.ar_EG.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك."
 "externalTrafficInformURI" = "مسار تنبيه الترافيك الخارجي"
 "externalTrafficInformURIDesc" = "تحديثات الترافيك هتتبعت للمسار ده."
+"restartXrayOnClientDisable" = "إعادة تشغيل Xray بعد التعطيل التلقائي"
+"restartXrayOnClientDisableDesc" = "عند تعطيل العميل تلقائيا بسبب انتهاء الصلاحية أو حد حركة المرور، أعد تشغيل Xray."
 "fragment" = "تجزئة"
 "fragmentDesc" = "يفعل تجزئة لحزمة TLS hello."
 "fragmentSett" = "إعدادات التجزئة"

+ 2 - 0
web/translation/translate.en_US.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
 "externalTrafficInformURI" = "External Traffic Inform URI"
 "externalTrafficInformURIDesc" = "Traffic updates are sent to this URI."
+"restartXrayOnClientDisable" = "Restart Xray After Auto Disable"
+"restartXrayOnClientDisableDesc" = "When a client is automatically disabled due to expiration or traffic limit, restart Xray."
 "fragment" = "Fragmentation"
 "fragmentDesc" = "Enable fragmentation for TLS hello packet."
 "fragmentSett" = "Fragmentation Settings"

+ 2 - 0
web/translation/translate.es_ES.toml

@@ -476,6 +476,8 @@
 "externalTrafficInformEnableDesc" = "Informar a la API externa sobre cada actualización de tráfico."
 "externalTrafficInformURI" = "URI de información de tráfico externo"
 "externalTrafficInformURIDesc" = "Las actualizaciones de tráfico se envían a este URI."
+"restartXrayOnClientDisable" = "Reiniciar Xray tras desactivación automática"
+"restartXrayOnClientDisableDesc" = "Cuando un cliente se desactive automáticamente por vencimiento o límite de tráfico, reiniciar Xray."
 "subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
 "fragment" = "Fragmentación"
 "fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS"

+ 2 - 0
web/translation/translate.fa_IR.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
 "externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
 "externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
+"restartXrayOnClientDisable" = "ری‌استارت Xray بعد از غیرفعال‌سازی خودکار"
+"restartXrayOnClientDisableDesc" = "وقتی کاربر به‌صورت خودکار به‌دلیل اتمام زمان یا ترافیک غیرفعال می‌شود، Xray ری‌استارت شود."
 "fragment" = "فرگمنت"
 "fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس"
 "fragmentSett" = "تنظیمات فرگمنت"

+ 2 - 0
web/translation/translate.id_ID.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
 "externalTrafficInformURI" = "Lalu Lintas Eksternal Menginformasikan URI"
 "externalTrafficInformURIDesc" = "Pembaruan lalu lintas dikirim ke URI ini."
+"restartXrayOnClientDisable" = "Nyalakan Ulang Xray Setelah Nonaktif Otomatis"
+"restartXrayOnClientDisableDesc" = "Saat klien otomatis dinonaktifkan karena kedaluwarsa atau batas trafik, mulai ulang Xray."
 "fragment" = "Fragmentasi"
 "fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
 "fragmentSett" = "Pengaturan Fragmentasi"

+ 2 - 0
web/translation/translate.ja_JP.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "トラフィックの更新ごとに外部 API に通知します。"
 "externalTrafficInformURI" = "外部トラフィック通知 URI"
 "externalTrafficInformURIDesc" = "トラフィックの更新ごとに外部 API に通知します。"
+"restartXrayOnClientDisable" = "自動無効化後に Xray を再起動"
+"restartXrayOnClientDisableDesc" = "有効期限切れまたはトラフィック上限でクライアントが自動的に無効化されたとき、Xray を再起動します。"
 "fragment" = "フラグメント"
 "fragmentDesc" = "TLS helloパケットのフラグメントを有効にする"
 "fragmentSett" = "設定"

+ 2 - 0
web/translation/translate.pt_BR.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Informar a API externa sobre cada atualização de tráfego."
 "externalTrafficInformURI" = "URI de informação de tráfego externo"
 "externalTrafficInformURIDesc" = "As atualizações de tráfego são enviadas para este URI."
+"restartXrayOnClientDisable" = "Reiniciar Xray Após Desativação Automática"
+"restartXrayOnClientDisableDesc" = "Quando um cliente for desativado automaticamente por expiração ou limite de tráfego, reinicie o Xray."
 "fragment" = "Fragmentação"
 "fragmentDesc" = "Ativa a fragmentação para o pacote TLS hello."
 "fragmentSett" = "Configurações de Fragmentação"

+ 2 - 0
web/translation/translate.ru_RU.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Информировать внешний API о каждом обновлении трафика"
 "externalTrafficInformURI" = "URI информации о внешнем трафике"
 "externalTrafficInformURIDesc" = "Обновления трафика отправляются на этот URI"
+"restartXrayOnClientDisable" = "Перезапускать Xray после автоотключения"
+"restartXrayOnClientDisableDesc" = "Когда клиент автоматически отключается из-за окончания срока действия или лимита трафика, перезапускать Xray."
 "fragment" = "Фрагментация"
 "fragmentDesc" = "Включить фрагментацию TLS-хэндшейка"
 "fragmentSett" = "Настройки фрагментации"

+ 2 - 0
web/translation/translate.tr_TR.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Her trafik güncellemesinde harici API'yi bilgilendirin."
 "externalTrafficInformURI" = "Harici Trafik Bilgisi URI'si"
 "externalTrafficInformURIDesc" = "Trafik güncellemeleri bu URI'ye gönderildi."
+"restartXrayOnClientDisable" = "Otomatik Devre Dışı Sonrası Xray'i Yeniden Başlat"
+"restartXrayOnClientDisableDesc" = "Bir istemci süre dolumu veya trafik limiti nedeniyle otomatik devre dışı bırakıldığında Xray'i yeniden başlat."
 "fragment" = "Parçalama"
 "fragmentDesc" = "TLS merhaba paketinin parçalanmasını etkinleştir."
 "fragmentSett" = "Parçalama Ayarları"

+ 2 - 0
web/translation/translate.uk_UA.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Інформувати зовнішній API про кожне оновлення трафіку."
 "externalTrafficInformURI" = "Інформаційний URI зовнішнього трафіку"
 "externalTrafficInformURIDesc" = "Оновлення трафіку надсилаються на цей URI."
+"restartXrayOnClientDisable" = "Перезапускати Xray після авто-вимкнення"
+"restartXrayOnClientDisableDesc" = "Коли клієнт автоматично вимикається через закінчення терміну дії або ліміт трафіку, перезапускати Xray."
 "fragment" = "Фрагментація"
 "fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS"
 "fragmentSett" = "Параметри фрагментації"

+ 2 - 0
web/translation/translate.vi_VN.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập."
 "externalTrafficInformURI" = "URI thông báo lưu lượng truy cập bên ngoài"
 "externalTrafficInformURIDesc" = "Cập nhật lưu lượng truy cập được gửi tới URI này."
+"restartXrayOnClientDisable" = "Khởi Động Lại Xray Sau Khi Tự Động Vô Hiệu Hóa"
+"restartXrayOnClientDisableDesc" = "Khi người dùng bị vô hiệu hóa tự động do hết hạn hoặc chạm giới hạn lưu lượng, hãy khởi động lại Xray."
 "fragment" = "Sự phân mảnh"
 "fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
 "fragmentSett" = "Cài đặt phân mảnh"

+ 2 - 0
web/translation/translate.zh_CN.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "每次流量更新时通知外部 API"
 "externalTrafficInformURI" = "外部流量通知 URI"
 "externalTrafficInformURIDesc" = "流量更新将发送到此 URI"
+"restartXrayOnClientDisable" = "客户端自动禁用后重启 Xray"
+"restartXrayOnClientDisableDesc" = "当客户端因到期或流量超限被自动禁用时,重启 Xray。"
 "fragment" = "分片"
 "fragmentDesc" = "启用 TLS hello 数据包分片"
 "fragmentSett" = "设置"

+ 2 - 0
web/translation/translate.zh_TW.toml

@@ -477,6 +477,8 @@
 "externalTrafficInformEnableDesc" = "每次流量更新時通知外部 API"
 "externalTrafficInformURI" = "外部流量通知 URI"
 "externalTrafficInformURIDesc" = "流量更新將會傳送到此 URI"
+"restartXrayOnClientDisable" = "用戶自動停用後重新啟動 Xray"
+"restartXrayOnClientDisableDesc" = "當用戶因到期或流量上限而被自動停用時,重新啟動 Xray。"
 "fragment" = "分片"
 "fragmentDesc" = "啟用 TLS hello 資料包分片"
 "fragmentSett" = "設定"