mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-27 18:32:52 +00:00
Compare commits
5 commits
135f843b3e
...
417c323e0a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417c323e0a | ||
|
|
1c8689dea9 | ||
|
|
a128f75f64 | ||
|
|
299572a4c2 | ||
|
|
22afa50901 |
6 changed files with 654 additions and 611 deletions
|
|
@ -22,16 +22,13 @@ type ServerController struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
|
|
||||||
lastStatus *service.Status
|
lastStatus *service.Status
|
||||||
lastGetStatusTime time.Time
|
|
||||||
|
|
||||||
lastVersions []string
|
lastVersions []string
|
||||||
lastGetVersionsTime time.Time
|
lastGetVersionsTime int64 // unix seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||||
a := &ServerController{
|
a := &ServerController{}
|
||||||
lastGetStatusTime: time.Now(),
|
|
||||||
}
|
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
a.startTask()
|
a.startTask()
|
||||||
return a
|
return a
|
||||||
|
|
@ -40,7 +37,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/status", a.status)
|
g.GET("/status", a.status)
|
||||||
g.GET("/cpuHistory", a.getCpuHistory)
|
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
|
||||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||||
g.GET("/getConfigJson", a.getConfigJson)
|
g.GET("/getConfigJson", a.getConfigJson)
|
||||||
g.GET("/getDb", a.getDb)
|
g.GET("/getDb", a.getDb)
|
||||||
|
|
@ -79,35 +76,34 @@ func (a *ServerController) startTask() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) status(c *gin.Context) {
|
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
||||||
a.lastGetStatusTime = time.Now()
|
|
||||||
|
|
||||||
jsonObj(c, a.lastStatus, nil)
|
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||||
}
|
bucketStr := c.Param("bucket")
|
||||||
|
bucket, err := strconv.Atoi(bucketStr)
|
||||||
// getCpuHistory returns recent CPU utilization points.
|
if err != nil || bucket <= 0 {
|
||||||
// Query param q=minutes (int). Bounds: 1..360 (6 hours). Defaults to 60.
|
jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket"))
|
||||||
func (a *ServerController) getCpuHistory(c *gin.Context) {
|
return
|
||||||
minsStr := c.Query("q")
|
|
||||||
mins := 60
|
|
||||||
if minsStr != "" {
|
|
||||||
if v, err := strconv.Atoi(minsStr); err == nil {
|
|
||||||
mins = v
|
|
||||||
}
|
}
|
||||||
|
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 < 1 {
|
if !allowed[bucket] {
|
||||||
mins = 1
|
jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if mins > 360 {
|
points := a.serverService.AggregateCpuHistory(bucket, 60)
|
||||||
mins = 360
|
jsonObj(c, points, nil)
|
||||||
}
|
|
||||||
res := a.serverService.GetCpuHistory(mins)
|
|
||||||
jsonObj(c, res, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||||
now := time.Now()
|
now := time.Now().Unix()
|
||||||
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
|
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
||||||
jsonObj(c, a.lastVersions, nil)
|
jsonObj(c, a.lastVersions, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +115,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.lastVersions = versions
|
a.lastVersions = versions
|
||||||
a.lastGetVersionsTime = time.Now()
|
a.lastGetVersionsTime = now
|
||||||
|
|
||||||
jsonObj(c, versions, nil)
|
jsonObj(c, versions, nil)
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +133,6 @@ func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
a.lastGetStatusTime = time.Now()
|
|
||||||
err := a.serverService.StopXrayService()
|
err := a.serverService.StopXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
||||||
|
|
@ -253,9 +248,7 @@ func (a *ServerController) importDB(c *gin.Context) {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
// Always restart Xray before return
|
// Always restart Xray before return
|
||||||
defer a.serverService.RestartXrayService()
|
defer a.serverService.RestartXrayService()
|
||||||
defer func() {
|
// lastGetStatusTime removed; no longer needed
|
||||||
a.lastGetStatusTime = time.Now()
|
|
||||||
}()
|
|
||||||
// Import it
|
// Import it
|
||||||
err = a.serverService.ImportDB(file)
|
err = a.serverService.ImportDB(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@ func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||||
|
|
||||||
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/xray")
|
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("/", a.getXraySetting)
|
||||||
g.POST("/update", a.updateSetting)
|
|
||||||
g.GET("/getXrayResult", a.getXrayResult)
|
|
||||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
|
||||||
g.POST("/warp/:action", a.warp)
|
g.POST("/warp/:action", a.warp)
|
||||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
|
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
|
||||||
message='{{ i18n "secAlertTitle" }}'
|
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||||
color="red"
|
|
||||||
description='{{ i18n "secAlertSsl" }}'
|
|
||||||
show-icon closable>
|
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
|
|
@ -29,8 +26,7 @@
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12" class="text-center">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal" :stroke-color="status.cpu.color"
|
||||||
:stroke-color="status.cpu.color"
|
|
||||||
:percent="status.cpu.percent"></a-progress>
|
:percent="status.cpu.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
<b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]
|
<b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]
|
||||||
|
|
@ -38,7 +34,8 @@
|
||||||
<a-icon type="area-chart"></a-icon>
|
<a-icon type="area-chart"></a-icon>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
|
<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>
|
</template>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
|
||||||
|
|
@ -49,11 +46,11 @@
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" class="text-center">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal" :stroke-color="status.mem.color"
|
||||||
:stroke-color="status.mem.color"
|
|
||||||
:percent="status.mem.percent"></a-progress>
|
:percent="status.mem.percent"></a-progress>
|
||||||
<div>
|
<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>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
@ -61,19 +58,19 @@
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12" class="text-center">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal" :stroke-color="status.swap.color"
|
||||||
:stroke-color="status.swap.color"
|
|
||||||
:percent="status.swap.percent"></a-progress>
|
:percent="status.swap.percent"></a-progress>
|
||||||
<div>
|
<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>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" class="text-center">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal" :stroke-color="status.disk.color"
|
||||||
:stroke-color="status.disk.color"
|
|
||||||
:percent="status.disk.percent"></a-progress>
|
:percent="status.disk.percent"></a-progress>
|
||||||
<div>
|
<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>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
@ -93,7 +90,9 @@
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<template v-if="status.xray.state != 'error'">
|
<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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||||
|
|
@ -110,7 +109,8 @@
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
<span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
||||||
</template>
|
</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>
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -130,7 +130,8 @@
|
||||||
<a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center">
|
<a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center">
|
||||||
<a-icon type="tool"></a-icon>
|
<a-icon type="tool"></a-icon>
|
||||||
<span v-if="!isMobile">
|
<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>
|
</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -175,7 +176,8 @@
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :lg="12">
|
<a-col :sm="24" :lg="12">
|
||||||
<a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
|
<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-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -193,7 +195,8 @@
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :lg="12">
|
<a-col :sm="24" :lg="12">
|
||||||
<a-card title='{{ i18n "usage"}}' hoverable>
|
<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-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -201,7 +204,8 @@
|
||||||
<a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
|
<a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
|
||||||
<a-row :gutter="isMobile ? [8,8] : 0">
|
<a-row :gutter="isMobile ? [8,8] : 0">
|
||||||
<a-col :span="12">
|
<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>
|
<template #prefix>
|
||||||
<a-icon type="arrow-up" />
|
<a-icon type="arrow-up" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -211,7 +215,8 @@
|
||||||
</a-custom-statistic>
|
</a-custom-statistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<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>
|
<template #prefix>
|
||||||
<a-icon type="arrow-down" />
|
<a-icon type="arrow-down" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -227,14 +232,16 @@
|
||||||
<a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
|
<a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
|
||||||
<a-row :gutter="isMobile ? [8,8] : 0">
|
<a-row :gutter="isMobile ? [8,8] : 0">
|
||||||
<a-col :span="12">
|
<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>
|
<template #prefix>
|
||||||
<a-icon type="cloud-upload" />
|
<a-icon type="cloud-upload" />
|
||||||
</template>
|
</template>
|
||||||
</a-custom-statistic>
|
</a-custom-statistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<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>
|
<template #prefix>
|
||||||
<a-icon type="cloud-download" />
|
<a-icon type="cloud-download" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -250,7 +257,8 @@
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ i18n "pages.index.toggleIpVisibility" }}
|
{{ i18n "pages.index.toggleIpVisibility" }}
|
||||||
</template>
|
</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>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
|
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
|
||||||
|
|
@ -297,55 +305,54 @@
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
|
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||||
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
|
:closable="true" @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
|
||||||
<a-collapse default-active-key="1">
|
<a-collapse default-active-key="1">
|
||||||
<a-collapse-panel key="1" header='Xray'>
|
<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-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 class="ant-version-list w-100" bordered>
|
||||||
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
|
<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-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-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="2" header='Geofiles'>
|
<a-collapse-panel key="2" header='Geofiles'>
|
||||||
<a-list class="ant-version-list w-100" bordered>
|
<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-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-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-item>
|
||||||
</a-list>
|
</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-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<a-modal id="log-modal" v-model="logModal.visible"
|
<a-modal id="log-modal" v-model="logModal.visible" :closable="true" @cancel="() => logModal.visible = false"
|
||||||
:closable="true" @cancel="() => logModal.visible = false"
|
:class="themeSwitcher.currentTheme" width="800px" footer="">
|
||||||
:class="themeSwitcher.currentTheme"
|
|
||||||
width="800px" footer="">
|
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
{{ i18n "pages.index.logs" }}
|
{{ i18n "pages.index.logs" }}
|
||||||
<a-icon :spin="logModal.loading"
|
<a-icon :spin="logModal.loading" type="sync" class="va-middle ml-10" :disabled="logModal.loading"
|
||||||
type="sync"
|
|
||||||
class="va-middle ml-10"
|
|
||||||
:disabled="logModal.loading"
|
|
||||||
@click="openLogs()">
|
@click="openLogs()">
|
||||||
</a-icon>
|
</a-icon>
|
||||||
</template>
|
</template>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item class="mr-05">
|
<a-form-item class="mr-05">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-select size="small" v-model="logModal.rows" class="w-70"
|
<a-select size="small" v-model="logModal.rows" class="w-70" @change="openLogs()"
|
||||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="10">10</a-select-option>
|
<a-select-option value="10">10</a-select-option>
|
||||||
<a-select-option value="20">20</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="50">50</a-select-option>
|
||||||
<a-select-option value="100">100</a-select-option>
|
<a-select-option value="100">100</a-select-option>
|
||||||
<a-select-option value="500">500</a-select-option>
|
<a-select-option value="500">500</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-select size="small" v-model="logModal.level" class="w-95"
|
<a-select size="small" v-model="logModal.level" class="w-95" @change="openLogs()"
|
||||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="debug">Debug</a-select-option>
|
<a-select-option value="debug">Debug</a-select-option>
|
||||||
<a-select-option value="info">Info</a-select-option>
|
<a-select-option value="info">Info</a-select-option>
|
||||||
<a-select-option value="notice">Notice</a-select-option>
|
<a-select-option value="notice">Notice</a-select-option>
|
||||||
|
|
@ -363,26 +370,19 @@
|
||||||
</a-form>
|
</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>
|
||||||
<a-modal id="xraylog-modal"
|
<a-modal id="xraylog-modal" v-model="xraylogModal.visible" :closable="true"
|
||||||
v-model="xraylogModal.visible"
|
@cancel="() => xraylogModal.visible = false" :class="themeSwitcher.currentTheme" width="80vw" footer="">
|
||||||
:closable="true" @cancel="() => xraylogModal.visible = false"
|
|
||||||
:class="themeSwitcher.currentTheme"
|
|
||||||
width="80vw"
|
|
||||||
footer="">
|
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
{{ i18n "pages.index.logs" }}
|
{{ i18n "pages.index.logs" }}
|
||||||
<a-icon :spin="xraylogModal.loading"
|
<a-icon :spin="xraylogModal.loading" type="sync" class="va-middle ml-10" :disabled="xraylogModal.loading"
|
||||||
type="sync"
|
|
||||||
class="va-middle ml-10"
|
|
||||||
:disabled="xraylogModal.loading"
|
|
||||||
@click="openXrayLogs()">
|
@click="openXrayLogs()">
|
||||||
</a-icon>
|
</a-icon>
|
||||||
</template>
|
</template>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item class="mr-05">
|
<a-form-item class="mr-05">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-select size="small" v-model="xraylogModal.rows" class="w-70"
|
<a-select size="small" v-model="xraylogModal.rows" class="w-70" @change="openXrayLogs()"
|
||||||
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="10">10</a-select-option>
|
<a-select-option value="10">10</a-select-option>
|
||||||
<a-select-option value="20">20</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="50">50</a-select-option>
|
||||||
|
|
@ -400,24 +400,20 @@
|
||||||
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item style="float: right;">
|
<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="downloadXrayLogs"></a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</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>
|
||||||
<a-modal id="backup-modal"
|
<a-modal id="backup-modal" v-model="backupModal.visible" title='{{ i18n "pages.index.backupTitle" }}' :closable="true"
|
||||||
v-model="backupModal.visible"
|
footer="" :class="themeSwitcher.currentTheme">
|
||||||
title='{{ i18n "pages.index.backupTitle" }}'
|
|
||||||
:closable="true"
|
|
||||||
footer=""
|
|
||||||
:class="themeSwitcher.currentTheme">
|
|
||||||
<a-list class="ant-backup-list w-100" bordered>
|
<a-list class="ant-backup-list w-100" bordered>
|
||||||
<a-list-item class="ant-backup-list-item">
|
<a-list-item class="ant-backup-list-item">
|
||||||
<a-list-item-meta>
|
<a-list-item-meta>
|
||||||
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
|
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
|
||||||
<template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
|
<template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
|
||||||
</a-list-item-meta>
|
</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>
|
||||||
<a-list-item class="ant-backup-list-item">
|
<a-list-item class="ant-backup-list-item">
|
||||||
<a-list-item-meta>
|
<a-list-item-meta>
|
||||||
|
|
@ -429,33 +425,25 @@
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<!-- CPU History Modal -->
|
<!-- CPU History Modal -->
|
||||||
<a-modal id="cpu-history-modal"
|
<a-modal id="cpu-history-modal" v-model="cpuHistoryModal.visible" :closable="true"
|
||||||
v-model="cpuHistoryModal.visible"
|
@cancel="() => cpuHistoryModal.visible = false" :class="themeSwitcher.currentTheme" width="900px" footer="">
|
||||||
:closable="true" @cancel="() => cpuHistoryModal.visible = false"
|
|
||||||
:class="themeSwitcher.currentTheme"
|
|
||||||
width="900px" footer="">
|
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
CPU History
|
CPU History
|
||||||
<a-select size="small" v-model="cpuHistoryModal.minutes" class="ml-10" style="width: 120px" @change="loadCpuHistory">
|
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 140px"
|
||||||
<a-select-option :value="15">15 min</a-select-option>
|
@change="fetchCpuHistoryBucket">
|
||||||
<a-select-option :value="60">1 hour</a-select-option>
|
<a-select-option :value="2">2s</a-select-option>
|
||||||
<a-select-option :value="180">3 hours</a-select-option>
|
<a-select-option :value="30">30s</a-select-option>
|
||||||
<a-select-option :value="360">6 hours</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>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
<div style="padding: 8px 0;">
|
<div style="padding: 8px 0;">
|
||||||
<sparkline :data="cpuHistoryLong"
|
<sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
|
||||||
:labels="cpuHistoryLabels"
|
:stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
|
||||||
:vb-width="840"
|
:max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
|
||||||
:height="220"
|
<div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div>
|
||||||
: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" />
|
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
@ -543,7 +531,7 @@
|
||||||
const last = this.pointsArr[this.pointsArr.length - 1]
|
const last = this.pointsArr[this.pointsArr.length - 1]
|
||||||
const line = this.points
|
const line = this.points
|
||||||
// Close to bottom to create an area fill
|
// 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() {
|
gridLines() {
|
||||||
if (!this.showGrid) return []
|
if (!this.showGrid) return []
|
||||||
|
|
@ -609,7 +597,8 @@
|
||||||
const labels = this.labelsSlice
|
const labels = this.labelsSlice
|
||||||
const idx = this.hoverIdx
|
const idx = this.hoverIdx
|
||||||
if (idx < 0 || idx >= this.dataSlice.length) return ''
|
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] : ''
|
const lab = labels[idx] != null ? labels[idx] : ''
|
||||||
return `${val}%${lab ? ' • ' + lab : ''}`
|
return `${val}%${lab ? ' • ' + lab : ''}`
|
||||||
},
|
},
|
||||||
|
|
@ -649,6 +638,7 @@
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class CurTotal {
|
class CurTotal {
|
||||||
|
|
||||||
constructor(current, total) {
|
constructor(current, total) {
|
||||||
|
|
@ -692,7 +682,7 @@
|
||||||
this.udpCount = 0;
|
this.udpCount = 0;
|
||||||
this.uptime = 0;
|
this.uptime = 0;
|
||||||
this.appUptime = 0;
|
this.appUptime = 0;
|
||||||
this.appStats = {threads: 0, mem: 0, uptime: 0};
|
this.appStats = { threads: 0, mem: 0, uptime: 0 };
|
||||||
|
|
||||||
this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" };
|
this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" };
|
||||||
|
|
||||||
|
|
@ -728,7 +718,7 @@
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
this.xray.color = "red";
|
this.xray.color = "red";
|
||||||
this.xray.stateMsg ='{{ i18n "pages.index.xrayStatusError" }}';
|
this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.xray.color = "gray";
|
this.xray.color = "gray";
|
||||||
|
|
@ -764,30 +754,30 @@
|
||||||
},
|
},
|
||||||
formatLogs(logs) {
|
formatLogs(logs) {
|
||||||
let formattedLogs = '';
|
let formattedLogs = '';
|
||||||
const levels = ["DEBUG","INFO","NOTICE","WARNING","ERROR"];
|
const levels = ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR"];
|
||||||
const levelColors = ["#3c89e8","#008771","#008771","#f37b24","#e04141","#bcbcbc"];
|
const levelColors = ["#3c89e8", "#008771", "#008771", "#f37b24", "#e04141", "#bcbcbc"];
|
||||||
|
|
||||||
logs.forEach((log, index) => {
|
logs.forEach((log, index) => {
|
||||||
let [data, message] = log.split(" - ",2);
|
let [data, message] = log.split(" - ", 2);
|
||||||
const parts = data.split(" ")
|
const parts = data.split(" ")
|
||||||
if(index>0) formattedLogs += '<br>';
|
if (index > 0) formattedLogs += '<br>';
|
||||||
|
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
const d = parts[0];
|
const d = parts[0];
|
||||||
const t = parts[1];
|
const t = parts[1];
|
||||||
const level = parts[2];
|
const level = parts[2];
|
||||||
const levelIndex = levels.indexOf(level,levels) || 5;
|
const levelIndex = levels.indexOf(level, levels) || 5;
|
||||||
|
|
||||||
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
|
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
|
||||||
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
|
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
|
||||||
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
|
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
|
||||||
} else {
|
} else {
|
||||||
const levelIndex = levels.indexOf(data,levels) || 5;
|
const levelIndex = levels.indexOf(data, levels) || 5;
|
||||||
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
|
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(message){
|
if (message) {
|
||||||
if(message.startsWith("XRAY:"))
|
if (message.startsWith("XRAY:"))
|
||||||
message = "<b>XRAY: </b>" + message.substring(5);
|
message = "<b>XRAY: </b>" + message.substring(5);
|
||||||
else
|
else
|
||||||
message = "<b>X-UI: </b>" + message;
|
message = "<b>X-UI: </b>" + message;
|
||||||
|
|
@ -872,7 +862,6 @@
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupModal = {
|
const backupModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
show() {
|
show() {
|
||||||
|
|
@ -894,10 +883,10 @@
|
||||||
spinning: false
|
spinning: false
|
||||||
},
|
},
|
||||||
status: new Status(),
|
status: new Status(),
|
||||||
cpuHistory: [], // keep last N cpu utilization points (0..100)
|
cpuHistory: [], // small live widget history
|
||||||
cpuHistoryLong: [], // long-range history for modal (values)
|
cpuHistoryLong: [], // aggregated points from backend
|
||||||
cpuHistoryLabels: [], // formatted timestamps matching long history
|
cpuHistoryLabels: [],
|
||||||
cpuHistoryModal: { visible: false, minutes: 60 },
|
cpuHistoryModal: { visible: false, bucket: 2 },
|
||||||
versionModal,
|
versionModal,
|
||||||
logModal,
|
logModal,
|
||||||
xraylogModal,
|
xraylogModal,
|
||||||
|
|
@ -935,37 +924,35 @@
|
||||||
if (this.cpuHistory.length > maxPoints) {
|
if (this.cpuHistory.length > maxPoints) {
|
||||||
this.cpuHistory.splice(0, this.cpuHistory.length - maxPoints)
|
this.cpuHistory.splice(0, this.cpuHistory.length - maxPoints)
|
||||||
}
|
}
|
||||||
|
// If modal open, refresh current bucketed data
|
||||||
|
if (this.cpuHistoryModal.visible) {
|
||||||
|
this.fetchCpuHistoryBucket()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
openCpuHistory() {
|
openCpuHistory() {
|
||||||
this.cpuHistoryModal.visible = true
|
this.cpuHistoryModal.visible = true
|
||||||
this.loadCpuHistory()
|
this.fetchCpuHistoryBucket()
|
||||||
},
|
},
|
||||||
async loadCpuHistory() {
|
async fetchCpuHistoryBucket() {
|
||||||
const mins = this.cpuHistoryModal.minutes || 60
|
const bucket = this.cpuHistoryModal.bucket || 2
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.get(`/panel/api/server/cpuHistory?q=${mins}`)
|
const msg = await HttpUtil.get(`/panel/api/server/cpuHistory/${bucket}`)
|
||||||
if (msg.success && Array.isArray(msg.obj)) {
|
if (msg.success && Array.isArray(msg.obj)) {
|
||||||
// msg.obj is array of {t, cpu}
|
const vals = []
|
||||||
const arr = msg.obj.map(p => Math.max(0, Math.min(100, Number(p.cpu || 0))))
|
const labels = []
|
||||||
const labels = msg.obj.map(p => {
|
for (const p of msg.obj) {
|
||||||
const t = p.t
|
const d = new Date(p.t * 1000)
|
||||||
let d
|
const hh = String(d.getHours()).padStart(2,'0')
|
||||||
if (typeof t === 'number') {
|
const mm = String(d.getMinutes()).padStart(2,'0')
|
||||||
// Heuristic: if seconds, convert to ms
|
const ss = String(d.getSeconds()).padStart(2,'0')
|
||||||
d = new Date(t < 1e12 ? t * 1000 : t)
|
labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
|
||||||
} else {
|
vals.push(Math.max(0, Math.min(100, p.cpu)))
|
||||||
d = new Date(t)
|
|
||||||
}
|
}
|
||||||
if (isNaN(d.getTime())) return ''
|
|
||||||
const hh = String(d.getHours()).padStart(2, '0')
|
|
||||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
||||||
return `${hh}:${mm}`
|
|
||||||
})
|
|
||||||
this.cpuHistoryLong = arr
|
|
||||||
this.cpuHistoryLabels = labels
|
this.cpuHistoryLabels = labels
|
||||||
|
this.cpuHistoryLong = vals
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch(e) {
|
||||||
console.error('Failed to load CPU history', e)
|
console.error('Failed to fetch bucketed cpu history', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async openSelectV2rayVersion() {
|
async openSelectV2rayVersion() {
|
||||||
|
|
@ -1029,9 +1016,9 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async openLogs(){
|
async openLogs() {
|
||||||
logModal.loading = true;
|
logModal.loading = true;
|
||||||
const msg = await HttpUtil.post('/panel/api/server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog });
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1039,9 +1026,9 @@
|
||||||
await PromiseUtil.sleep(500);
|
await PromiseUtil.sleep(500);
|
||||||
logModal.loading = false;
|
logModal.loading = false;
|
||||||
},
|
},
|
||||||
async openXrayLogs(){
|
async openXrayLogs() {
|
||||||
xraylogModal.loading = true;
|
xraylogModal.loading = true;
|
||||||
const msg = await HttpUtil.post('/panel/api/server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
|
const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy });
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1049,6 +1036,25 @@
|
||||||
await PromiseUtil.sleep(500);
|
await PromiseUtil.sleep(500);
|
||||||
xraylogModal.loading = false;
|
xraylogModal.loading = false;
|
||||||
},
|
},
|
||||||
|
downloadXrayLogs() {
|
||||||
|
if (!Array.isArray(this.xraylogModal.logs) || this.xraylogModal.logs.length === 0) {
|
||||||
|
FileManager.downloadTextFile('', 'x-ui.log');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = this.xraylogModal.logs.map(l => {
|
||||||
|
try {
|
||||||
|
const dt = l.DateTime ? new Date(l.DateTime) : null;
|
||||||
|
const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
|
||||||
|
const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
|
||||||
|
const eventText = eventMap[l.Event] || String(l.Event ?? '');
|
||||||
|
const emailPart = l.Email ? ` Email=${l.Email}` : '';
|
||||||
|
return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
|
||||||
|
} catch (e) {
|
||||||
|
return JSON.stringify(l);
|
||||||
|
}
|
||||||
|
}).join('\n');
|
||||||
|
FileManager.downloadTextFile(lines, 'x-ui.log');
|
||||||
|
},
|
||||||
async openConfig() {
|
async openConfig() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
||||||
|
|
|
||||||
|
|
@ -1951,8 +1951,8 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if t != nil && client != nil {
|
if t != nil && client != nil {
|
||||||
// Ensure enable mirrors the client's current enable flag in settings
|
|
||||||
t.Enable = client.Enable
|
t.Enable = client.Enable
|
||||||
|
t.SubId = client.SubID
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -1993,6 +1993,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
|
||||||
for i := range traffics {
|
for i := range traffics {
|
||||||
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||||
traffics[i].Enable = client.Enable
|
traffics[i].Enable = client.Enable
|
||||||
|
traffics[i].SubId = client.SubID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return traffics, err
|
return traffics, err
|
||||||
|
|
|
||||||
|
|
@ -99,17 +99,76 @@ type ServerService struct {
|
||||||
cachedIPv4 string
|
cachedIPv4 string
|
||||||
cachedIPv6 string
|
cachedIPv6 string
|
||||||
noIPv6 bool
|
noIPv6 bool
|
||||||
// CPU utilization smoothing state
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
lastCPUTimes cpu.TimesStat
|
lastCPUTimes cpu.TimesStat
|
||||||
hasLastCPUSample bool
|
hasLastCPUSample bool
|
||||||
emaCPU float64
|
emaCPU float64
|
||||||
// CPU history buffer (in-memory, protected by mu)
|
|
||||||
cpuHistory []CPUSample
|
cpuHistory []CPUSample
|
||||||
cpuCapacity int
|
cachedCpuSpeedMhz float64
|
||||||
|
lastCpuInfoAttempt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPUSample represents a single CPU utilization sample with timestamp
|
// AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds over recent data.
|
||||||
|
func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any {
|
||||||
|
if bucketSeconds <= 0 || maxPoints <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cutoff := time.Now().Add(-time.Duration(bucketSeconds*maxPoints) * time.Second).Unix()
|
||||||
|
s.mu.Lock()
|
||||||
|
// find start index (history sorted ascending)
|
||||||
|
hist := s.cpuHistory
|
||||||
|
// binary-ish scan (simple linear from end since size capped ~10800 is fine)
|
||||||
|
startIdx := 0
|
||||||
|
for i := len(hist) - 1; i >= 0; i-- {
|
||||||
|
if hist[i].T < cutoff {
|
||||||
|
startIdx = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if startIdx >= len(hist) {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return []map[string]any{}
|
||||||
|
}
|
||||||
|
slice := hist[startIdx:]
|
||||||
|
// copy for unlock
|
||||||
|
tmp := make([]CPUSample, len(slice))
|
||||||
|
copy(tmp, slice)
|
||||||
|
s.mu.Unlock()
|
||||||
|
if len(tmp) == 0 {
|
||||||
|
return []map[string]any{}
|
||||||
|
}
|
||||||
|
var out []map[string]any
|
||||||
|
var acc []float64
|
||||||
|
bSize := int64(bucketSeconds)
|
||||||
|
curBucket := (tmp[0].T / bSize) * bSize
|
||||||
|
flush := func(ts int64) {
|
||||||
|
if len(acc) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sum := 0.0
|
||||||
|
for _, v := range acc {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
avg := sum / float64(len(acc))
|
||||||
|
out = append(out, map[string]any{"t": ts, "cpu": avg})
|
||||||
|
acc = acc[:0]
|
||||||
|
}
|
||||||
|
for _, p := range tmp {
|
||||||
|
b := (p.T / bSize) * bSize
|
||||||
|
if b != curBucket {
|
||||||
|
flush(curBucket)
|
||||||
|
curBucket = b
|
||||||
|
}
|
||||||
|
acc = append(acc, p.Cpu)
|
||||||
|
}
|
||||||
|
flush(curBucket)
|
||||||
|
if len(out) > maxPoints {
|
||||||
|
out = out[len(out)-maxPoints:]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPUSample single CPU utilization sample
|
||||||
type CPUSample struct {
|
type CPUSample struct {
|
||||||
T int64 `json:"t"` // unix seconds
|
T int64 `json:"t"` // unix seconds
|
||||||
Cpu float64 `json:"cpu"` // percent 0..100
|
Cpu float64 `json:"cpu"` // percent 0..100
|
||||||
|
|
@ -178,14 +237,31 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||||
|
|
||||||
status.LogicalPro = runtime.NumCPU()
|
status.LogicalPro = runtime.NumCPU()
|
||||||
|
|
||||||
|
if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {
|
||||||
|
s.lastCpuInfoAttempt = time.Now()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
cpuInfos, err := cpu.Info()
|
cpuInfos, err := cpu.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("get cpu info failed:", err)
|
logger.Warning("get cpu info failed:", err)
|
||||||
} else if len(cpuInfos) > 0 {
|
return
|
||||||
status.CpuSpeedMhz = cpuInfos[0].Mhz
|
}
|
||||||
|
if len(cpuInfos) > 0 {
|
||||||
|
s.cachedCpuSpeedMhz = cpuInfos[0].Mhz
|
||||||
|
status.CpuSpeedMhz = s.cachedCpuSpeedMhz
|
||||||
} else {
|
} else {
|
||||||
logger.Warning("could not find cpu info")
|
logger.Warning("could not find cpu info")
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(1500 * time.Millisecond):
|
||||||
|
logger.Warning("cpu info query timed out; will retry later")
|
||||||
|
}
|
||||||
|
} else if s.cachedCpuSpeedMhz != 0 {
|
||||||
|
status.CpuSpeedMhz = s.cachedCpuSpeedMhz
|
||||||
|
}
|
||||||
|
|
||||||
// Uptime
|
// Uptime
|
||||||
upTime, err := host.Uptime()
|
upTime, err := host.Uptime()
|
||||||
|
|
@ -332,55 +408,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendCpuSample appends a CPU sample into the in-memory history with capacity trimming.
|
|
||||||
func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
|
func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
|
||||||
|
const capacity = 9000 // ~5 hours @ 2s interval
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if s.cpuCapacity == 0 {
|
|
||||||
s.cpuCapacity = 10800 // ~6 hours at 2s per sample
|
|
||||||
}
|
|
||||||
p := CPUSample{T: t.Unix(), Cpu: v}
|
p := CPUSample{T: t.Unix(), Cpu: v}
|
||||||
|
if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T {
|
||||||
|
s.cpuHistory[n-1] = p
|
||||||
|
} else {
|
||||||
s.cpuHistory = append(s.cpuHistory, p)
|
s.cpuHistory = append(s.cpuHistory, p)
|
||||||
if len(s.cpuHistory) > s.cpuCapacity {
|
}
|
||||||
drop := len(s.cpuHistory) - s.cpuCapacity
|
if len(s.cpuHistory) > capacity {
|
||||||
s.cpuHistory = s.cpuHistory[drop:]
|
s.cpuHistory = s.cpuHistory[len(s.cpuHistory)-capacity:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCpuHistory returns samples from the last 'mins' minutes (bounded 1..360).
|
|
||||||
func (s *ServerService) GetCpuHistory(mins int) []CPUSample {
|
|
||||||
if mins < 1 {
|
|
||||||
mins = 1
|
|
||||||
}
|
|
||||||
if mins > 360 {
|
|
||||||
mins = 360
|
|
||||||
}
|
|
||||||
cutoff := time.Now().Add(-time.Duration(mins) * time.Minute).Unix()
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if len(s.cpuHistory) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// find first index >= cutoff (linear scan from end is fine for these sizes)
|
|
||||||
i := len(s.cpuHistory) - 1
|
|
||||||
for ; i >= 0; i-- {
|
|
||||||
if s.cpuHistory[i].T < cutoff {
|
|
||||||
i++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i < 0 {
|
|
||||||
i = 0
|
|
||||||
}
|
|
||||||
// copy to avoid exposing internal slice
|
|
||||||
out := make([]CPUSample, len(s.cpuHistory)-i)
|
|
||||||
copy(out, s.cpuHistory[i:])
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// sampleCPUUtilization returns a smoothed total CPU utilization percentage across all logical processors.
|
|
||||||
// It computes utilization from CPU time deltas (non-blocking) and applies an exponential moving average
|
|
||||||
// to reduce spikes similar to Task Manager's smoothing.
|
|
||||||
func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
||||||
// Prefer native Windows API to avoid external deps for CPU percent
|
// Prefer native Windows API to avoid external deps for CPU percent
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ type ClientTraffic struct {
|
||||||
InboundId int `json:"inboundId" form:"inboundId"`
|
InboundId int `json:"inboundId" form:"inboundId"`
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"`
|
||||||
Email string `json:"email" form:"email" gorm:"unique"`
|
Email string `json:"email" form:"email" gorm:"unique"`
|
||||||
|
SubId string `json:"subId" form:"subId" gorm:"-"`
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up"`
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"`
|
||||||
AllTime int64 `json:"allTime" form:"allTime"`
|
AllTime int64 `json:"allTime" form:"allTime"`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue