diff options
| -rw-r--r-- | web/controller/server.go | 61 | ||||
| -rw-r--r-- | web/controller/xray_setting.go | 8 | ||||
| -rw-r--r-- | web/html/index.html | 1001 | ||||
| -rw-r--r-- | web/service/server.go | 166 |
4 files changed, 631 insertions, 605 deletions
diff --git a/web/controller/server.go b/web/controller/server.go index 3b93afd9..169a1ae7 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -21,17 +21,14 @@ type ServerController struct { serverService service.ServerService settingService service.SettingService - lastStatus *service.Status - lastGetStatusTime time.Time + lastStatus *service.Status lastVersions []string - lastGetVersionsTime time.Time + lastGetVersionsTime int64 // unix seconds } func NewServerController(g *gin.RouterGroup) *ServerController { - a := &ServerController{ - lastGetStatusTime: time.Now(), - } + a := &ServerController{} a.initRouter(g) a.startTask() return a @@ -40,7 +37,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController { func (a *ServerController) initRouter(g *gin.RouterGroup) { g.GET("/status", a.status) - g.GET("/cpuHistory", a.getCpuHistory) + g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.GET("/getXrayVersion", a.getXrayVersion) g.GET("/getConfigJson", a.getConfigJson) g.GET("/getDb", a.getDb) @@ -79,35 +76,34 @@ func (a *ServerController) startTask() { }) } -func (a *ServerController) status(c *gin.Context) { - a.lastGetStatusTime = time.Now() - - jsonObj(c, a.lastStatus, nil) -} +func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) } -// getCpuHistory returns recent CPU utilization points. -// Query param q=minutes (int). Bounds: 1..360 (6 hours). Defaults to 60. -func (a *ServerController) getCpuHistory(c *gin.Context) { - minsStr := c.Query("q") - mins := 60 - if minsStr != "" { - if v, err := strconv.Atoi(minsStr); err == nil { - mins = v - } +func (a *ServerController) getCpuHistoryBucket(c *gin.Context) { + bucketStr := c.Param("bucket") + bucket, err := strconv.Atoi(bucketStr) + if err != nil || bucket <= 0 { + jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket")) + return } - if mins < 1 { - mins = 1 + allowed := map[int]bool{ + 2: true, // Real-time view + 30: true, // 30s intervals + 60: true, // 1m intervals + 120: true, // 2m intervals + 180: true, // 3m intervals + 300: true, // 5m intervals } - if mins > 360 { - mins = 360 + if !allowed[bucket] { + jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket")) + return } - res := a.serverService.GetCpuHistory(mins) - jsonObj(c, res, nil) + points := a.serverService.AggregateCpuHistory(bucket, 60) + jsonObj(c, points, nil) } func (a *ServerController) getXrayVersion(c *gin.Context) { - now := time.Now() - if now.Sub(a.lastGetVersionsTime) <= time.Minute { + now := time.Now().Unix() + if now-a.lastGetVersionsTime <= 60 { // 1 minute cache jsonObj(c, a.lastVersions, nil) return } @@ -119,7 +115,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { } a.lastVersions = versions - a.lastGetVersionsTime = time.Now() + a.lastGetVersionsTime = now jsonObj(c, versions, nil) } @@ -137,7 +133,6 @@ func (a *ServerController) updateGeofile(c *gin.Context) { } func (a *ServerController) stopXrayService(c *gin.Context) { - a.lastGetStatusTime = time.Now() err := a.serverService.StopXrayService() if err != nil { jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err) @@ -253,9 +248,7 @@ func (a *ServerController) importDB(c *gin.Context) { defer file.Close() // Always restart Xray before return defer a.serverService.RestartXrayService() - defer func() { - a.lastGetStatusTime = time.Now() - }() + // lastGetStatusTime removed; no longer needed // Import it err = a.serverService.ImportDB(file) if err != nil { diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go index 5b2d1036..2b5e0db1 100644 --- a/web/controller/xray_setting.go +++ b/web/controller/xray_setting.go @@ -23,13 +23,13 @@ func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { func (a *XraySettingController) initRouter(g *gin.RouterGroup) { g = g.Group("/xray") + g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) + g.GET("/getOutboundsTraffic", a.getOutboundsTraffic) + g.GET("/getXrayResult", a.getXrayResult) g.POST("/", a.getXraySetting) - g.POST("/update", a.updateSetting) - g.GET("/getXrayResult", a.getXrayResult) - g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.POST("/warp/:action", a.warp) - g.GET("/getOutboundsTraffic", a.getOutboundsTraffic) + g.POST("/update", a.updateSetting) g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) } diff --git a/web/html/index.html b/web/html/index.html index b6a9993e..819d6df2 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -9,10 +9,7 @@ <a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip"> <transition name="list" appear> <a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10" - 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> @@ -29,16 +26,16 @@ <a-col :sm="24" :md="12"> <a-row> <a-col :span="12" class="text-center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.cpu.color" + <a-progress type="dashboard" status="normal" :stroke-color="status.cpu.color" :percent="status.cpu.percent"></a-progress> <div> - <b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]] + <b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]] <a-tooltip> - <a-icon type="area-chart"></a-icon> + <a-icon type="area-chart"></a-icon> <template slot="title"> <div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div> - <div><b>{{ i18n "pages.index.frequency" }}:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div> + <div><b>{{ i18n "pages.index.frequency" }}:</b> [[ + CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div> </template> </a-tooltip> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> @@ -49,11 +46,11 @@ </div> </a-col> <a-col :span="12" class="text-center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.mem.color" + <a-progress type="dashboard" status="normal" :stroke-color="status.mem.color" :percent="status.mem.percent"></a-progress> <div> - <b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]] + <b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / + [[ SizeFormatter.sizeFormat(status.mem.total) ]] </div> </a-col> </a-row> @@ -61,19 +58,19 @@ <a-col :sm="24" :md="12"> <a-row> <a-col :span="12" class="text-center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.swap.color" + <a-progress type="dashboard" status="normal" :stroke-color="status.swap.color" :percent="status.swap.percent"></a-progress> <div> - <b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]] + <b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / + [[ SizeFormatter.sizeFormat(status.swap.total) ]] </div> </a-col> <a-col :span="12" class="text-center"> - <a-progress type="dashboard" status="normal" - :stroke-color="status.disk.color" + <a-progress type="dashboard" status="normal" :stroke-color="status.disk.color" :percent="status.disk.percent"></a-progress> <div> - <b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]] + <b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] + / [[ SizeFormatter.sizeFormat(status.disk.total) ]] </div> </a-col> </a-row> @@ -93,7 +90,9 @@ </template> <template #extra> <template v-if="status.xray.state != 'error'"> - <a-badge status="processing" :class="({ green: 'xray-running-animation', orange: 'xray-stop-animation' }[status.xray.color]) || 'xray-processing-animation'" :text="status.xray.stateMsg" :color="status.xray.color"/> + <a-badge status="processing" + :class="({ green: 'xray-running-animation', orange: 'xray-stop-animation' }[status.xray.color]) || 'xray-processing-animation'" + :text="status.xray.stateMsg" :color="status.xray.color" /> </template> <template v-else> <a-popover :overlay-class-name="themeSwitcher.currentTheme"> @@ -110,7 +109,8 @@ <template slot="content"> <span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span> </template> - <a-badge :text="status.xray.stateMsg" :color="status.xray.color" :class="status.xray.color === 'red' ? 'xray-error-animation' : ''"/> + <a-badge :text="status.xray.stateMsg" :color="status.xray.color" + :class="status.xray.color === 'red' ? 'xray-error-animation' : ''" /> </a-popover> </template> </template> @@ -130,7 +130,8 @@ <a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center"> <a-icon type="tool"></a-icon> <span v-if="!isMobile"> - [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]] + [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n + "pages.index.xraySwitch" }}' ]] </span> </a-space> </template> @@ -175,7 +176,8 @@ </a-col> <a-col :sm="24" :lg="12"> <a-card title='{{ i18n "pages.index.operationHours" }}' hoverable> - <a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag> + <a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) + ]]</a-tag> <a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag> </a-card> </a-col> @@ -193,7 +195,8 @@ </a-col> <a-col :sm="24" :lg="12"> <a-card title='{{ i18n "usage"}}' hoverable> - <a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag> + <a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ + SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag> <a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag> </a-card> </a-col> @@ -201,7 +204,8 @@ <a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable> <a-row :gutter="isMobile ? [8,8] : 0"> <a-col :span="12"> - <a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)"> + <a-custom-statistic title='{{ i18n "pages.index.upload" }}' + :value="SizeFormatter.sizeFormat(status.netIO.up)"> <template #prefix> <a-icon type="arrow-up" /> </template> @@ -211,7 +215,8 @@ </a-custom-statistic> </a-col> <a-col :span="12"> - <a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)"> + <a-custom-statistic title='{{ i18n "pages.index.download" }}' + :value="SizeFormatter.sizeFormat(status.netIO.down)"> <template #prefix> <a-icon type="arrow-down" /> </template> @@ -227,14 +232,16 @@ <a-card title='{{ i18n "pages.index.totalData" }}' hoverable> <a-row :gutter="isMobile ? [8,8] : 0"> <a-col :span="12"> - <a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)"> + <a-custom-statistic title='{{ i18n "pages.index.sent" }}' + :value="SizeFormatter.sizeFormat(status.netTraffic.sent)"> <template #prefix> <a-icon type="cloud-upload" /> </template> </a-custom-statistic> </a-col> <a-col :span="12"> - <a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)"> + <a-custom-statistic title='{{ i18n "pages.index.received" }}' + :value="SizeFormatter.sizeFormat(status.netTraffic.recv)"> <template #prefix> <a-icon type="cloud-download" /> </template> @@ -250,7 +257,8 @@ <template #title> {{ i18n "pages.index.toggleIpVisibility" }} </template> - <a-icon :type="showIp ? 'eye' : 'eye-invisible'" class="fs-1rem" @click="showIp = !showIp"></a-icon> + <a-icon :type="showIp ? 'eye' : 'eye-invisible'" class="fs-1rem" + @click="showIp = !showIp"></a-icon> </a-tooltip> </template> <a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0"> @@ -297,55 +305,54 @@ </a-spin> </a-layout-content> </a-layout> - <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true" - @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer=""> + <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' + :closable="true" @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer=""> <a-collapse default-active-key="1"> <a-collapse-panel key="1" header='Xray'> - <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert> - <a-list class="ant-version-list w-100" bordered> + <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' + show-icon></a-alert> + <a-list class="ant-version-list w-100" bordered> <a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions"> <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag> - <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio> + <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" + @click="switchV2rayVersion(version)"></a-radio> </a-list-item> </a-list> </a-collapse-panel> <a-collapse-panel key="2" header='Geofiles'> - <a-list class="ant-version-list w-100" bordered> - <a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']"> + <a-list class="ant-version-list w-100" bordered> + <a-list-item class="ant-version-list-item" + v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']"> <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag> - <a-icon type="reload" @click="updateGeofile(file)" class="mr-8"/> + <a-icon type="reload" @click="updateGeofile(file)" class="mr-8" /> </a-list-item> </a-list> - <div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button></div> + <div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n + "pages.index.geofilesUpdateAll" }}</a-button></div> </a-collapse-panel> </a-collapse> </a-modal> - <a-modal id="log-modal" v-model="logModal.visible" - :closable="true" @cancel="() => logModal.visible = false" - :class="themeSwitcher.currentTheme" - width="800px" footer=""> + <a-modal id="log-modal" v-model="logModal.visible" :closable="true" @cancel="() => logModal.visible = false" + :class="themeSwitcher.currentTheme" width="800px" footer=""> <template slot="title"> {{ i18n "pages.index.logs" }} - <a-icon :spin="logModal.loading" - type="sync" - class="va-middle ml-10" - :disabled="logModal.loading" + <a-icon :spin="logModal.loading" type="sync" class="va-middle ml-10" :disabled="logModal.loading" @click="openLogs()"> </a-icon> </template> <a-form layout="inline"> - <a-form-item class="mr-05"> + <a-form-item class="mr-05"> <a-input-group compact> - <a-select size="small" v-model="logModal.rows" class="w-70" - @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select size="small" v-model="logModal.rows" class="w-70" @change="openLogs()" + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="10">10</a-select-option> <a-select-option value="20">20</a-select-option> <a-select-option value="50">50</a-select-option> <a-select-option value="100">100</a-select-option> <a-select-option value="500">500</a-select-option> </a-select> - <a-select size="small" v-model="logModal.level" class="w-95" - @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select size="small" v-model="logModal.level" class="w-95" @change="openLogs()" + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="debug">Debug</a-select-option> <a-select-option value="info">Info</a-select-option> <a-select-option value="notice">Notice</a-select-option> @@ -358,31 +365,25 @@ <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox> </a-form-item> <a-form-item style="float: right;"> - <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button> + <a-button type="primary" icon="download" + @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button> </a-form-item> </a-form> - <div class="ant-input log-container" v-html="logModal.formattedLogs"></div> + <div class="ant-input log-container" v-html="logModal.formattedLogs"></div> </a-modal> - <a-modal id="xraylog-modal" - v-model="xraylogModal.visible" - :closable="true" @cancel="() => xraylogModal.visible = false" - :class="themeSwitcher.currentTheme" - width="80vw" - footer=""> + <a-modal id="xraylog-modal" v-model="xraylogModal.visible" :closable="true" + @cancel="() => xraylogModal.visible = false" :class="themeSwitcher.currentTheme" width="80vw" footer=""> <template slot="title"> {{ i18n "pages.index.logs" }} - <a-icon :spin="xraylogModal.loading" - type="sync" - class="va-middle ml-10" - :disabled="xraylogModal.loading" + <a-icon :spin="xraylogModal.loading" type="sync" class="va-middle ml-10" :disabled="xraylogModal.loading" @click="openXrayLogs()"> </a-icon> </template> <a-form layout="inline"> - <a-form-item class="mr-05"> + <a-form-item class="mr-05"> <a-input-group compact> - <a-select size="small" v-model="xraylogModal.rows" class="w-70" - @change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select size="small" v-model="xraylogModal.rows" class="w-70" @change="openXrayLogs()" + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="10">10</a-select-option> <a-select-option value="20">20</a-select-option> <a-select-option value="50">50</a-select-option> @@ -400,24 +401,21 @@ <a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox> </a-form-item> <a-form-item style="float: right;"> - <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button> + <a-button type="primary" icon="download" + @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button> </a-form-item> </a-form> - <div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div> + <div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div> </a-modal> - <a-modal id="backup-modal" - v-model="backupModal.visible" - title='{{ i18n "pages.index.backupTitle" }}' - :closable="true" - footer="" - :class="themeSwitcher.currentTheme"> - <a-list class="ant-backup-list w-100" bordered> + <a-modal id="backup-modal" v-model="backupModal.visible" title='{{ i18n "pages.index.backupTitle" }}' :closable="true" + footer="" :class="themeSwitcher.currentTheme"> + <a-list class="ant-backup-list w-100" bordered> <a-list-item class="ant-backup-list-item"> <a-list-item-meta> <template #title>{{ i18n "pages.index.exportDatabase" }}</template> <template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template> </a-list-item-meta> - <a-button @click="exportDatabase()" type="primary" icon="download"/> + <a-button @click="exportDatabase()" type="primary" icon="download" /> </a-list-item> <a-list-item class="ant-backup-list-item"> <a-list-item-meta> @@ -429,33 +427,25 @@ </a-list> </a-modal> <!-- CPU History Modal --> - <a-modal id="cpu-history-modal" - v-model="cpuHistoryModal.visible" - :closable="true" @cancel="() => cpuHistoryModal.visible = false" - :class="themeSwitcher.currentTheme" - width="900px" footer=""> + <a-modal id="cpu-history-modal" v-model="cpuHistoryModal.visible" :closable="true" + @cancel="() => cpuHistoryModal.visible = false" :class="themeSwitcher.currentTheme" width="900px" footer=""> <template slot="title"> CPU History - <a-select size="small" v-model="cpuHistoryModal.minutes" class="ml-10" style="width: 120px" @change="loadCpuHistory"> - <a-select-option :value="15">15 min</a-select-option> - <a-select-option :value="60">1 hour</a-select-option> - <a-select-option :value="180">3 hours</a-select-option> - <a-select-option :value="360">6 hours</a-select-option> + <a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 140px" + @change="fetchCpuHistoryBucket"> + <a-select-option :value="2">2s</a-select-option> + <a-select-option :value="30">30s</a-select-option> + <a-select-option :value="60">1m</a-select-option> + <a-select-option :value="120">2m</a-select-option> + <a-select-option :value="180">3m</a-select-option> + <a-select-option :value="300">5m</a-select-option> </a-select> </template> <div style="padding: 8px 0;"> - <sparkline :data="cpuHistoryLong" - :labels="cpuHistoryLabels" - :vb-width="840" - :height="220" - :stroke="status.cpu.color" - :stroke-width="2.2" - :show-grid="true" - :show-axes="true" - :tick-count-x="5" - :fill-opacity="0.18" - :marker-radius="3.2" - :show-tooltip="true" /> + <sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220" + :stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5" + :max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" /> + <div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div> </div> </a-modal> </a-layout> @@ -543,7 +533,7 @@ const last = this.pointsArr[this.pointsArr.length - 1] const line = this.points // Close to bottom to create an area fill - return `M ${first[0]},${this.paddingTop + this.drawHeight} L ${line.replace(/ /g,' L ')} L ${last[0]},${this.paddingTop + this.drawHeight} Z` + return `M ${first[0]},${this.paddingTop + this.drawHeight} L ${line.replace(/ /g, ' L ')} L ${last[0]},${this.paddingTop + this.drawHeight} Z` }, gridLines() { if (!this.showGrid) return [] @@ -609,7 +599,8 @@ const labels = this.labelsSlice const idx = this.hoverIdx if (idx < 0 || idx >= this.dataSlice.length) return '' - const val = Math.max(0, Math.min(100, Number(this.dataSlice[idx] || 0))) + const raw = Math.max(0, Math.min(100, Number(this.dataSlice[idx] || 0))) + const val = Number.isFinite(raw) ? raw.toFixed(2) : raw const lab = labels[idx] != null ? labels[idx] : '' return `${val}%${lab ? ' • ' + lab : ''}` }, @@ -649,195 +640,196 @@ `, }) - class CurTotal { - constructor(current, total) { - this.current = current; - this.total = total; - } + class CurTotal { - get percent() { - if (this.total === 0) { - return 0; - } - return NumberFormatter.toFixed(this.current / this.total * 100, 2); - } + constructor(current, total) { + this.current = current; + this.total = total; + } - get color() { - const percent = this.percent; - if (percent < 80) { - return '#008771'; // Green - } else if (percent < 90) { - return "#f37b24"; // Orange - } else { - return "#cf3c3c"; // Red - } - } + get percent() { + if (this.total === 0) { + return 0; + } + return NumberFormatter.toFixed(this.current / this.total * 100, 2); } - class Status { - constructor(data) { - this.cpu = new CurTotal(0, 0); - this.cpuCores = 0; - this.logicalPro = 0; - this.cpuSpeedMhz = 0; - this.disk = new CurTotal(0, 0); - this.loads = [0, 0, 0]; - this.mem = new CurTotal(0, 0); - this.netIO = { up: 0, down: 0 }; - this.netTraffic = { sent: 0, recv: 0 }; - this.publicIP = { ipv4: 0, ipv6: 0 }; - this.swap = new CurTotal(0, 0); - this.tcpCount = 0; - this.udpCount = 0; - this.uptime = 0; - this.appUptime = 0; - this.appStats = {threads: 0, mem: 0, uptime: 0}; + get color() { + const percent = this.percent; + if (percent < 80) { + return '#008771'; // Green + } else if (percent < 90) { + return "#f37b24"; // Orange + } else { + return "#cf3c3c"; // Red + } + } + } - this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" }; + class Status { + constructor(data) { + this.cpu = new CurTotal(0, 0); + this.cpuCores = 0; + this.logicalPro = 0; + this.cpuSpeedMhz = 0; + this.disk = new CurTotal(0, 0); + this.loads = [0, 0, 0]; + this.mem = new CurTotal(0, 0); + this.netIO = { up: 0, down: 0 }; + this.netTraffic = { sent: 0, recv: 0 }; + this.publicIP = { ipv4: 0, ipv6: 0 }; + this.swap = new CurTotal(0, 0); + this.tcpCount = 0; + this.udpCount = 0; + this.uptime = 0; + this.appUptime = 0; + this.appStats = { threads: 0, mem: 0, uptime: 0 }; - if (data == null) { - return; - } + this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" }; - this.cpu = new CurTotal(data.cpu, 100); - this.cpuCores = data.cpuCores; - this.logicalPro = data.logicalPro; - this.cpuSpeedMhz = data.cpuSpeedMhz; - this.disk = new CurTotal(data.disk.current, data.disk.total); - this.loads = data.loads.map(load => NumberFormatter.toFixed(load, 2)); - this.mem = new CurTotal(data.mem.current, data.mem.total); - this.netIO = data.netIO; - this.netTraffic = data.netTraffic; - this.publicIP = data.publicIP; - this.swap = new CurTotal(data.swap.current, data.swap.total); - this.tcpCount = data.tcpCount; - this.udpCount = data.udpCount; - this.uptime = data.uptime; - this.appUptime = data.appUptime; - this.appStats = data.appStats; - this.xray = data.xray; - switch (this.xray.state) { - case 'running': - this.xray.color = "green"; - this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}'; - break; - case 'stop': - this.xray.color = "orange"; - this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}'; - break; - case 'error': - this.xray.color = "red"; - this.xray.stateMsg ='{{ i18n "pages.index.xrayStatusError" }}'; - break; - default: - this.xray.color = "gray"; - this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusUnknown" }}'; - break; - } - } + if (data == null) { + return; + } + + this.cpu = new CurTotal(data.cpu, 100); + this.cpuCores = data.cpuCores; + this.logicalPro = data.logicalPro; + this.cpuSpeedMhz = data.cpuSpeedMhz; + this.disk = new CurTotal(data.disk.current, data.disk.total); + this.loads = data.loads.map(load => NumberFormatter.toFixed(load, 2)); + this.mem = new CurTotal(data.mem.current, data.mem.total); + this.netIO = data.netIO; + this.netTraffic = data.netTraffic; + this.publicIP = data.publicIP; + this.swap = new CurTotal(data.swap.current, data.swap.total); + this.tcpCount = data.tcpCount; + this.udpCount = data.udpCount; + this.uptime = data.uptime; + this.appUptime = data.appUptime; + this.appStats = data.appStats; + this.xray = data.xray; + switch (this.xray.state) { + case 'running': + this.xray.color = "green"; + this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}'; + break; + case 'stop': + this.xray.color = "orange"; + this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}'; + break; + case 'error': + this.xray.color = "red"; + this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}'; + break; + default: + this.xray.color = "gray"; + this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusUnknown" }}'; + break; + } } + } - const versionModal = { - visible: false, - versions: [], - show(versions) { - this.visible = true; - this.versions = versions; - }, - hide() { - this.visible = false; - }, - }; + const versionModal = { + visible: false, + versions: [], + show(versions) { + this.visible = true; + this.versions = versions; + }, + hide() { + this.visible = false; + }, + }; - const logModal = { - visible: false, - logs: [], - rows: 20, - level: 'info', - syslog: false, - loading: false, - show(logs) { - this.visible = true; - this.logs = logs; - this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record..."; - }, - formatLogs(logs) { - let formattedLogs = ''; - const levels = ["DEBUG","INFO","NOTICE","WARNING","ERROR"]; - const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"]; + const logModal = { + visible: false, + logs: [], + rows: 20, + level: 'info', + syslog: false, + loading: false, + show(logs) { + this.visible = true; + this.logs = logs; + this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record..."; + }, + formatLogs(logs) { + let formattedLogs = ''; + const levels = ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR"]; + const levelColors = ["#3c89e8", "#008771", "#008771", "#f37b24", "#e04141", "#bcbcbc"]; - logs.forEach((log, index) => { - let [data, message] = log.split(" - ",2); - const parts = data.split(" ") - if(index>0) formattedLogs += '<br>'; + logs.forEach((log, index) => { + let [data, message] = log.split(" - ", 2); + const parts = data.split(" ") + if (index > 0) formattedLogs += '<br>'; - if (parts.length === 3) { - const d = parts[0]; - const t = parts[1]; - const level = parts[2]; - const levelIndex = levels.indexOf(level,levels) || 5; + if (parts.length === 3) { + const d = parts[0]; + const t = parts[1]; + const level = parts[2]; + const levelIndex = levels.indexOf(level, levels) || 5; - //formattedLogs += `<span style="color: gray;">${index + 1}.</span>`; - formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `; - formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`; - } else { - const levelIndex = levels.indexOf(data,levels) || 5; - formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`; - } + //formattedLogs += `<span style="color: gray;">${index + 1}.</span>`; + formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `; + formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`; + } else { + const levelIndex = levels.indexOf(data, levels) || 5; + formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`; + } - if(message){ - if(message.startsWith("XRAY:")) - message = "<b>XRAY: </b>" + message.substring(5); - else - message = "<b>X-UI: </b>" + message; - } + if (message) { + if (message.startsWith("XRAY:")) + message = "<b>XRAY: </b>" + message.substring(5); + else + message = "<b>X-UI: </b>" + message; + } - formattedLogs += message ? ' - ' + message : ''; - }); + formattedLogs += message ? ' - ' + message : ''; + }); - return formattedLogs; - }, - hide() { - this.visible = false; - }, contacts: admin@thfree.ru |
