|
@@ -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;
|
|
|
- },
|
|
|
- };
|
|
|
+ return formattedLogs;
|
|
|
+ },
|
|
|
+ hide() {
|
|
|
+ this.visible = false;
|
|
|
+ },
|
|
|
+ };
|
|
|
|
|
|
- const xraylogModal = {
|
|
|
- visible: false,
|
|
|
- logs: [],
|
|
|
- rows: 20,
|
|
|
- showDirect: true,
|
|
|
- showBlocked: true,
|
|
|
- showProxy: true,
|
|
|
- loading: false,
|
|
|
- show(logs) {
|
|
|
- this.visible = true;
|
|
|
- this.logs = logs;
|
|
|
- this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
|
|
|
- },
|
|
|
- formatLogs(logs) {
|
|
|
- let formattedLogs = '';
|
|
|
+ const xraylogModal = {
|
|
|
+ visible: false,
|
|
|
+ logs: [],
|
|
|
+ rows: 20,
|
|
|
+ showDirect: true,
|
|
|
+ showBlocked: true,
|
|
|
+ showProxy: true,
|
|
|
+ loading: false,
|
|
|
+ show(logs) {
|
|
|
+ this.visible = true;
|
|
|
+ this.logs = logs;
|
|
|
+ this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
|
|
|
+ },
|
|
|
+ formatLogs(logs) {
|
|
|
+ let formattedLogs = '';
|
|
|
|
|
|
- logs.forEach((log, index) => {
|
|
|
- if(index > 0) formattedLogs += '<br>';
|
|
|
+ logs.forEach((log, index) => {
|
|
|
+ if (index > 0) formattedLogs += '<br>';
|
|
|
|
|
|
- const parts = log.split(' ');
|
|
|
+ const parts = log.split(' ');
|
|
|
|
|
|
- if(parts.length === 10) {
|
|
|
- const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
|
|
|
- const from = `<b>${parts[3]}</b>`;
|
|
|
- const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
|
|
|
+ if (parts.length === 10) {
|
|
|
+ const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
|
|
|
+ const from = `<b>${parts[3]}</b>`;
|
|
|
+ const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
|
|
|
|
|
|
- let outboundColor = '';
|
|
|
- if (parts[9] === "b") {
|
|
|
- outboundColor = ' style="color: #e04141;"'; //red for blocked
|
|
|
- }
|
|
|
- else if (parts[9] === "p") {
|
|
|
- outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
|
|
|
- }
|
|
|
+ let outboundColor = '';
|
|
|
+ if (parts[9] === "b") {
|
|
|
+ outboundColor = ' style="color: #e04141;"'; //red for blocked
|
|
|
+ }
|
|
|
+ else if (parts[9] === "p") {
|
|
|
+ outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
|
|
|
+ }
|
|
|
|
|
|
- formattedLogs += `<span${outboundColor}>
|
|
|
+ formattedLogs += `<span${outboundColor}>
|
|
|
${dateTime}
|
|
|
${parts[2]}
|
|
|
${from}
|
|
@@ -845,261 +837,260 @@ ${dateTime}
|
|
|
${to}
|
|
|
${parts.slice(6, 9).join(' ')}
|
|
|
</span>`;
|
|
|
- } else {
|
|
|
- formattedLogs += `<span>${log}</span>`;
|
|
|
- }
|
|
|
- });
|
|
|
+ } else {
|
|
|
+ formattedLogs += `<span>${log}</span>`;
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- return formattedLogs;
|
|
|
- },
|
|
|
- hide() {
|
|
|
- this.visible = false;
|
|
|
- },
|
|
|
- };
|
|
|
+ return formattedLogs;
|
|
|
+ },
|
|
|
+ hide() {
|
|
|
+ this.visible = false;
|
|
|
+ },
|
|
|
+ };
|
|
|
|
|
|
- const backupModal = {
|
|
|
- visible: false,
|
|
|
- show() {
|
|
|
- this.visible = true;
|
|
|
- },
|
|
|
- hide() {
|
|
|
- this.visible = false;
|
|
|
- },
|
|
|
- };
|
|
|
+ const backupModal = {
|
|
|
+ visible: false,
|
|
|
+ show() {
|
|
|
+ this.visible = true;
|
|
|
+ },
|
|
|
+ hide() {
|
|
|
+ this.visible = false;
|
|
|
+ },
|
|
|
+ };
|
|
|
|
|
|
- const app = new Vue({
|
|
|
- delimiters: ['[[', ']]'],
|
|
|
- el: '#app',
|
|
|
- mixins: [MediaQueryMixin],
|
|
|
- data: {
|
|
|
- themeSwitcher,
|
|
|
- loadingStates: {
|
|
|
- fetched: false,
|
|
|
- spinning: false
|
|
|
- },
|
|
|
- status: new Status(),
|
|
|
- cpuHistory: [], // keep last N cpu utilization points (0..100)
|
|
|
- cpuHistoryLong: [], // long-range history for modal (values)
|
|
|
- cpuHistoryLabels: [], // formatted timestamps matching long history
|
|
|
- cpuHistoryModal: { visible: false, minutes: 60 },
|
|
|
- versionModal,
|
|
|
- logModal,
|
|
|
- xraylogModal,
|
|
|
- backupModal,
|
|
|
- loadingTip: '{{ i18n "loading"}}',
|
|
|
- showAlert: false,
|
|
|
- showIp: false,
|
|
|
- ipLimitEnable: false,
|
|
|
- },
|
|
|
- methods: {
|
|
|
- loading(spinning, tip = '{{ i18n "loading"}}') {
|
|
|
- this.loadingStates.spinning = spinning;
|
|
|
- this.loadingTip = tip;
|
|
|
- },
|
|
|
- async getStatus() {
|
|
|
- try {
|
|
|
- const msg = await HttpUtil.get('/panel/api/server/status');
|
|
|
- if (msg.success) {
|
|
|
- if (!this.loadingStates.fetched) {
|
|
|
- this.loadingStates.fetched = true;
|
|
|
- }
|
|
|
+ const app = new Vue({
|
|
|
+ delimiters: ['[[', ']]'],
|
|
|
+ el: '#app',
|
|
|
+ mixins: [MediaQueryMixin],
|
|
|
+ data: {
|
|
|
+ themeSwitcher,
|
|
|
+ loadingStates: {
|
|
|
+ fetched: false,
|
|
|
+ spinning: false
|
|
|
+ },
|
|
|
+ status: new Status(),
|
|
|
+ cpuHistory: [], // small live widget history
|
|
|
+ cpuHistoryLong: [], // aggregated points from backend
|
|
|
+ cpuHistoryLabels: [],
|
|
|
+ cpuHistoryModal: { visible: false, bucket: 2 },
|
|
|
+ versionModal,
|
|
|
+ logModal,
|
|
|
+ xraylogModal,
|
|
|
+ backupModal,
|
|
|
+ loadingTip: '{{ i18n "loading"}}',
|
|
|
+ showAlert: false,
|
|
|
+ showIp: false,
|
|
|
+ ipLimitEnable: false,
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ loading(spinning, tip = '{{ i18n "loading"}}') {
|
|
|
+ this.loadingStates.spinning = spinning;
|
|
|
+ this.loadingTip = tip;
|
|
|
+ },
|
|
|
+ async getStatus() {
|
|
|
+ try {
|
|
|
+ const msg = await HttpUtil.get('/panel/api/server/status');
|
|
|
+ if (msg.success) {
|
|
|
+ if (!this.loadingStates.fetched) {
|
|
|
+ this.loadingStates.fetched = true;
|
|
|
+ }
|
|
|
|
|
|
- this.setStatus(msg.obj, true);
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- console.error("Failed to get status:", e);
|
|
|
- }
|
|
|
- },
|
|
|
- setStatus(data) {
|
|
|
- this.status = new Status(data);
|
|
|
- // Push CPU percent into history (clamped 0..100)
|
|
|
- const v = Math.max(0, Math.min(100, Number(data?.cpu ?? 0)))
|
|
|
- this.cpuHistory.push(v)
|
|
|
- const maxPoints = this.isMobile ? 60 : 120
|
|
|
- if (this.cpuHistory.length > maxPoints) {
|
|
|
- this.cpuHistory.splice(0, this.cpuHistory.length - maxPoints)
|
|
|
- }
|
|
|
- },
|
|
|
+ this.setStatus(msg.obj, true);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error("Failed to get status:", e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ setStatus(data) {
|
|
|
+ this.status = new Status(data);
|
|
|
+ // Push CPU percent into history (clamped 0..100)
|
|
|
+ const v = Math.max(0, Math.min(100, Number(data?.cpu ?? 0)))
|
|
|
+ this.cpuHistory.push(v)
|
|
|
+ const maxPoints = this.isMobile ? 60 : 120
|
|
|
+ if (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() {
|
|
|
this.cpuHistoryModal.visible = true
|
|
|
- this.loadCpuHistory()
|
|
|
+ this.fetchCpuHistoryBucket()
|
|
|
},
|
|
|
- async loadCpuHistory() {
|
|
|
- const mins = this.cpuHistoryModal.minutes || 60
|
|
|
+ async fetchCpuHistoryBucket() {
|
|
|
+ const bucket = this.cpuHistoryModal.bucket || 2
|
|
|
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)) {
|
|
|
- // msg.obj is array of {t, cpu}
|
|
|
- const arr = msg.obj.map(p => Math.max(0, Math.min(100, Number(p.cpu || 0))))
|
|
|
- const labels = msg.obj.map(p => {
|
|
|
- const t = p.t
|
|
|
- let d
|
|
|
- if (typeof t === 'number') {
|
|
|
- // Heuristic: if seconds, convert to ms
|
|
|
- d = new Date(t < 1e12 ? t * 1000 : t)
|
|
|
- } else {
|
|
|
- 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
|
|
|
+ const vals = []
|
|
|
+ const labels = []
|
|
|
+ for (const p of msg.obj) {
|
|
|
+ const d = new Date(p.t * 1000)
|
|
|
+ const hh = String(d.getHours()).padStart(2,'0')
|
|
|
+ const mm = String(d.getMinutes()).padStart(2,'0')
|
|
|
+ const ss = String(d.getSeconds()).padStart(2,'0')
|
|
|
+ labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
|
|
|
+ vals.push(Math.max(0, Math.min(100, p.cpu)))
|
|
|
+ }
|
|
|
this.cpuHistoryLabels = labels
|
|
|
+ this.cpuHistoryLong = vals
|
|
|
}
|
|
|
- } catch (e) {
|
|
|
- console.error('Failed to load CPU history', e)
|
|
|
+ } catch(e) {
|
|
|
+ console.error('Failed to fetch bucketed cpu history', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async openSelectV2rayVersion() {
|
|
|
+ this.loading(true);
|
|
|
+ const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
|
|
|
+ this.loading(false);
|
|
|
+ if (!msg.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ versionModal.show(msg.obj);
|
|
|
+ },
|
|
|
+ switchV2rayVersion(version) {
|
|
|
+ this.$confirm({
|
|
|
+ title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
|
|
+ content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}'.replace('#version#', version),
|
|
|
+ okText: '{{ i18n "confirm"}}',
|
|
|
+ class: themeSwitcher.currentTheme,
|
|
|
+ cancelText: '{{ i18n "cancel"}}',
|
|
|
+ onOk: async () => {
|
|
|
+ versionModal.hide();
|
|
|
+ this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
|
|
+ await HttpUtil.post(`/panel/api/server/installXray/${version}`);
|
|
|
+ this.loading(false);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ updateGeofile(fileName) {
|
|
|
+ const isSingleFile = !!fileName;
|
|
|
+ this.$confirm({
|
|
|
+ title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
|
|
|
+ content: isSingleFile
|
|
|
+ ? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
|
|
|
+ : '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
|
|
|
+ okText: '{{ i18n "confirm"}}',
|
|
|
+ class: themeSwitcher.currentTheme,
|
|
|
+ cancelText: '{{ i18n "cancel"}}',
|
|
|
+ onOk: async () => {
|
|
|
+ versionModal.hide();
|
|
|
+ this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
|
|
+ const url = isSingleFile
|
|
|
+ ? `/panel/api/server/updateGeofile/${fileName}`
|
|
|
+ : `/panel/api/server/updateGeofile`;
|
|
|
+ await HttpUtil.post(url);
|
|
|
+ this.loading(false);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ async stopXrayService() {
|
|
|
+ this.loading(true);
|
|
|
+ const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
|
|
|
+ this.loading(false);
|
|
|
+ if (!msg.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async restartXrayService() {
|
|
|
+ this.loading(true);
|
|
|
+ const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
|
|
+ this.loading(false);
|
|
|
+ if (!msg.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async openLogs() {
|
|
|
+ logModal.loading = true;
|
|
|
+ const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog });
|
|
|
+ if (!msg.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ logModal.show(msg.obj);
|
|
|
+ await PromiseUtil.sleep(500);
|
|
|
+ logModal.loading = false;
|
|
|
+ },
|
|
|
+ async openXrayLogs() {
|
|
|
+ xraylogModal.loading = true;
|
|
|
+ const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy });
|
|
|
+ if (!msg.success) {
|
|
|
+ return;
|
|
|
}
|
|
|
+ xraylogModal.show(msg.obj);
|
|
|
+ await PromiseUtil.sleep(500);
|
|
|
+ xraylogModal.loading = false;
|
|
|
},
|
|
|
- async openSelectV2rayVersion() {
|
|
|
- this.loading(true);
|
|
|
- const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
|
|
|
- this.loading(false);
|
|
|
- if (!msg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- versionModal.show(msg.obj);
|
|
|
- },
|
|
|
- switchV2rayVersion(version) {
|
|
|
- this.$confirm({
|
|
|
- title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
|
|
- content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}'.replace('#version#', version),
|
|
|
- okText: '{{ i18n "confirm"}}',
|
|
|
- class: themeSwitcher.currentTheme,
|
|
|
- cancelText: '{{ i18n "cancel"}}',
|
|
|
- onOk: async () => {
|
|
|
- versionModal.hide();
|
|
|
- this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
|
|
- await HttpUtil.post(`/panel/api/server/installXray/${version}`);
|
|
|
- this.loading(false);
|
|
|
- },
|
|
|
- });
|
|
|
- },
|
|
|
- updateGeofile(fileName) {
|
|
|
- const isSingleFile = !!fileName;
|
|
|
- this.$confirm({
|
|
|
- title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
|
|
|
- content: isSingleFile
|
|
|
- ? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
|
|
|
- : '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
|
|
|
- okText: '{{ i18n "confirm"}}',
|
|
|
- class: themeSwitcher.currentTheme,
|
|
|
- cancelText: '{{ i18n "cancel"}}',
|
|
|
- onOk: async () => {
|
|
|
- versionModal.hide();
|
|
|
- this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
|
|
- const url = isSingleFile
|
|
|
- ? `/panel/api/server/updateGeofile/${fileName}`
|
|
|
- : `/panel/api/server/updateGeofile`;
|
|
|
- await HttpUtil.post(url);
|
|
|
- this.loading(false);
|
|
|
- },
|
|
|
- });
|
|
|
- },
|
|
|
- async stopXrayService() {
|
|
|
- this.loading(true);
|
|
|
- const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
|
|
|
- this.loading(false);
|
|
|
- if (!msg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- },
|
|
|
- async restartXrayService() {
|
|
|
- this.loading(true);
|
|
|
- const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
|
|
- this.loading(false);
|
|
|
- if (!msg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- },
|
|
|
- async openLogs(){
|
|
|
- logModal.loading = true;
|
|
|
- const msg = await HttpUtil.post('/panel/api/server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
|
|
- if (!msg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- logModal.show(msg.obj);
|
|
|
- await PromiseUtil.sleep(500);
|
|
|
- logModal.loading = false;
|
|
|
- },
|
|
|
- async openXrayLogs(){
|
|
|
- xraylogModal.loading = true;
|
|
|
- const msg = await HttpUtil.post('/panel/api/server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
|
|
|
- if (!msg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- xraylogModal.show(msg.obj);
|
|
|
- await PromiseUtil.sleep(500);
|
|
|
- xraylogModal.loading = false;
|
|
|
- },
|
|
|
- async openConfig() {
|
|
|
- this.loading(true);
|
|
|
- const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
|
|
- this.loading(false);
|
|
|
- if (!msg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
|
|
|
- },
|
|
|
- openBackup() {
|
|
|
- backupModal.show();
|
|
|
- },
|
|
|
- exportDatabase() {
|
|
|
- window.location = basePath + 'panel/api/server/getDb';
|
|
|
- },
|
|
|
- importDatabase() {
|
|
|
- const fileInput = document.createElement('input');
|
|
|
- fileInput.type = 'file';
|
|
|
- fileInput.accept = '.db';
|
|
|
- fileInput.addEventListener('change', async (event) => {
|
|
|
- const dbFile = event.target.files[0];
|
|
|
- if (dbFile) {
|
|
|
- const formData = new FormData();
|
|
|
- formData.append('db', dbFile);
|
|
|
- backupModal.hide();
|
|
|
- this.loading(true);
|
|
|
- const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
|
|
|
- headers: {
|
|
|
- 'Content-Type': 'multipart/form-data',
|
|
|
- }
|
|
|
- });
|
|
|
- this.loading(false);
|
|
|
- if (!uploadMsg.success) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.loading(true);
|
|
|
- const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
|
|
|
- this.loading(false);
|
|
|
- if (restartMsg.success) {
|
|
|
- this.loading(true);
|
|
|
- await PromiseUtil.sleep(5000);
|
|
|
- location.reload();
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- fileInput.click();
|
|
|
- },
|
|
|
- },
|
|
|
- async mounted() {
|
|
|
- if (window.location.protocol !== "https:") {
|
|
|
- this.showAlert = true;
|
|
|
+ async openConfig() {
|
|
|
+ this.loading(true);
|
|
|
+ const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
|
|
+ this.loading(false);
|
|
|
+ if (!msg.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
|
|
|
+ },
|
|
|
+ openBackup() {
|
|
|
+ backupModal.show();
|
|
|
+ },
|
|
|
+ exportDatabase() {
|
|
|
+ window.location = basePath + 'panel/api/server/getDb';
|
|
|
+ },
|
|
|
+ importDatabase() {
|
|
|
+ const fileInput = document.createElement('input');
|
|
|
+ fileInput.type = 'file';
|
|
|
+ fileInput.accept = '.db';
|
|
|
+ fileInput.addEventListener('change', async (event) => {
|
|
|
+ const dbFile = event.target.files[0];
|
|
|
+ if (dbFile) {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('db', dbFile);
|
|
|
+ backupModal.hide();
|
|
|
+ this.loading(true);
|
|
|
+ const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data',
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.loading(false);
|
|
|
+ if (!uploadMsg.success) {
|
|
|
+ return;
|
|
|
}
|
|
|
-
|
|
|
- const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
|
|
- if (msg.success) {
|
|
|
- this.ipLimitEnable = msg.obj.ipLimitEnable;
|
|
|
+ this.loading(true);
|
|
|
+ const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
|
|
|
+ this.loading(false);
|
|
|
+ if (restartMsg.success) {
|
|
|
+ this.loading(true);
|
|
|
+ await PromiseUtil.sleep(5000);
|
|
|
+ location.reload();
|
|
|
}
|
|
|
+ }
|
|
|
+ });
|
|
|
+ fileInput.click();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ computed: {},
|
|
|
+ async mounted() {
|
|
|
+ if (window.location.protocol !== "https:") {
|
|
|
+ this.showAlert = true;
|
|
|
+ }
|
|
|
|
|
|
- while (true) {
|
|
|
- try {
|
|
|
- await this.getStatus();
|
|
|
- } catch (e) {
|
|
|
- console.error(e);
|
|
|
- }
|
|
|
- await PromiseUtil.sleep(2000);
|
|
|
- }
|
|
|
- },
|
|
|
- });
|
|
|
+ const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
|
|
+ if (msg.success) {
|
|
|
+ this.ipLimitEnable = msg.obj.ipLimitEnable;
|
|
|
+ }
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ try {
|
|
|
+ await this.getStatus();
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ }
|
|
|
+ await PromiseUtil.sleep(2000);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
</script>
|
|
|
{{ template "page/body_end" .}}
|