fix download Xray Logs

This commit is contained in:
mhsanaei 2025-09-17 12:51:46 +02:00
parent 135f843b3e
commit a128f75f64
No known key found for this signature in database
GPG key ID: D875CD086CF668A0

View file

@ -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>
@ -358,31 +365,24 @@
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox> <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</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(logModal.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="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,21 @@
<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="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></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 +426,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 +532,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 +598,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 +639,7 @@
`, `,
}) })
class CurTotal { class CurTotal {
constructor(current, total) { constructor(current, total) {
@ -692,7 +683,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 +719,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 +755,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;
@ -894,10 +885,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 +926,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 +1018,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 +1028,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 +1038,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');
@ -1097,6 +1105,7 @@
fileInput.click(); fileInput.click();
}, },
}, },
computed: {},
async mounted() { async mounted() {
if (window.location.protocol !== "https:") { if (window.location.protocol !== "https:") {
this.showAlert = true; this.showAlert = true;