Browse Source

bug fix: del Depleted

mhsanaei 23 hours ago
parent
commit
dc21f41932
4 changed files with 1162 additions and 1073 deletions
  1. 4 4
      web/html/component/aClientTable.html
  2. 1138 1064
      web/html/inbounds.html
  3. 11 3
      web/html/modals/inbound_info_modal.html
  4. 9 2
      web/service/inbound.go

+ 4 - 4
web/html/component/aClientTable.html

@@ -37,7 +37,7 @@
     <template slot="content" >
       {{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
     </template>
-    <template v-if="client.enable && isClientOnline(client.email)">
+    <template v-if="client.enable && isClientOnline(client.email) && !isClientDepleted">
       <a-tag color="green">{{ i18n "online" }}</a-tag>
     </template>
     <template v-else>
@@ -49,9 +49,9 @@
   <a-space direction="horizontal" :size="2">
     <a-tooltip>
       <template slot="title">
-        <template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
-        <template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
-        <template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
+        <template v-if="isClientDepleted">{{ i18n "depleted" }}</template>
+        <template v-if="!isClientDepleted && !client.enable">{{ i18n "disabled" }}</template>
+        <template v-if="!isClientDepleted && 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-tooltip>

+ 1138 - 1064
web/html/inbounds.html

@@ -9,15 +9,13 @@
       <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
         <transition name="list" appear>
           <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
-            message='{{ i18n "secAlertTitle" }}'
-            color="red"
-            description='{{ i18n "secAlertSsl" }}'
-            show-icon closable>
+            message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
           </a-alert>
         </transition>
         <transition name="list" appear>
           <a-row v-if="!loadingStates.fetched">
-            <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
+            <a-card
+              :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
               <a-spin tip='{{ i18n "loading" }}'></a-spin>
             </a-card>
           </a-row>
@@ -26,40 +24,47 @@
               <a-card size="small" :style="{ padding: '16px' }" hoverable>
                 <a-row>
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
+                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}'
+                      :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
                       <template #prefix>
                         <a-icon type="swap"></a-icon>
                       </template>
                     </a-custom-statistic>
                   </a-col>
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
+                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}'
+                      :value="SizeFormatter.sizeFormat(total.up + total.down)"
+                      :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                         <a-icon type="pie-chart"></a-icon>
                       </template>
                     </a-custom-statistic>
                   </a-col>
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
+                    <a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}'
+                      :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                         <a-icon type="history"></a-icon>
                       </template>
                     </a-custom-statistic>
                   </a-col>
                   <a-col :sm="12" :md="5">
-                    <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
+                    <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length"
+                      :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                         <a-icon type="bars"></a-icon>
                       </template>
                     </a-custom-statistic>
                   </a-col>
                   <a-col :sm="12" :md="4">
-                    <a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
+                    <a-custom-statistic title='{{ i18n "clients" }}' value=" "
+                      :style="{ marginTop: isMobile ? '10px' : 0 }">
                       <template #prefix>
                         <a-space direction="horizontal">
                           <a-icon type="team"></a-icon>
                           <div>
-                            <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
+                            <a-back-top :target="() => document.getElementById('content-layout')"
+                              visibility-height="200"></a-back-top>
                             <a-tag color="green">[[ total.clients ]]</a-tag>
                             <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                               <template slot="content">
@@ -73,7 +78,8 @@
                               </template>
                               <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
                             </a-popover>
-                            <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                            <a-popover title='{{ i18n "depletingSoon" }}'
+                              :overlay-class-name="themeSwitcher.currentTheme">
                               <template slot="content">
                                 <div v-for="clientEmail in total.expiring"><span>[[ clientEmail ]]</span></div>
                               </template>
@@ -136,7 +142,7 @@
                 <template #extra>
                   <a-button-group>
                     <a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
-                    <a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme"> 
+                    <a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
                       <template #title>
                         <div class="ant-custom-popover-title">
                           <a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
@@ -146,11 +152,8 @@
                       <template #content>
                         <a-space direction="vertical">
                           <span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span>
-                          <a-select v-model="refreshInterval"
-                              :disabled="!isRefreshEnabled"
-                              :style="{ width: '100%' }"
-                              @change="changeRefreshInterval"
-                              :dropdown-class-name="themeSwitcher.currentTheme">
+                          <a-select v-model="refreshInterval" :disabled="!isRefreshEnabled" :style="{ width: '100%' }"
+                            @change="changeRefreshInterval" :dropdown-class-name="themeSwitcher.currentTheme">
                             <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
                           </a-select>
                         </a-space>
@@ -162,13 +165,15 @@
                 <a-space direction="vertical">
                   <div :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
                     <a-switch v-model="enableFilter"
-                        :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
-                        @change="toggleFilter">
+                      :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
+                      @change="toggleFilter">
                       <a-icon slot="checkedChildren" type="search"></a-icon>
                       <a-icon slot="unCheckedChildren" type="filter"></a-icon>
                     </a-switch>
-                    <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus :style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input>
-                    <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
+                    <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus
+                      :style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input>
+                    <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="deactive">{{ i18n "disabled" }}</a-radio-button>
                       <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
@@ -177,25 +182,24 @@
                     </a-radio-group>
                   </div>
                   <a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
-                      :data-source="searchedInbounds"
-                      :scroll="isMobile ? {} : { x: 1000 }"
-                      :pagination=pagination(searchedInbounds)
-                      :expand-icon-as-cell="false"
-                      :expand-row-by-click="false"
-                      :expand-icon-column-index="0"
-                      :indent-size="0"
-                      :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
-                      :style="{ marginTop: '10px' }"
-                      :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}`, emptyText: `{{ i18n "noData" }}` }'>
+                    :data-source="searchedInbounds" :scroll="isMobile ? {} : { x: 1000 }"
+                    :pagination=pagination(searchedInbounds) :expand-icon-as-cell="false" :expand-row-by-click="false"
+                    :expand-icon-column-index="0" :indent-size="0"
+                    :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
+                    :style="{ marginTop: '10px' }"
+                    :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}`, emptyText: `{{ i18n "noData" }}` }'>
                     <template slot="action" slot-scope="text, dbInbound">
                       <a-dropdown :trigger="['click']">
-                        <a-icon @click="e => e.preventDefault()" type="more" :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
-                        <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
+                        <a-icon @click="e => e.preventDefault()" type="more"
+                          :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
+                        <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)"
+                          :theme="themeSwitcher.currentTheme">
                           <a-menu-item key="edit">
                             <a-icon type="edit"></a-icon>
                             {{ i18n "edit" }}
                           </a-menu-item>
-                          <a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
+                          <a-menu-item key="qrcode"
+                            v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
                             <a-icon type="qrcode"></a-icon>
                             {{ i18n "qrCode" }}
                           </a-menu-item>
@@ -247,7 +251,8 @@
                             </span>
                           </a-menu-item>
                           <a-menu-item v-if="isMobile">
-                            <a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
+                            <a-switch size="small" v-model="dbInbound.enable"
+                              @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                             {{ i18n "pages.inbounds.enable" }}
                           </a-menu-item>
                         </a-menu>
@@ -257,8 +262,10 @@
                       <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
                       <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
                         <a-tag :style="{ margin: '0' }" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
-                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
-                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
+                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls"
+                          color="blue">TLS</a-tag>
+                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality"
+                          color="blue">Reality</a-tag>
                       </template>
                     </template>
                     <template slot="clients" slot-scope="text, dbInbound">
@@ -266,59 +273,75 @@
                         <a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
                         <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" class="client-popup-item">
+                            <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail"
+                              class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                   [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                 </template>
-                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                <a-icon type="message"
+                                  v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                             </div>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }"
+                            v-if="clientCount[dbInbound.id].deactive.length">[[
+                            clientCount[dbInbound.id].deactive.length ]]</a-tag>
                         </a-popover>
                         <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" class="client-popup-item">
+                            <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail"
+                              class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                   [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                 </template>
-                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                <a-icon type="message"
+                                  v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                             </div>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red"
+                            v-if="clientCount[dbInbound.id].depleted.length">[[
+                            clientCount[dbInbound.id].depleted.length ]]</a-tag>
                         </a-popover>
                         <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" class="client-popup-item">
+                            <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail"
+                              class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                   [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                 </template>
-                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                <a-icon type="message"
+                                  v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                             </div>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange"
+                            v-if="clientCount[dbInbound.id].expiring.length">[[
+                            clientCount[dbInbound.id].expiring.length ]]</a-tag>
                         </a-popover>
                         <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
                           <template slot="content">
-                            <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" class="client-popup-item">
+                            <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail"
+                              class="client-popup-item">
                               <span>[[ clientEmail ]]</span>
                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                 <template #title>
                                   [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                 </template>
-                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                <a-icon type="message"
+                                  v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                               </a-tooltip>
                             </div>
                           </template>
-                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
+                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue"
+                            v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length
+                            ]]</a-tag>
                         </a-popover>
                       </template>
                     </template>
@@ -336,14 +359,17 @@
                             </tr>
                           </table>
                         </template>
-                        <a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
+                        <a-tag
+                          :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
                           [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
                           <template v-if="dbInbound.total > 0">
-                              [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
+                            [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
                           </template>
                           <template v-else>
                             <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>
                           </template>
                         </a-tag>
@@ -353,7 +379,8 @@
                       <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
                     </template>
                     <template slot="enable" slot-scope="text, dbInbound">
-                      <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
+                      <a-switch v-model="dbInbound.enable"
+                        @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                     </template>
                     <template slot="expiryTime" slot-scope="text, dbInbound">
                       <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
@@ -363,28 +390,36 @@
                         <template v-else slot="content">
                           [[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
                         </template>
-                        <a-tag :style="{ minWidth: '50px' }" :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
+                        <a-tag :style="{ minWidth: '50px' }"
+                          :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
                           [[ remainedDays(dbInbound._expiryTime) ]]
                         </a-tag>
                       </a-popover>
                       <a-tag v-else color="purple" 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>
                     <template slot="info" slot-scope="text, dbInbound">
-                      <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
+                      <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme"
+                        trigger="click">
                         <template slot="content">
                           <table cellpadding="2">
                             <tr>
                               <td>{{ i18n "pages.inbounds.protocol" }}</td>
                               <td>
                                 <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
-                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
-                                  <a-tag :style="{ margin: '0' }" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
-                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
-                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
+                                <template
+                                  v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+                                  <a-tag :style="{ margin: '0' }" color="blue">[[ dbInbound.toInbound().stream.network
+                                    ]]</a-tag>
+                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls"
+                                    color="green">tls</a-tag>
+                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality"
+                                    color="green">reality</a-tag>
                                 </template>
                               </td>
                             </tr>
@@ -395,62 +430,82 @@
                             <tr v-if="clientCount[dbInbound.id]">
                               <td>{{ i18n "clients" }}</td>
                               <td>
-                                <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
-                                <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients
+                                  ]]</a-tag>
+                                <a-popover title='{{ i18n "disabled" }}'
+                                  :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" class="client-popup-item">
+                                    <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail"
+                                      class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                           [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                         </template>
-                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                        <a-icon type="message"
+                                          v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                     </div>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
+                                  <a-tag :style="{ margin: '0', padding: '0 2px' }"
+                                    v-if="clientCount[dbInbound.id].deactive.length">[[
+                                    clientCount[dbInbound.id].deactive.length ]]</a-tag>
                                 </a-popover>
-                                <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                <a-popover title='{{ i18n "depleted" }}'
+                                  :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" class="client-popup-item">
+                                    <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail"
+                                      class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                           [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                         </template>
-                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                        <a-icon type="message"
+                                          v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                     </div>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
+                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red"
+                                    v-if="clientCount[dbInbound.id].depleted.length">[[
+                                    clientCount[dbInbound.id].depleted.length ]]</a-tag>
                                 </a-popover>
-                                <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                <a-popover title='{{ i18n "depletingSoon" }}'
+                                  :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" class="client-popup-item">
+                                    <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail"
+                                      class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                           [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                         </template>
-                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                        <a-icon type="message"
+                                          v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                     </div>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
+                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange"
+                                    v-if="clientCount[dbInbound.id].expiring.length">[[
+                                    clientCount[dbInbound.id].expiring.length ]]</a-tag>
                                 </a-popover>
                                 <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                   <template slot="content">
-                                    <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" class="client-popup-item">
+                                    <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail"
+                                      class="client-popup-item">
                                       <span>[[ clientEmail ]]</span>
                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                         <template #title>
                                           [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                         </template>
-                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
+                                        <a-icon type="message"
+                                          v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                       </a-tooltip>
                                     </div>
                                   </template>
-                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
+                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green"
+                                    v-if="clientCount[dbInbound.id].online.length">[[
+                                    clientCount[dbInbound.id].online.length ]]</a-tag>
                                 </a-popover>
                               </td>
                             </tr>
@@ -464,20 +519,25 @@
                                         <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
                                         <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
                                       </tr>
-                                      <tr v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
+                                      <tr
+                                        v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
                                         <td>{{ i18n "remained" }}</td>
-                                        <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
+                                        <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down)
+                                          ]]</td>
                                       </tr>
                                     </table>
                                   </template>
-                                  <a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
-                                      [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
-                                      <template v-if="dbInbound.total > 0">
-                                          [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
-                                      </template>
+                                  <a-tag
+                                    :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
+                                    [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
+                                    <template v-if="dbInbound.total > 0">
+                                      [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
+                                    </template>
                                     <template v-else>
                                       <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>
                                     </template>
                                   </a-tag>
@@ -487,8 +547,8 @@
                             <tr>
                               <td>{{ i18n "pages.inbounds.expireDate" }}</td>
                               <td>
-                                <a-tag :style="{ minWidth: '50px', textAlign: 'center' }" v-if="dbInbound.expiryTime > 0"
-                                  :color="dbInbound.isExpiry? 'red': 'blue'">
+                                <a-tag :style="{ minWidth: '50px', textAlign: 'center' }"
+                                  v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
                                   <template v-if="app.datepicker === 'gregorian'">
                                     [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
                                   </template>
@@ -498,7 +558,9 @@
                                 </a-tag>
                                 <a-tag v-else :style="{ textAlign: 'center' }" color="purple" 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>
                               </td>
@@ -506,13 +568,15 @@
                             <tr>
                               <td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
                               <td>
-                                <a-tag color="blue">[[ i18n("pages.inbounds.periodicTrafficReset." + dbInbound.trafficReset) ]]</a-tag>
+                                <a-tag color="blue">[[ i18n("pages.inbounds.periodicTrafficReset." +
+                                  dbInbound.trafficReset) ]]</a-tag>
                               </td>
                             </tr>
                           </table>
                         </template>
                         <a-badge>
-                          <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
+                          <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle"
+                            :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
                           <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
                             <a-icon type="info"></a-icon>
                           </a-button>
@@ -520,18 +584,15 @@
                       </a-popover>
                     </template>
                     <template slot="expandedRowRender" slot-scope="record">
-                      <a-table
-                        :row-key="client => client.id"
-                        :columns="isMobile ? innerMobileColumns : innerColumns"
-                        :data-source="getInboundClients(record)"
-                        :pagination=pagination(getInboundClients(record))
+                      <a-table :row-key="client => client.id" :columns="isMobile ? innerMobileColumns : innerColumns"
+                        :data-source="getInboundClients(record)" :pagination=pagination(getInboundClients(record))
                         :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
                         {{template "component/aClientTable"}}
                       </a-table>
                     </template>
                   </a-table>
                 </a-space>
-              </a-card> 
+              </a-card>
             </a-col>
           </a-row>
         </transition>
@@ -556,989 +617,1002 @@
 {{template "modals/clientsModal"}}
 {{template "modals/clientsBulkModal"}}
 <script>
-    const columns = [{
-        title: "ID",
-        align: 'right',
-        dataIndex: "id",
-        width: 30,
-        responsive: ["xs"],
-    }, {
-        title: '{{ i18n "pages.inbounds.operate" }}',
-        align: 'center',
-        width: 30,
-        scopedSlots: { customRender: 'action' },
-    }, {
-        title: '{{ i18n "pages.inbounds.enable" }}',
-        align: 'center',
-        width: 35,
-        scopedSlots: { customRender: 'enable' },
-    }, {
-        title: '{{ i18n "pages.inbounds.remark" }}',
-        align: 'center',
-        width: 60,
-        dataIndex: "remark",
-    }, {
-        title: '{{ i18n "pages.inbounds.port" }}',
-        align: 'center',
-        dataIndex: "port",
-        width: 40,
-    }, {
-        title: '{{ i18n "pages.inbounds.protocol" }}',
-        align: 'left',
-        width: 70,
-        scopedSlots: { customRender: 'protocol' },
-    }, {
-        title: '{{ i18n "clients" }}',
-        align: 'left',
-        width: 50,
-        scopedSlots: { customRender: 'clients' },
-    }, {
-        title: '{{ i18n "pages.inbounds.traffic" }}',
-        align: 'center',
-        width: 90,
-        scopedSlots: { customRender: 'traffic' },
-    }, {
-        title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
-        align: 'center',
-        width: 70,
-        scopedSlots: { customRender: 'allTimeInbound' },
-    }, {
-        title: '{{ i18n "pages.inbounds.expireDate" }}',
-        align: 'center',
-        width: 40,
-        scopedSlots: { customRender: 'expiryTime' },
-    }];
+  const columns = [{
+    title: "ID",
+    align: 'right',
+    dataIndex: "id",
+    width: 30,
+    responsive: ["xs"],
+  }, {
+    title: '{{ i18n "pages.inbounds.operate" }}',
+    align: 'center',
+    width: 30,
+    scopedSlots: { customRender: 'action' },
+  }, {
+    title: '{{ i18n "pages.inbounds.enable" }}',
+    align: 'center',
+    width: 35,
+    scopedSlots: { customRender: 'enable' },
+  }, {
+    title: '{{ i18n "pages.inbounds.remark" }}',
+    align: 'center',
+    width: 60,
+    dataIndex: "remark",
+  }, {
+    title: '{{ i18n "pages.inbounds.port" }}',
+    align: 'center',
+    dataIndex: "port",
+    width: 40,
+  }, {
+    title: '{{ i18n "pages.inbounds.protocol" }}',
+    align: 'left',
+    width: 70,
+    scopedSlots: { customRender: 'protocol' },
+  }, {
+    title: '{{ i18n "clients" }}',
+    align: 'left',
+    width: 50,
+    scopedSlots: { customRender: 'clients' },
+  }, {
+    title: '{{ i18n "pages.inbounds.traffic" }}',
+    align: 'center',
+    width: 90,
+    scopedSlots: { customRender: 'traffic' },
+  }, {
+    title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
+    align: 'center',
+    width: 70,
+    scopedSlots: { customRender: 'allTimeInbound' },
+  }, {
+    title: '{{ i18n "pages.inbounds.expireDate" }}',
+    align: 'center',
+    width: 40,
+    scopedSlots: { customRender: 'expiryTime' },
+  }];
 
-    const mobileColumns = [{
-        title: "ID",
-        align: 'right',
-        dataIndex: "id",
-        width: 10,
-        responsive: ["s"],
-    }, {
-        title: '{{ i18n "pages.inbounds.operate" }}',
-        align: 'center',
-        width: 25,
-        scopedSlots: { customRender: 'action' },
-    }, {
-        title: '{{ i18n "pages.inbounds.remark" }}',
-        align: 'left',
-        width: 70,
-        dataIndex: "remark",
-    }, {
-        title: '{{ i18n "pages.inbounds.info" }}',
-        align: 'center',
-        width: 10,
-        scopedSlots: { customRender: 'info' },
-    }];
+  const mobileColumns = [{
+    title: "ID",
+    align: 'right',
+    dataIndex: "id",
+    width: 10,
+    responsive: ["s"],
+  }, {
+    title: '{{ i18n "pages.inbounds.operate" }}',
+    align: 'center',
+    width: 25,
+    scopedSlots: { customRender: 'action' },
+  }, {
+    title: '{{ i18n "pages.inbounds.remark" }}',
+    align: 'left',
+    width: 70,
+    dataIndex: "remark",
+  }, {
+    title: '{{ i18n "pages.inbounds.info" }}',
+    align: 'center',
+    width: 10,
+    scopedSlots: { customRender: 'info' },
+  }];
 
-    const innerColumns = [
-        { title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
-        { title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
-        { title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
-        { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
-        { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
-        { title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
-        { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
-    ];
+  const innerColumns = [
+    { title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
+    { title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
+    { title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
+    { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
+    { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
+    { title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
+    { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
+  ];
 
-    const innerMobileColumns = [
-        { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
-        { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
-        { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
-    ];
+  const innerMobileColumns = [
+    { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
+    { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
+    { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
+  ];
 
-    const app = new Vue({
-        delimiters: ['[[', ']]'],
-        el: '#app',
-        mixins: [MediaQueryMixin],
-        data: {
-            themeSwitcher,
-            persianDatepicker,
-            loadingStates: {
-              fetched: false,
-              spinning: false
-            },
-            inbounds: [],
-            dbInbounds: [],
-            searchKey: '',
-            enableFilter: false,
-            filterBy: '',
-            searchedInbounds: [],
-            expireDiff: 0,
-            trafficDiff: 0,
-            defaultCert: '',
-            defaultKey: '',
-            clientCount: [],
-            onlineClients: [],
-            lastOnlineMap: {},
-            isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
-            refreshing: false,
-            refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
-            subSettings: {
-                enable : false,
-                subTitle : '',
-                subURI : '',
-                subJsonURI : '',
-            },
-            remarkModel: '-ieo',
-            datepicker: 'gregorian',
-            tgBotEnable: false,
-            showAlert: false,
-            ipLimitEnable: false,
-            pageSize: 50,
-        },
-        methods: {
-            loading(spinning = true) {
-                this.loadingStates.spinning = spinning;
-            },
-            async getDBInbounds() {
-                this.refreshing = true;
-                const msg = await HttpUtil.get('/panel/api/inbounds/list');
-                if (!msg.success) {
-                    this.refreshing = false;
-                    return;
-                }
+  const app = new Vue({
+    delimiters: ['[[', ']]'],
+    el: '#app',
+    mixins: [MediaQueryMixin],
+    data: {
+      themeSwitcher,
+      persianDatepicker,
+      loadingStates: {
+        fetched: false,
+        spinning: false
+      },
+      inbounds: [],
+      dbInbounds: [],
+      searchKey: '',
+      enableFilter: false,
+      filterBy: '',
+      searchedInbounds: [],
+      expireDiff: 0,
+      trafficDiff: 0,
+      defaultCert: '',
+      defaultKey: '',
+      clientCount: [],
+      onlineClients: [],
+      lastOnlineMap: {},
+      isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
+      refreshing: false,
+      refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
+      subSettings: {
+        enable: false,
+        subTitle: '',
+        subURI: '',
+        subJsonURI: '',
+      },
+      remarkModel: '-ieo',
+      datepicker: 'gregorian',
+      tgBotEnable: false,
+      showAlert: false,
+      ipLimitEnable: false,
+      pageSize: 50,
+    },
+    methods: {
+      loading(spinning = true) {
+        this.loadingStates.spinning = spinning;
+      },
+      async getDBInbounds() {
+        this.refreshing = true;
+        const msg = await HttpUtil.get('/panel/api/inbounds/list');
+        if (!msg.success) {
+          this.refreshing = false;
+          return;
+        }
 
-                await this.getLastOnlineMap();
-                await this.getOnlineUsers();
-                
-                this.setInbounds(msg.obj);
-                setTimeout(() => {
-                    this.refreshing = false;
-                }, 500);
-            },
-            async getOnlineUsers() {
-                const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
-                if (!msg.success) {
-                    return;
-                }
-                this.onlineClients = msg.obj != null ? msg.obj : [];
-            },
-            async getLastOnlineMap() {
-                const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
-                if (!msg.success || !msg.obj) return;
-                this.lastOnlineMap = msg.obj || {}
-            },
-            async getDefaultSettings() {
-                const msg = await HttpUtil.post('/panel/setting/defaultSettings');
-                if (!msg.success) {
-                    return;
-                }
-                with(msg.obj){
-                    this.expireDiff = expireDiff * 86400000;
-                    this.trafficDiff = trafficDiff * 1073741824;
-                    this.defaultCert = defaultCert;
-                    this.defaultKey = defaultKey;
-                    this.tgBotEnable = tgBotEnable;
-                    this.subSettings = {
-                        enable : subEnable,
-                        subTitle : subTitle,
-                        subURI: subURI,
-                        subJsonURI: subJsonURI
-                    };
-                    this.pageSize = pageSize;
-                    this.remarkModel = remarkModel;
-                    this.datepicker = datepicker;
-                    this.ipLimitEnable = ipLimitEnable;
-                }
-            },
-            setInbounds(dbInbounds) {
-                this.inbounds.splice(0);
-                this.dbInbounds.splice(0);
-                this.clientCount.splice(0);
-                for (const inbound of dbInbounds) {
-                    const dbInbound = new DBInbound(inbound);
-                    to_inbound = dbInbound.toInbound()
-                    this.inbounds.push(to_inbound);
-                    this.dbInbounds.push(dbInbound);
-                    if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
-                        if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
-                            continue;
-                        }
-                        this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
-                    }
-                }
-                if (!this.loadingStates.fetched) {
-                    this.loadingStates.fetched = true
-                }
-                if(this.enableFilter){
-                    this.filterInbounds();
-                } else {
-                    this.searchInbounds(this.searchKey);
-                }
-            },
-            getClientCounts(dbInbound, inbound) {
-                let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [], comments = new Map();
-                clients = inbound.clients;
-                clientStats = dbInbound.clientStats
-                now = new Date().getTime()
-                if (clients) {
-                    clientCount = clients.length;
-                    if (dbInbound.enable) {
-                        clients.forEach(client => {
-                            if (client.comment) {
-                              comments.set(client.email, client.comment)
-                            }
-                            if (client.enable) {
-                                active.push(client.email);
-                                if (this.isClientOnline(client.email)) online.push(client.email);
-                            } else {
-                                deactive.push(client.email);
-                            }
-                        });
-                        clientStats.forEach(client => {
-                            if (!client.enable) {
-                                depleted.push(client.email);
-                            } else {
-                                if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
-                                    (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
-                            }
-                        });
-                    } else {
-                        clients.forEach(client => {
-                            deactive.push(client.email);
-                        });
-                    }
-                }
-                return {
-                    clients: clientCount,
-                    active: active,
-                    deactive: deactive,
-                    depleted: depleted,
-                    expiring: expiring,
-                    online: online,
-                    comments: comments,
-                };
-            },
+        await this.getLastOnlineMap();
+        await this.getOnlineUsers();
 
-            searchInbounds(key) {
-                if (ObjectUtil.isEmpty(key)) {
-                    this.searchedInbounds = this.dbInbounds.slice();
-                } else {
-                    this.searchedInbounds.splice(0, this.searchedInbounds.length);
-                    this.dbInbounds.forEach(inbound => {
-                        if (ObjectUtil.deepSearch(inbound, key)) {
-                            const newInbound = new DBInbound(inbound);
-                            const inboundSettings = JSON.parse(inbound.settings);
-                            if (inboundSettings.hasOwnProperty('clients')) {
-                                const searchedSettings = { "clients": [] };
-                                inboundSettings.clients.forEach(client => {
-                                    if (ObjectUtil.deepSearch(client, key)) {
-                                        searchedSettings.clients.push(client);
-                                    }
-                                });
-                                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
-                            }
-                            this.searchedInbounds.push(newInbound);
-                        }
-                    });
-                }
-            },
-            filterInbounds() {
-                if (ObjectUtil.isEmpty(this.filterBy)) {
-                    this.searchedInbounds = this.dbInbounds.slice();
-                } else {
-                    this.searchedInbounds.splice(0, this.searchedInbounds.length);
-                    this.dbInbounds.forEach(inbound => {
-                        const newInbound = new DBInbound(inbound);
-                        const inboundSettings = JSON.parse(inbound.settings);
-                        if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
-                            const list = this.clientCount[inbound.id][this.filterBy];
-                            if (list.length > 0) {
-                                const filteredSettings = { "clients": [] };
-                                if (inboundSettings.clients) {
-                                    inboundSettings.clients.forEach(client => {
-                                        if (list.includes(client.email)) {
-                                            filteredSettings.clients.push(client);
-                                        }
-                                    });
-                                }
-                                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
-                                this.searchedInbounds.push(newInbound);
-                            }
-                        }
-                    });
-                }
-            },
-            toggleFilter(){
-                if(this.enableFilter) {
-                    this.searchKey = '';
-                } else {
-                    this.filterBy = '';
-                    this.searchedInbounds = this.dbInbounds.slice();
-                }
-            },
-            generalActions(action) {
-                switch (action.key) {
-                    case "import":
-                        this.importInbound();
-                        break;
-                    case "export":
-                        this.exportAllLinks();
-                        break;
-                    case "subs":
-                        this.exportAllSubs();
-                        break;
-                    case "resetInbounds":
-                        this.resetAllTraffic();
-                        break;
-                    case "resetClients":
-                        this.resetAllClientTraffics(-1);
-                        break;
-                    case "delDepletedClients":
-                        this.delDepletedClients(-1)
-                        break;
-                }
-            },
-            clickAction(action, dbInbound) {
-                switch (action.key) {
-                    case "qrcode":
-                        this.showQrcode(dbInbound.id);
-                        break;
-                    case "showInfo":
-                        this.showInfo(dbInbound.id);
-                        break;
-                    case "edit":
-                        this.openEditInbound(dbInbound.id);
-                        break;
-                    case "addClient":
-                        this.openAddClient(dbInbound.id)
-                        break;
-                    case "addBulkClient":
-                        this.openAddBulkClient(dbInbound.id)
-                        break;
-                    case "export":
-                        this.inboundLinks(dbInbound.id);
-                        break;
-                    case "subs":
-                        this.exportSubs(dbInbound.id);
-                        break;
-                    case "clipboard":
-                        this.copy(dbInbound.id);
-                        break;
-                    case "resetTraffic":
-                        this.resetTraffic(dbInbound.id);
-                        break;
-                    case "resetClients":
-                        this.resetAllClientTraffics(dbInbound.id);
-                        break;
-                    case "clone":
-                        this.openCloneInbound(dbInbound);
-                        break;
-                    case "delete":
-                        this.delInbound(dbInbound.id);
-                        break;
-                    case "delDepletedClients":
-                        this.delDepletedClients(dbInbound.id)
-                        break;
-                }
-            },
-            openCloneInbound(dbInbound) {
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
-                    content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
-                    okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
-                    class: themeSwitcher.currentTheme,
-                    cancelText: '{{ i18n "cancel" }}',
-                    onOk: () => {
-                        const baseInbound = dbInbound.toInbound();
-                        dbInbound.up = 0;
-                        dbInbound.down = 0;
-                        this.cloneInbound(baseInbound, dbInbound);
-                    },
-                });
-            },
-            async cloneInbound(baseInbound, dbInbound) {
-                const data = {
-                    up: dbInbound.up,
-                    down: dbInbound.down,
-                    total: dbInbound.total,
-                    remark: dbInbound.remark + " - Cloned",
-                    enable: dbInbound.enable,
-                    expiryTime: dbInbound.expiryTime,
-                    trafficReset: dbInbound.trafficReset,
-                    lastTrafficResetTime: dbInbound.lastTrafficResetTime,
+        this.setInbounds(msg.obj);
+        setTimeout(() => {
+          this.refreshing = false;
+        }, 500);
+      },
+      async getOnlineUsers() {
+        const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
+        if (!msg.success) {
+          return;
+        }
+        this.onlineClients = msg.obj != null ? msg.obj : [];
+      },
+      async getLastOnlineMap() {
+        const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
+        if (!msg.success || !msg.obj) return;
+        this.lastOnlineMap = msg.obj || {}
+      },
+      async getDefaultSettings() {
+        const msg = await HttpUtil.post('/panel/setting/defaultSettings');
+        if (!msg.success) {
+          return;
+        }
+        with (msg.obj) {
+          this.expireDiff = expireDiff * 86400000;
+          this.trafficDiff = trafficDiff * 1073741824;
+          this.defaultCert = defaultCert;
+          this.defaultKey = defaultKey;
+          this.tgBotEnable = tgBotEnable;
+          this.subSettings = {
+            enable: subEnable,
+            subTitle: subTitle,
+            subURI: subURI,
+            subJsonURI: subJsonURI
+          };
+          this.pageSize = pageSize;
+          this.remarkModel = remarkModel;
+          this.datepicker = datepicker;
+          this.ipLimitEnable = ipLimitEnable;
+        }
+      },
+      setInbounds(dbInbounds) {
+        this.inbounds.splice(0);
+        this.dbInbounds.splice(0);
+        this.clientCount.splice(0);
+        for (const inbound of dbInbounds) {
+          const dbInbound = new DBInbound(inbound);
+          to_inbound = dbInbound.toInbound()
+          this.inbounds.push(to_inbound);
+          this.dbInbounds.push(dbInbound);
+          if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
+            if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
+              continue;
+            }
+            this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
+          }
+        }
+        if (!this.loadingStates.fetched) {
+          this.loadingStates.fetched = true
+        }
+        if (this.enableFilter) {
+          this.filterInbounds();
+        } else {
+          this.searchInbounds(this.searchKey);
+        }
+      },
+      getClientCounts(dbInbound, inbound) {
+        let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [], comments = new Map();
+        clients = inbound.clients;
+        clientStats = dbInbound.clientStats
+        now = new Date().getTime()
+        if (clients) {
+          clientCount = clients.length;
+          if (dbInbound.enable) {
+            clients.forEach(client => {
+              if (client.comment) {
+                comments.set(client.email, client.comment)
+              }
+              if (client.enable) {
+                active.push(client.email);
+                if (this.isClientOnline(client.email)) online.push(client.email);
+              } else {
+                deactive.push(client.email);
+              }
+            });
+            clientStats.forEach(stats => {
+              const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
+              const expired = stats.expiryTime > 0 && stats.expiryTime <= now;
+              if (expired || exhausted) {
+                depleted.push(stats.email);
+              } else {
+                const expiringSoon = (stats.expiryTime > 0 && (stats.expiryTime - now < this.expireDiff)) ||
+                  (stats.total > 0 && (stats.total - (stats.up + stats.down) < this.trafficDiff));
+                if (expiringSoon) expiring.push(stats.email);
+              }
+            });
+          } else {
+            clients.forEach(client => {
+              deactive.push(client.email);
+            });
+          }
+        }
+        return {
+          clients: clientCount,
+          active: active,
+          deactive: deactive,
+          depleted: depleted,
+          expiring: expiring,
+          online: online,
+          comments: comments,
+        };
+      },
 
-                    listen: '',
-                    port: RandomUtil.randomInteger(10000, 60000),
-                    protocol: baseInbound.protocol,
-                    settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
-                    streamSettings: baseInbound.stream.toString(),
-                    sniffing: baseInbound.sniffing.toString(),
-                };
-                await this.submit('/panel/api/inbounds/add', data, inModal);
-            },
-            openAddInbound() {
-                inModal.show({
-                    title: '{{ i18n "pages.inbounds.addInbound"}}',
-                    okText: '{{ i18n "create"}}',
-                    cancelText: '{{ i18n "close" }}',
-                    confirm: async (inbound, dbInbound) => {
-                        await this.addInbound(inbound, dbInbound, inModal);
-                    },
-                    isEdit: false
-                });
-            },
-            openEditInbound(dbInboundId) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                const inbound = dbInbound.toInbound();
-                inModal.show({
-                    title: '{{ i18n "pages.inbounds.modifyInbound"}}',
-                    okText: '{{ i18n "update"}}',
-                    cancelText: '{{ i18n "close" }}',
-                    inbound: inbound,
-                    dbInbound: dbInbound,
-                    confirm: async (inbound, dbInbound) => {
-                        await this.updateInbound(inbound, dbInbound);
-                    },
-                    isEdit: true
+      searchInbounds(key) {
+        if (ObjectUtil.isEmpty(key)) {
+          this.searchedInbounds = this.dbInbounds.slice();
+        } else {
+          this.searchedInbounds.splice(0, this.searchedInbounds.length);
+          this.dbInbounds.forEach(inbound => {
+            if (ObjectUtil.deepSearch(inbound, key)) {
+              const newInbound = new DBInbound(inbound);
+              const inboundSettings = JSON.parse(inbound.settings);
+              if (inboundSettings.hasOwnProperty('clients')) {
+                const searchedSettings = { "clients": [] };
+                inboundSettings.clients.forEach(client => {
+                  if (ObjectUtil.deepSearch(client, key)) {
+                    searchedSettings.clients.push(client);
+                  }
                 });
-            },
-            async addInbound(inbound, dbInbound) {
-                const data = {
-                    up: dbInbound.up,
-                    down: dbInbound.down,
-                    total: dbInbound.total,
-                    remark: dbInbound.remark,
-                    enable: dbInbound.enable,
-                    expiryTime: dbInbound.expiryTime,
-                    trafficReset: dbInbound.trafficReset,
-                    lastTrafficResetTime: dbInbound.lastTrafficResetTime,
-
-                    listen: inbound.listen,
-                    port: inbound.port,
-                    protocol: inbound.protocol,
-                    settings: inbound.settings.toString(),
-                };
-                if (inbound.canEnableStream()){
-                  data.streamSettings = inbound.stream.toString();
-                } else if (inbound.stream?.sockopt) {
-                  data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
+                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
+              }
+              this.searchedInbounds.push(newInbound);
+            }
+          });
+        }
+      },
+      filterInbounds() {
+        if (ObjectUtil.isEmpty(this.filterBy)) {
+          this.searchedInbounds = this.dbInbounds.slice();
+        } else {
+          this.searchedInbounds.splice(0, this.searchedInbounds.length);
+          this.dbInbounds.forEach(inbound => {
+            const newInbound = new DBInbound(inbound);
+            const inboundSettings = JSON.parse(inbound.settings);
+            if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)) {
+              const list = this.clientCount[inbound.id][this.filterBy];
+              if (list.length > 0) {
+                const filteredSettings = { "clients": [] };
+                if (inboundSettings.clients) {
+                  inboundSettings.clients.forEach(client => {
+                    if (list.includes(client.email)) {
+                      filteredSettings.clients.push(client);
+                    }
+                  });
                 }
-                data.sniffing = inbound.sniffing.toString();
+                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
+                this.searchedInbounds.push(newInbound);
+              }
+            }
+          });
+        }
+      },
+      toggleFilter() {
+        if (this.enableFilter) {
+          this.searchKey = '';
+        } else {
+          this.filterBy = '';
+          this.searchedInbounds = this.dbInbounds.slice();
+        }
+      },
+      generalActions(action) {
+        switch (action.key) {
+          case "import":
+            this.importInbound();
+            break;
+          case "export":
+            this.exportAllLinks();
+            break;
+          case "subs":
+            this.exportAllSubs();
+            break;
+          case "resetInbounds":
+            this.resetAllTraffic();
+            break;
+          case "resetClients":
+            this.resetAllClientTraffics(-1);
+            break;
+          case "delDepletedClients":
+            this.delDepletedClients(-1)
+            break;
+        }
+      },
+      clickAction(action, dbInbound) {
+        switch (action.key) {
+          case "qrcode":
+            this.showQrcode(dbInbound.id);
+            break;
+          case "showInfo":
+            this.showInfo(dbInbound.id);
+            break;
+          case "edit":
+            this.openEditInbound(dbInbound.id);
+            break;
+          case "addClient":
+            this.openAddClient(dbInbound.id)
+            break;
+          case "addBulkClient":
+            this.openAddBulkClient(dbInbound.id)
+            break;
+          case "export":
+            this.inboundLinks(dbInbound.id);
+            break;
+          case "subs":
+            this.exportSubs(dbInbound.id);
+            break;
+          case "clipboard":
+            this.copy(dbInbound.id);
+            break;
+          case "resetTraffic":
+            this.resetTraffic(dbInbound.id);
+            break;
+          case "resetClients":
+            this.resetAllClientTraffics(dbInbound.id);
+            break;
+          case "clone":
+            this.openCloneInbound(dbInbound);
+            break;
+          case "delete":
+            this.delInbound(dbInbound.id);
+            break;
+          case "delDepletedClients":
+            this.delDepletedClients(dbInbound.id)
+            break;
+        }
+      },
+      openCloneInbound(dbInbound) {
+        this.$confirm({
+          title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
+          content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
+          okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
+          class: themeSwitcher.currentTheme,
+          cancelText: '{{ i18n "cancel" }}',
+          onOk: () => {
+            const baseInbound = dbInbound.toInbound();
+            dbInbound.up = 0;
+            dbInbound.down = 0;
+            this.cloneInbound(baseInbound, dbInbound);
+          },
+        });
+      },
+      async cloneInbound(baseInbound, dbInbound) {
+        const data = {
+          up: dbInbound.up,
+          down: dbInbound.down,
+          total: dbInbound.total,
+          remark: dbInbound.remark + " - Cloned",
+          enable: dbInbound.enable,
+          expiryTime: dbInbound.expiryTime,
+          trafficReset: dbInbound.trafficReset,
+          lastTrafficResetTime: dbInbound.lastTrafficResetTime,
 
-                await this.submit('/panel/api/inbounds/add', data, inModal);
-            },
-            async updateInbound(inbound, dbInbound) {
-                const data = {
-                    up: dbInbound.up,
-                    down: dbInbound.down,
-                    total: dbInbound.total,
-                    remark: dbInbound.remark,
-                    enable: dbInbound.enable,
-                    expiryTime: dbInbound.expiryTime,
-                    trafficReset: dbInbound.trafficReset,
-                    lastTrafficResetTime: dbInbound.lastTrafficResetTime,
+          listen: '',
+          port: RandomUtil.randomInteger(10000, 60000),
+          protocol: baseInbound.protocol,
+          settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
+          streamSettings: baseInbound.stream.toString(),
+          sniffing: baseInbound.sniffing.toString(),
+        };
+        await this.submit('/panel/api/inbounds/add', data, inModal);
+      },
+      openAddInbound() {
+        inModal.show({
+          title: '{{ i18n "pages.inbounds.addInbound"}}',
+          okText: '{{ i18n "create"}}',
+          cancelText: '{{ i18n "close" }}',
+          confirm: async (inbound, dbInbound) => {
+            await this.addInbound(inbound, dbInbound, inModal);
+          },
+          isEdit: false
+        });
+      },
+      openEditInbound(dbInboundId) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        const inbound = dbInbound.toInbound();
+        inModal.show({
+          title: '{{ i18n "pages.inbounds.modifyInbound"}}',
+          okText: '{{ i18n "update"}}',
+          cancelText: '{{ i18n "close" }}',
+          inbound: inbound,
+          dbInbound: dbInbound,
+          confirm: async (inbound, dbInbound) => {
+            await this.updateInbound(inbound, dbInbound);
+          },
+          isEdit: true
+        });
+      },
+      async addInbound(inbound, dbInbound) {
+        const data = {
+          up: dbInbound.up,
+          down: dbInbound.down,
+          total: dbInbound.total,
+          remark: dbInbound.remark,
+          enable: dbInbound.enable,
+          expiryTime: dbInbound.expiryTime,
+          trafficReset: dbInbound.trafficReset,
+          lastTrafficResetTime: dbInbound.lastTrafficResetTime,
 
-                    listen: inbound.listen,
-                    port: inbound.port,
-                    protocol: inbound.protocol,
-                    settings: inbound.settings.toString(),
-                };
-                if (inbound.canEnableStream()){
-                  data.streamSettings = inbound.stream.toString();
-                } else if (inbound.stream?.sockopt) {
-                  data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
-                }
-                data.sniffing = inbound.sniffing.toString();
+          listen: inbound.listen,
+          port: inbound.port,
+          protocol: inbound.protocol,
+          settings: inbound.settings.toString(),
+        };
+        if (inbound.canEnableStream()) {
+          data.streamSettings = inbound.stream.toString();
+        } else if (inbound.stream?.sockopt) {
+          data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
+        }
+        data.sniffing = inbound.sniffing.toString();
 
-                await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
-            },
-            openAddClient(dbInboundId) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                clientModal.show({
-                    title: '{{ i18n "pages.client.add"}}',
-                    okText: '{{ i18n "pages.client.submitAdd"}}',
-                    dbInbound: dbInbound,
-                    confirm: async (clients, dbInboundId) => {
-                        await this.addClient(clients, dbInboundId, clientModal);
-                    },
-                    isEdit: false
-                });
-            },
-            openAddBulkClient(dbInboundId) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                clientsBulkModal.show({
-                    title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
-                    okText: '{{ i18n "pages.client.bulk"}}',
-                    dbInbound: dbInbound,
-                    confirm: async (clients, dbInboundId) => {
-                        await this.addClient(clients, dbInboundId, clientsBulkModal);
-                    },
-                });
-            },
-            openEditClient(dbInboundId, client) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                clients = this.getInboundClients(dbInbound);
-                index = this.findIndexOfClient(dbInbound.protocol, clients, client);
-                clientModal.show({
-                    title: '{{ i18n "pages.client.edit"}}',
-                    okText: '{{ i18n "pages.client.submitEdit"}}',
-                    dbInbound: dbInbound,
-                    index: index,
-                    confirm: async (client, dbInboundId, clientId) => {
-                        clientModal.loading();
-                        await this.updateClient(client, dbInboundId, clientId);
-                        clientModal.close();
-                    },
-                    isEdit: true
-                });
-            },
-            findIndexOfClient(protocol, clients, client) {
-                switch (protocol) {
-                    case Protocols.TROJAN:
-                    case Protocols.SHADOWSOCKS:
-                        return clients.findIndex(item => item.password === client.password && item.email === client.email);
-                    default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
-                }
-            },
-            async addClient(clients, dbInboundId, modal) {
-                const data = {
-                    id: dbInboundId,
-                    settings: '{"clients": [' + clients.toString() + ']}',
-                };
-                await this.submit(`/panel/api/inbounds/addClient`, data, modal);
-            },
-            async updateClient(client, dbInboundId, clientId) {
-                const data = {
-                    id: dbInboundId,
-                    settings: '{"clients": [' + client.toString() + ']}',
-                };
-                await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
-            },
-            resetTraffic(dbInboundId) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
-                    content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
-                    class: themeSwitcher.currentTheme,
-                    okText: '{{ i18n "reset"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => {
-                        const inbound = dbInbound.toInbound();
-                        dbInbound.up = 0;
-                        dbInbound.down = 0;
-                        this.updateInbound(inbound, dbInbound);
-                    },
-                });
-            },
-            delInbound(dbInboundId) {
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
-                    content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
-                    class: themeSwitcher.currentTheme,
-                    okText: '{{ i18n "delete"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
-                });
-            },
-            delClient(dbInboundId, client,confirmation = true) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                clientId = this.getClientId(dbInbound.protocol, client);
-                if (confirmation){
-                    this.$confirm({
-                        title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
-                        content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
-                        class: themeSwitcher.currentTheme,
-                        okText: '{{ i18n "delete"}}',
-                        cancelText: '{{ i18n "cancel"}}',
-                        onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
-                    });
-                } else {
-                    this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
-                }
-            },
-            getSubGroupClients(dbInbounds, currentClient) {
-                const response = {
-                  inbounds: [],
-                  clients: [],
-                  editIds: []
-                }
-                if (dbInbounds && dbInbounds.length > 0 && currentClient) {
-                    dbInbounds.forEach((dbInboundItem) => {
-                        const dbInbound = new DBInbound(dbInboundItem);
-                        if (dbInbound) {
-                            const inbound = dbInbound.toInbound();
-                            if (inbound) {
-                                const clients = inbound.clients;
-                                if (clients.length > 0) {
-                                    clients.forEach((client) => {
-                                        if (client['subId'] === currentClient['subId']) {
-                                            client['inboundId'] = dbInboundItem.id
-                                            client['clientId'] = this.getClientId(dbInbound.protocol, client)
-                                            response.inbounds.push(dbInboundItem.id)
-                                            response.clients.push(client)
-                                            response.editIds.push(client['clientId'])
-                                        }
-                                    })
-                                }
-                            }
-                        }
-                    })
-                }
-                return response;
-            },
-            getClientId(protocol, client) {
-                switch (protocol) {
-                    case Protocols.TROJAN: return client.password;
-                    case Protocols.SHADOWSOCKS: return client.email;
-                    default: return client.id;
-                }
-            },
-            checkFallback(dbInbound) {
-                newDbInbound = new DBInbound(dbInbound);
-                if (dbInbound.listen.startsWith("@")){
-                    rootInbound = this.inbounds.find((i) => 
-                        i.isTcp && 
-                        ['trojan','vless'].includes(i.protocol) &&
-                        i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
-                    );
-                    if (rootInbound) {
-                        newDbInbound.listen = rootInbound.listen;
-                        newDbInbound.port = rootInbound.port;
-                        newInbound = newDbInbound.toInbound();
-                        newInbound.stream.security = rootInbound.stream.security;
-                        newInbound.stream.tls = rootInbound.stream.tls;
-                        newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
-                        newDbInbound.streamSettings = newInbound.stream.toString();
-                    }
-                }
-                return newDbInbound;
-            },
-            showQrcode(dbInboundId, client) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                newDbInbound = this.checkFallback(dbInbound);
-                qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
-            },
-            showInfo(dbInboundId, client) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                index=0;
-                if (dbInbound.isMultiUser()){
-                    inbound = dbInbound.toInbound();
-                    clients = inbound.clients;
-                    index = this.findIndexOfClient(dbInbound.protocol, clients, client);
-                }
-                newDbInbound = this.checkFallback(dbInbound);
-                infoModal.show(newDbInbound, index);
-            },
-            switchEnable(dbInboundId,state) {
-              dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-              dbInbound.enable = state;
-              this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
-            },
-            async switchEnableClient(dbInboundId, client) {
-                this.loading()
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                inbound = dbInbound.toInbound();
-                clients = inbound.clients;
-                index = this.findIndexOfClient(dbInbound.protocol, clients, client);
-                clients[index].enable = !clients[index].enable;
-                clientId = this.getClientId(dbInbound.protocol, clients[index]);
-                await this.updateClient(clients[index], dbInboundId, clientId);
-                this.loading(false);
-            },
-            async submit(url, data, modal) {
-                const msg = await HttpUtil.postWithModal(url, data, modal);
-                if (msg.success) {
-                    await this.getDBInbounds();
-                }
-            },
-            getInboundClients(dbInbound) {
-                return dbInbound.toInbound().clients;
-            },
-            resetClientTraffic(client, dbInboundId, confirmation = true) {
-                if (confirmation){
-                    this.$confirm({
-                        title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
-                        content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
-                        class: themeSwitcher.currentTheme,
-                        okText: '{{ i18n "reset"}}',
-                        cancelText: '{{ i18n "cancel"}}',
-                        onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
-                    })
-                } else {
-                    this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
-                }
-            },
-            resetAllTraffic() {
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
-                    content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
-                    class: themeSwitcher.currentTheme,
-                    okText: '{{ i18n "reset"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
-                });
-            },
-            resetAllClientTraffics(dbInboundId) {
-                this.$confirm({
-                    title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
-                    content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
-                    class: themeSwitcher.currentTheme,
-                    okText: '{{ i18n "reset"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
-                })
-            },
-            delDepletedClients(dbInboundId) {
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
-                    content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
-                    class: themeSwitcher.currentTheme,
-                    okText: '{{ i18n "delete"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
-                })
-            },
-            isExpiry(dbInbound, index) {
-                return dbInbound.toInbound().isExpiry(index);
-            },
-            getUpStats(dbInbound, email) {
-                if (email.length == 0) return 0;
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                return clientStats ? clientStats.up : 0;
-            },
-            getDownStats(dbInbound, email) {
-                if (email.length == 0) return 0;
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                return clientStats ? clientStats.down : 0;
-            },
-            getSumStats(dbInbound, email) {
-                if (email.length == 0) return 0;
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                return clientStats ? clientStats.up + clientStats.down : 0;
-            },
-            getAllTimeClient(dbInbound, email) {
-                if (email.length == 0) return 0;
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                if (!clientStats) return 0;
-                return clientStats.allTime || (clientStats.up + clientStats.down);
-            },
-            getRemStats(dbInbound, email) {
-                if (email.length == 0) return 0;
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                if (!clientStats) return 0;
-                remained = clientStats.total - (clientStats.up + clientStats.down);
-                return remained>0 ? remained : 0;
-            },
-            clientStatsColor(dbInbound, email) {
-                if (email.length == 0) return ColorUtils.clientUsageColor();
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
-            },
-            statsProgress(dbInbound, email) {
-                if (email.length == 0) return 100;
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                if (!clientStats) return 0;
-                if (clientStats.total == 0) return 100;
-                return 100*(clientStats.down + clientStats.up)/clientStats.total;
-            },
-            expireProgress(expTime, reset) {
-                now = new Date().getTime();
-                remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000;
-                resetSeconds = reset * 86400;
-                if (remainedSeconds >= resetSeconds) return 0;
-                return 100*(1-(remainedSeconds/resetSeconds));
-            },
-            remainedDays(expTime){
-                if (expTime == 0) return null;
-                if (expTime < 0) return TimeFormatter.formatSecond(expTime/-1000);
-                now = new Date().getTime();
-                if (expTime < now) return '{{ i18n "depleted" }}';
-                return TimeFormatter.formatSecond((expTime-now)/1000);
-            },
-            statsExpColor(dbInbound, email){
-                if (email.length == 0) return '#7a316f';
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                if (!clientStats) return '#7a316f';
-                statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
-                expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
-                switch (true) {
-                    case statsColor == "red" || expColor == "red":
-                        return "#cf3c3c"; // Red
-                    case statsColor == "orange" || expColor == "orange":
-                        return "#f37b24"; // Orange
-                    case statsColor == "green" || expColor == "green":
-                        return "#008771"; // Green
-                    default:
-                        return "#7a316f"; // purple
-                }
-            },
-            isClientEnabled(dbInbound, email) {
-                clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
-                return clientStats ? clientStats['enable'] : true;
-            },
-            isClientOnline(email) {
-                return this.onlineClients.includes(email);
-            },
-            getLastOnline(email) {
-                return this.lastOnlineMap[email] || null
-            },
-            formatLastOnline(email) {
-                const ts = this.getLastOnline(email)
-                if (!ts) return '-'
-                if (this.datepicker === 'gregorian') {
-                    return DateUtil.formatMillis(ts)
-                }
-                return DateUtil.convertToJalalian(moment(ts))
-            },
-            isRemovable(dbInboundId) {
-                return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
-            },
-            inboundLinks(dbInboundId) {
-                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                newDbInbound = this.checkFallback(dbInbound);
-                txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);
-            },
-            exportSubs(dbInboundId) {
-                const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                const clients = this.getInboundClients(dbInbound);
-                let subLinks = []
-                if (clients != null){
-                    clients.forEach(c => {
-                        if (c.subId && c.subId.length>0){
-                            subLinks.push(this.subSettings.subURI + c.subId)
-                        }
-                    })
-                }
-                txtModal.show(
-                    '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
-                    [...new Set(subLinks)].join('\n'),
-                    dbInbound.remark + "-Subs");
-            },
-            importInbound() {
-                promptModal.open({
-                    title: '{{ i18n "pages.inbounds.importInbound" }}',
-                    type: 'textarea',
-                    value: '',
-                    okText: '{{ i18n "pages.inbounds.import" }}',
-                    confirm: async (dbInboundText) => {
-                        await this.submit('/panel/api/inbounds/import', {data: dbInboundText}, promptModal);
-                    },
-                });
-            },
-            exportAllSubs() {
-                let subLinks = []
-                for (const dbInbound of this.dbInbounds) {
-                    const clients = this.getInboundClients(dbInbound);
-                    if (clients != null){
-                        clients.forEach(c => {
-                            if (c.subId && c.subId.length>0){
-                                subLinks.push(this.subSettings.subURI + c.subId)
-                            }
-                        })
-                    }
-                }
-                txtModal.show(
-                    '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
-                    [...new Set(subLinks)].join('\r\n'),
-                    'All-Inbounds-Subs');
-            },
-            exportAllLinks() {
-                let copyText = [];
-                for (const dbInbound of this.dbInbounds) {
-                    copyText.push(dbInbound.genInboundLinks(this.remarkModel));
-                }
-                txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
-            },
-            copy(dbInboundId) {
-              dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-              txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
-            },
-            async startDataRefreshLoop() {
-                while (this.isRefreshEnabled) {
-                    try {
-                        await this.getDBInbounds();
-                    } catch (e) {
-                        console.error(e);
-                    }
-                    await PromiseUtil.sleep(this.refreshInterval);
-                }
-            },
-            toggleRefresh() {
-                localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
-                if (this.isRefreshEnabled) {
-                    this.startDataRefreshLoop();
-                }
-            },
-            changeRefreshInterval() {
-                localStorage.setItem("refreshInterval", this.refreshInterval);
-            },
-            async manualRefresh() {
-                if (!this.refreshing) {
-                    this.loadingStates.spinning = true;
-                    await this.getDBInbounds();
-                    this.loadingStates.spinning = false;
-                }
-            },
-            pagination(obj){
-                if (this.pageSize > 0 && obj.length>this.pageSize) {
-                    // Set page options based on object size
-                    sizeOptions = [];
-                    for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
-                        sizeOptions.push(i.toString());
-                    }
-                    // Add option to see all in one page
-                    sizeOptions.push(i.toString());
+        await this.submit('/panel/api/inbounds/add', data, inModal);
+      },
+      async updateInbound(inbound, dbInbound) {
+        const data = {
+          up: dbInbound.up,
+          down: dbInbound.down,
+          total: dbInbound.total,
+          remark: dbInbound.remark,
+          enable: dbInbound.enable,
+          expiryTime: dbInbound.expiryTime,
+          trafficReset: dbInbound.trafficReset,
+          lastTrafficResetTime: dbInbound.lastTrafficResetTime,
+
+          listen: inbound.listen,
+          port: inbound.port,
+          protocol: inbound.protocol,
+          settings: inbound.settings.toString(),
+        };
+        if (inbound.canEnableStream()) {
+          data.streamSettings = inbound.stream.toString();
+        } else if (inbound.stream?.sockopt) {
+          data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
+        }
+        data.sniffing = inbound.sniffing.toString();
 
-                    p = {
-                        showSizeChanger: true,
-                        size: 'small',
-                        position: 'bottom',
-                        pageSize: this.pageSize,
-                        pageSizeOptions: sizeOptions
-                    };
-                    return p;
+        await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
+      },
+      openAddClient(dbInboundId) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        clientModal.show({
+          title: '{{ i18n "pages.client.add"}}',
+          okText: '{{ i18n "pages.client.submitAdd"}}',
+          dbInbound: dbInbound,
+          confirm: async (clients, dbInboundId) => {
+            await this.addClient(clients, dbInboundId, clientModal);
+          },
+          isEdit: false
+        });
+      },
+      openAddBulkClient(dbInboundId) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        clientsBulkModal.show({
+          title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
+          okText: '{{ i18n "pages.client.bulk"}}',
+          dbInbound: dbInbound,
+          confirm: async (clients, dbInboundId) => {
+            await this.addClient(clients, dbInboundId, clientsBulkModal);
+          },
+        });
+      },
+      openEditClient(dbInboundId, client) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        clients = this.getInboundClients(dbInbound);
+        index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+        clientModal.show({
+          title: '{{ i18n "pages.client.edit"}}',
+          okText: '{{ i18n "pages.client.submitEdit"}}',
+          dbInbound: dbInbound,
+          index: index,
+          confirm: async (client, dbInboundId, clientId) => {
+            clientModal.loading();
+            await this.updateClient(client, dbInboundId, clientId);
+            clientModal.close();
+          },
+          isEdit: true
+        });
+      },
+      findIndexOfClient(protocol, clients, client) {
+        switch (protocol) {
+          case Protocols.TROJAN:
+          case Protocols.SHADOWSOCKS:
+            return clients.findIndex(item => item.password === client.password && item.email === client.email);
+          default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
+        }
+      },
+      async addClient(clients, dbInboundId, modal) {
+        const data = {
+          id: dbInboundId,
+          settings: '{"clients": [' + clients.toString() + ']}',
+        };
+        await this.submit(`/panel/api/inbounds/addClient`, data, modal);
+      },
+      async updateClient(client, dbInboundId, clientId) {
+        const data = {
+          id: dbInboundId,
+          settings: '{"clients": [' + client.toString() + ']}',
+        };
+        await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
+      },
+      resetTraffic(dbInboundId) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        this.$confirm({
+          title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
+          content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
+          class: themeSwitcher.currentTheme,
+          okText: '{{ i18n "reset"}}',
+          cancelText: '{{ i18n "cancel"}}',
+          onOk: () => {
+            const inbound = dbInbound.toInbound();
+            dbInbound.up = 0;
+            dbInbound.down = 0;
+            this.updateInbound(inbound, dbInbound);
+          },
+        });
+      },
+      delInbound(dbInboundId) {
+        this.$confirm({
+          title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
+          content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
+          class: themeSwitcher.currentTheme,
+          okText: '{{ i18n "delete"}}',
+          cancelText: '{{ i18n "cancel"}}',
+          onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
+        });
+      },
+      delClient(dbInboundId, client, confirmation = true) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        clientId = this.getClientId(dbInbound.protocol, client);
+        if (confirmation) {
+          this.$confirm({
+            title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
+            content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
+            class: themeSwitcher.currentTheme,
+            okText: '{{ i18n "delete"}}',
+            cancelText: '{{ i18n "cancel"}}',
+            onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
+          });
+        } else {
+          this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
+        }
+      },
+      getSubGroupClients(dbInbounds, currentClient) {
+        const response = {
+          inbounds: [],
+          clients: [],
+          editIds: []
+        }
+        if (dbInbounds && dbInbounds.length > 0 && currentClient) {
+          dbInbounds.forEach((dbInboundItem) => {
+            const dbInbound = new DBInbound(dbInboundItem);
+            if (dbInbound) {
+              const inbound = dbInbound.toInbound();
+              if (inbound) {
+                const clients = inbound.clients;
+                if (clients.length > 0) {
+                  clients.forEach((client) => {
+                    if (client['subId'] === currentClient['subId']) {
+                      client['inboundId'] = dbInboundItem.id
+                      client['clientId'] = this.getClientId(dbInbound.protocol, client)
+                      response.inbounds.push(dbInboundItem.id)
+                      response.clients.push(client)
+                      response.editIds.push(client['clientId'])
+                    }
+                  })
                 }
-                return false
-            }
-        },
-        watch: {
-            searchKey: Utils.debounce(function (newVal) {
-                this.searchInbounds(newVal);
-            }, 500)
-        },
-        mounted() {
-            if (window.location.protocol !== "https:") {
-                this.showAlert = true;
+              }
             }
-            this.loading();
-            this.getDefaultSettings();
-            if (this.isRefreshEnabled) {
-                this.startDataRefreshLoop();
+          })
+        }
+        return response;
+      },
+      getClientId(protocol, client) {
+        switch (protocol) {
+          case Protocols.TROJAN: return client.password;
+          case Protocols.SHADOWSOCKS: return client.email;
+          default: return client.id;
+        }
+      },
+      checkFallback(dbInbound) {
+        newDbInbound = new DBInbound(dbInbound);
+        if (dbInbound.listen.startsWith("@")) {
+          rootInbound = this.inbounds.find((i) =>
+            i.isTcp &&
+            ['trojan', 'vless'].includes(i.protocol) &&
+            i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
+          );
+          if (rootInbound) {
+            newDbInbound.listen = rootInbound.listen;
+            newDbInbound.port = rootInbound.port;
+            newInbound = newDbInbound.toInbound();
+            newInbound.stream.security = rootInbound.stream.security;
+            newInbound.stream.tls = rootInbound.stream.tls;
+            newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
+            newDbInbound.streamSettings = newInbound.stream.toString();
+          }
+        }
+        return newDbInbound;
+      },
+      showQrcode(dbInboundId, client) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        newDbInbound = this.checkFallback(dbInbound);
+        qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
+      },
+      showInfo(dbInboundId, client) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        index = 0;
+        if (dbInbound.isMultiUser()) {
+          inbound = dbInbound.toInbound();
+          clients = inbound.clients;
+          index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+        }
+        newDbInbound = this.checkFallback(dbInbound);
+        infoModal.show(newDbInbound, index);
+      },
+      switchEnable(dbInboundId, state) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        dbInbound.enable = state;
+        this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
+      },
+      async switchEnableClient(dbInboundId, client) {
+        this.loading()
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        inbound = dbInbound.toInbound();
+        clients = inbound.clients;
+        index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+        clients[index].enable = !clients[index].enable;
+        clientId = this.getClientId(dbInbound.protocol, clients[index]);
+        await this.updateClient(clients[index], dbInboundId, clientId);
+        this.loading(false);
+      },
+      async submit(url, data, modal) {
+        const msg = await HttpUtil.postWithModal(url, data, modal);
+        if (msg.success) {
+          await this.getDBInbounds();
+        }
+      },
+      getInboundClients(dbInbound) {
+        return dbInbound.toInbound().clients;
+      },
+      resetClientTraffic(client, dbInboundId, confirmation = true) {
+        if (confirmation) {
+          this.$confirm({
+            title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
+            content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
+            class: themeSwitcher.currentTheme,
+            okText: '{{ i18n "reset"}}',
+            cancelText: '{{ i18n "cancel"}}',
+            onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
+          })
+        } else {
+          this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
+        }
+      },
+      resetAllTraffic() {
+        this.$confirm({
+          title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
+          content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
+          class: themeSwitcher.currentTheme,
+          okText: '{{ i18n "reset"}}',
+          cancelText: '{{ i18n "cancel"}}',
+          onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
+        });
+      },
+      resetAllClientTraffics(dbInboundId) {
+        this.$confirm({
+          title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
+          content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
+          class: themeSwitcher.currentTheme,
+          okText: '{{ i18n "reset"}}',
+          cancelText: '{{ i18n "cancel"}}',
+          onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
+        })
+      },
+      delDepletedClients(dbInboundId) {
+        this.$confirm({
+          title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
+          content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
+          class: themeSwitcher.currentTheme,
+          okText: '{{ i18n "delete"}}',
+          cancelText: '{{ i18n "cancel"}}',
+          onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
+        })
+      },
+      isExpiry(dbInbound, index) {
+        return dbInbound.toInbound().isExpiry(index);
+      },
+      getUpStats(dbInbound, email) {
+        if (email.length == 0) return 0;
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        return clientStats ? clientStats.up : 0;
+      },
+      getDownStats(dbInbound, email) {
+        if (email.length == 0) return 0;
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        return clientStats ? clientStats.down : 0;
+      },
+      getSumStats(dbInbound, email) {
+        if (email.length == 0) return 0;
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        return clientStats ? clientStats.up + clientStats.down : 0;
+      },
+      getAllTimeClient(dbInbound, email) {
+        if (email.length == 0) return 0;
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        if (!clientStats) return 0;
+        return clientStats.allTime || (clientStats.up + clientStats.down);
+      },
+      getRemStats(dbInbound, email) {
+        if (email.length == 0) return 0;
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        if (!clientStats) return 0;
+        remained = clientStats.total - (clientStats.up + clientStats.down);
+        return remained > 0 ? remained : 0;
+      },
+      clientStatsColor(dbInbound, email) {
+        if (email.length == 0) return ColorUtils.clientUsageColor();
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
+      },
+      statsProgress(dbInbound, email) {
+        if (email.length == 0) return 100;
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        if (!clientStats) return 0;
+        if (clientStats.total == 0) return 100;
+        return 100 * (clientStats.down + clientStats.up) / clientStats.total;
+      },
+      expireProgress(expTime, reset) {
+        now = new Date().getTime();
+        remainedSeconds = expTime < 0 ? -expTime / 1000 : (expTime - now) / 1000;
+        resetSeconds = reset * 86400;
+        if (remainedSeconds >= resetSeconds) return 0;
+        return 100 * (1 - (remainedSeconds / resetSeconds));
+      },
+      remainedDays(expTime) {
+        if (expTime == 0) return null;
+        if (expTime < 0) return TimeFormatter.formatSecond(expTime / -1000);
+        now = new Date().getTime();
+        if (expTime < now) return '{{ i18n "depleted" }}';
+        return TimeFormatter.formatSecond((expTime - now) / 1000);
+      },
+      statsExpColor(dbInbound, email) {
+        if (email.length == 0) return '#7a316f';
+        clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+        if (!clientStats) return '#7a316f';
+        statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
+        expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
+        switch (true) {
+          case statsColor == "red" || expColor == "red":
+            return "#cf3c3c"; // Red
+          case statsColor == "orange" || expColor == "orange":
+            return "#f37b24"; // Orange
+          case statsColor == "green" || expColor == "green":
+            return "#008771"; // Green
+          default:
+            return "#7a316f"; // purple
+        }
+      },
+      isClientEnabled(dbInbound, email) {
+        clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
+        return clientStats ? clientStats['enable'] : true;
+      },
+      // Returns true when client's traffic is exhausted or expiry time is passed
+      isClientDepleted(dbInbound, email) {
+        if (!email || !dbInbound || !dbInbound.clientStats) return false;
+        const stats = dbInbound.clientStats.find(s => s.email === email);
+        if (!stats) return false;
+        const now = new Date().getTime();
+        const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
+        const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
+        return exhausted || expired;
+      },
+      isClientOnline(email) {
+        return this.onlineClients.includes(email);
+      },
+      getLastOnline(email) {
+        return this.lastOnlineMap[email] || null
+      },
+      formatLastOnline(email) {
+        const ts = this.getLastOnline(email)
+        if (!ts) return '-'
+        if (this.datepicker === 'gregorian') {
+          return DateUtil.formatMillis(ts)
+        }
+        return DateUtil.convertToJalalian(moment(ts))
+      },
+      isRemovable(dbInboundId) {
+        return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
+      },
+      inboundLinks(dbInboundId) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        newDbInbound = this.checkFallback(dbInbound);
+        txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);
+      },
+      exportSubs(dbInboundId) {
+        const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        const clients = this.getInboundClients(dbInbound);
+        let subLinks = []
+        if (clients != null) {
+          clients.forEach(c => {
+            if (c.subId && c.subId.length > 0) {
+              subLinks.push(this.subSettings.subURI + c.subId)
             }
-            else {
-                this.getDBInbounds();
-            }
-            this.loading(false);
-        },
-        computed: {
-            total() {
-                let down = 0, up = 0, allTime = 0;
-                let clients = 0, deactive = [], depleted = [], expiring = [];
-                this.dbInbounds.forEach(dbInbound => {
-                    down += dbInbound.down;
-                    up += dbInbound.up;
-                    allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
-                    if (this.clientCount[dbInbound.id]) {
-                        clients += this.clientCount[dbInbound.id].clients;
-                        deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
-                        depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
-                        expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
-                    }
-                });
-                return {
-                    down: down,
-                    up: up,
-                    allTime: allTime,
-                    clients: clients,
-                    deactive: deactive,
-                    depleted: depleted,
-                    expiring: expiring,
-                };
-            }
-        },
-    });
+          })
+        }
+        txtModal.show(
+          '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
+          [...new Set(subLinks)].join('\n'),
+          dbInbound.remark + "-Subs");
+      },
+      importInbound() {
+        promptModal.open({
+          title: '{{ i18n "pages.inbounds.importInbound" }}',
+          type: 'textarea',
+          value: '',
+          okText: '{{ i18n "pages.inbounds.import" }}',
+          confirm: async (dbInboundText) => {
+            await this.submit('/panel/api/inbounds/import', { data: dbInboundText }, promptModal);
+          },
+        });
+      },
+      exportAllSubs() {
+        let subLinks = []
+        for (const dbInbound of this.dbInbounds) {
+          const clients = this.getInboundClients(dbInbound);
+          if (clients != null) {
+            clients.forEach(c => {
+              if (c.subId && c.subId.length > 0) {
+                subLinks.push(this.subSettings.subURI + c.subId)
+              }
+            })
+          }
+        }
+        txtModal.show(
+          '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
+          [...new Set(subLinks)].join('\r\n'),
+          'All-Inbounds-Subs');
+      },
+      exportAllLinks() {
+        let copyText = [];
+        for (const dbInbound of this.dbInbounds) {
+          copyText.push(dbInbound.genInboundLinks(this.remarkModel));
+        }
+        txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
+      },
+      copy(dbInboundId) {
+        dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+        txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
+      },
+      async startDataRefreshLoop() {
+        while (this.isRefreshEnabled) {
+          try {
+            await this.getDBInbounds();
+          } catch (e) {
+            console.error(e);
+          }
+          await PromiseUtil.sleep(this.refreshInterval);
+        }
+      },
+      toggleRefresh() {
+        localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
+        if (this.isRefreshEnabled) {
+          this.startDataRefreshLoop();
+        }
+      },
+      changeRefreshInterval() {
+        localStorage.setItem("refreshInterval", this.refreshInterval);
+      },
+      async manualRefresh() {
+        if (!this.refreshing) {
+          this.loadingStates.spinning = true;
+          await this.getDBInbounds();
+          this.loadingStates.spinning = false;
+        }
+      },
+      pagination(obj) {
+        if (this.pageSize > 0 && obj.length > this.pageSize) {
+          // Set page options based on object size
+          sizeOptions = [];
+          for (i = this.pageSize; i <= obj.length; i = i + this.pageSize) {
+            sizeOptions.push(i.toString());
+          }
+          // Add option to see all in one page
+          sizeOptions.push(i.toString());
+
+          p = {
+            showSizeChanger: true,
+            size: 'small',
+            position: 'bottom',
+            pageSize: this.pageSize,
+            pageSizeOptions: sizeOptions
+          };
+          return p;
+        }
+        return false
+      }
+    },
+    watch: {
+      searchKey: Utils.debounce(function (newVal) {
+        this.searchInbounds(newVal);
+      }, 500)
+    },
+    mounted() {
+      if (window.location.protocol !== "https:") {
+        this.showAlert = true;
+      }
+      this.loading();
+      this.getDefaultSettings();
+      if (this.isRefreshEnabled) {
+        this.startDataRefreshLoop();
+      }
+      else {
+        this.getDBInbounds();
+      }
+      this.loading(false);
+    },
+    computed: {
+      total() {
+        let down = 0, up = 0, allTime = 0;
+        let clients = 0, deactive = [], depleted = [], expiring = [];
+        this.dbInbounds.forEach(dbInbound => {
+          down += dbInbound.down;
+          up += dbInbound.up;
+          allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
+          if (this.clientCount[dbInbound.id]) {
+            clients += this.clientCount[dbInbound.id].clients;
+            deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
+            depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
+            expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
+          }
+        });
+        return {
+          down: down,
+          up: up,
+          allTime: allTime,
+          clients: clients,
+          deactive: deactive,
+          depleted: depleted,
+          expiring: expiring,
+        };
+      }
+    },
+  });
 </script>
 {{ template "page/body_end" .}}

+ 11 - 3
web/html/modals/inbound_info_modal.html

@@ -180,9 +180,9 @@
         <tr>
           <td>{{ i18n "status" }}</td>
           <td>
-            <a-tag v-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
-            <a-tag v-else>{{ i18n "disabled" }}</a-tag>
-            <a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
+            <a-tag v-if="isEnable && isActive && !isDepleted" color="green">{{ i18n "enabled" }}</a-tag>
+            <a-tag v-if="!isEnable && !isDepleted">{{ i18n "disabled" }}</a-tag>
+            <a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
           </td>
         </tr>
         <tr v-if="infoModal.clientStats">
@@ -587,6 +587,14 @@
         }
         return infoModal.dbInbound.isEnable;
       },
+      get isDepleted() {
+        const stats = this.infoModal.clientStats;
+        if (!stats) return false;
+        const now = new Date().getTime();
+        const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
+        const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
+        return expired || exhausted;
+      },
     },
     methods: {
       copy(content) {

+ 9 - 2
web/service/inbound.go

@@ -1837,8 +1837,14 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
 		whereText += "= ?"
 	}
 
+	// Only consider truly depleted clients: expired OR traffic exhausted
+	now := time.Now().Unix() * 1000
 	depletedClients := []xray.ClientTraffic{}
-	err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error
+	err = db.Model(xray.ClientTraffic{}).
+		Where(whereText+" and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))", id, now).
+		Select("inbound_id, GROUP_CONCAT(email) as email").
+		Group("inbound_id").
+		Find(&depletedClients).Error
 	if err != nil {
 		return err
 	}
@@ -1889,7 +1895,8 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
 		}
 	}
 
-	err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
+	// Delete stats only for truly depleted clients
+	err = tx.Where(whereText+" and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))", id, now).Delete(xray.ClientTraffic{}).Error
 	if err != nil {
 		return err
 	}