1
0

index.html 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183
  1. {{ template "page/head_start" .}}
  2. {{ template "page/head_end" .}}
  3. {{ template "page/body_start" .}}
  4. <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' index-page'">
  5. <a-sidebar></a-sidebar>
  6. <a-layout id="content-layout">
  7. <a-layout-content>
  8. <a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip" size="large">
  9. <transition name="list" appear>
  10. <a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
  11. message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
  12. </a-alert>
  13. </transition>
  14. <transition name="list" appear>
  15. <template>
  16. <a-row v-if="!loadingStates.fetched">
  17. <div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
  18. </a-row>
  19. <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
  20. <a-col>
  21. <a-card hoverable>
  22. <a-row :gutter="[0, isMobile ? 16 : 0]">
  23. <a-col :sm="24" :md="12">
  24. <a-row>
  25. <a-col :span="12" class="text-center">
  26. <a-progress type="dashboard" status="normal" :stroke-color="status.cpu.color"
  27. :percent="status.cpu.percent"></a-progress>
  28. <div>
  29. <b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]
  30. <a-tooltip>
  31. <a-icon type="area-chart"></a-icon>
  32. <template slot="title">
  33. <div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
  34. <div><b>{{ i18n "pages.index.frequency" }}:</b> [[
  35. CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
  36. </template>
  37. </a-tooltip>
  38. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  39. <a-button size="small" shape="circle" class="ml-8" @click="openCpuHistory()">
  40. <a-icon type="history" />
  41. </a-button>
  42. </a-tooltip>
  43. </div>
  44. </a-col>
  45. <a-col :span="12" class="text-center">
  46. <a-progress type="dashboard" status="normal" :stroke-color="status.mem.color"
  47. :percent="status.mem.percent"></a-progress>
  48. <div>
  49. <b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] /
  50. [[ SizeFormatter.sizeFormat(status.mem.total) ]]
  51. </div>
  52. </a-col>
  53. </a-row>
  54. </a-col>
  55. <a-col :sm="24" :md="12">
  56. <a-row>
  57. <a-col :span="12" class="text-center">
  58. <a-progress type="dashboard" status="normal" :stroke-color="status.swap.color"
  59. :percent="status.swap.percent"></a-progress>
  60. <div>
  61. <b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] /
  62. [[ SizeFormatter.sizeFormat(status.swap.total) ]]
  63. </div>
  64. </a-col>
  65. <a-col :span="12" class="text-center">
  66. <a-progress type="dashboard" status="normal" :stroke-color="status.disk.color"
  67. :percent="status.disk.percent"></a-progress>
  68. <div>
  69. <b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]]
  70. / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
  71. </div>
  72. </a-col>
  73. </a-row>
  74. </a-col>
  75. </a-row>
  76. </a-card>
  77. </a-col>
  78. <a-col :sm="24" :lg="12">
  79. <a-card hoverable>
  80. <template #title>
  81. <a-space direction="horizontal">
  82. <span>{{ i18n "pages.index.xrayStatus" }}</span>
  83. <a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
  84. v[[ status.xray.version ]]
  85. </a-tag>
  86. </a-space>
  87. </template>
  88. <template #extra>
  89. <template v-if="status.xray.state != 'error'">
  90. <a-badge status="processing"
  91. :class="({ green: 'xray-running-animation', orange: 'xray-stop-animation' }[status.xray.color]) || 'xray-processing-animation'"
  92. :text="status.xray.stateMsg" :color="status.xray.color" />
  93. </template>
  94. <template v-else>
  95. <a-popover :overlay-class-name="themeSwitcher.currentTheme">
  96. <span slot="title">
  97. <a-row type="flex" align="middle" justify="space-between">
  98. <a-col>
  99. <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
  100. </a-col>
  101. <a-col>
  102. <a-icon type="bars" class="cursor-pointer float-right" @click="openLogs()"></a-icon>
  103. </a-col>
  104. </a-row>
  105. </span>
  106. <template slot="content">
  107. <span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
  108. </template>
  109. <a-badge :text="status.xray.stateMsg" :color="status.xray.color"
  110. :class="status.xray.color === 'red' ? 'xray-error-animation' : ''" />
  111. </a-popover>
  112. </template>
  113. </template>
  114. <template #actions>
  115. <a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" class="jc-center">
  116. <a-icon type="bars"></a-icon>
  117. <span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
  118. </a-space>
  119. <a-space direction="horizontal" @click="stopXrayService" class="jc-center">
  120. <a-icon type="poweroff"></a-icon>
  121. <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
  122. </a-space>
  123. <a-space direction="horizontal" @click="restartXrayService" class="jc-center">
  124. <a-icon type="reload"></a-icon>
  125. <span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
  126. </a-space>
  127. <a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center">
  128. <a-icon type="tool"></a-icon>
  129. <span v-if="!isMobile">
  130. [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n
  131. "pages.index.xraySwitch" }}' ]]
  132. </span>
  133. </a-space>
  134. </template>
  135. </a-card>
  136. </a-col>
  137. <a-col :sm="24" :lg="12">
  138. <a-card title='{{ i18n "menu.link" }}' hoverable>
  139. <template #actions>
  140. <a-space direction="horizontal" @click="openLogs()" class="jc-center">
  141. <a-icon type="bars"></a-icon>
  142. <span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
  143. </a-space>
  144. <a-space direction="horizontal" @click="openConfig" class="jc-center">
  145. <a-icon type="control"></a-icon>
  146. <span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
  147. </a-space>
  148. <a-space direction="horizontal" @click="openBackup" class="jc-center">
  149. <a-icon type="cloud-server"></a-icon>
  150. <span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
  151. </a-space>
  152. </template>
  153. </a-card>
  154. </a-col>
  155. <a-col :sm="24" :lg="12">
  156. <a-card title='3X-UI' hoverable>
  157. <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank">
  158. <a-tag color="green">
  159. <span>v{{ .cur_ver }}</span>
  160. </a-tag>
  161. </a>
  162. <a rel="noopener" href="https://t.me/XrayUI" target="_blank">
  163. <a-tag color="green">
  164. <span>@XrayUI</span>
  165. </a-tag>
  166. </a>
  167. <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank">
  168. <a-tag color="purple">
  169. <span>{{ i18n "pages.index.documentation" }}</span>
  170. </a-tag>
  171. </a>
  172. </a-card>
  173. </a-col>
  174. <a-col :sm="24" :lg="12">
  175. <a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
  176. <a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime)
  177. ]]</a-tag>
  178. <a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
  179. </a-card>
  180. </a-col>
  181. <a-col :sm="24" :lg="12">
  182. <a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
  183. <a-tag color="green">
  184. <a-tooltip>
  185. [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
  186. <template slot="title">
  187. {{ i18n "pages.index.systemLoadDesc" }}
  188. </template>
  189. </a-tooltip>
  190. </a-tag>
  191. </a-card>
  192. </a-col>
  193. <a-col :sm="24" :lg="12">
  194. <a-card title='{{ i18n "usage"}}' hoverable>
  195. <a-tag color="green"> {{ i18n "pages.index.memory" }}: [[
  196. SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
  197. <a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
  198. </a-card>
  199. </a-col>
  200. <a-col :sm="24" :lg="12">
  201. <a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
  202. <a-row :gutter="isMobile ? [8,8] : 0">
  203. <a-col :span="12">
  204. <a-custom-statistic title='{{ i18n "pages.index.upload" }}'
  205. :value="SizeFormatter.sizeFormat(status.netIO.up)">
  206. <template #prefix>
  207. <a-icon type="arrow-up" />
  208. </template>
  209. <template #suffix>
  210. /s
  211. </template>
  212. </a-custom-statistic>
  213. </a-col>
  214. <a-col :span="12">
  215. <a-custom-statistic title='{{ i18n "pages.index.download" }}'
  216. :value="SizeFormatter.sizeFormat(status.netIO.down)">
  217. <template #prefix>
  218. <a-icon type="arrow-down" />
  219. </template>
  220. <template #suffix>
  221. /s
  222. </template>
  223. </a-custom-statistic>
  224. </a-col>
  225. </a-row>
  226. </a-card>
  227. </a-col>
  228. <a-col :sm="24" :lg="12">
  229. <a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
  230. <a-row :gutter="isMobile ? [8,8] : 0">
  231. <a-col :span="12">
  232. <a-custom-statistic title='{{ i18n "pages.index.sent" }}'
  233. :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
  234. <template #prefix>
  235. <a-icon type="cloud-upload" />
  236. </template>
  237. </a-custom-statistic>
  238. </a-col>
  239. <a-col :span="12">
  240. <a-custom-statistic title='{{ i18n "pages.index.received" }}'
  241. :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
  242. <template #prefix>
  243. <a-icon type="cloud-download" />
  244. </template>
  245. </a-custom-statistic>
  246. </a-col>
  247. </a-row>
  248. </a-card>
  249. </a-col>
  250. <a-col :sm="24" :lg="12">
  251. <a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
  252. <template #extra>
  253. <a-tooltip :placement="isMobile ? 'topRight' : 'top'">
  254. <template #title>
  255. {{ i18n "pages.index.toggleIpVisibility" }}
  256. </template>
  257. <a-icon :type="showIp ? 'eye' : 'eye-invisible'" class="fs-1rem"
  258. @click="showIp = !showIp"></a-icon>
  259. </a-tooltip>
  260. </template>
  261. <a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
  262. <a-col :span="isMobile ? 24 : 12">
  263. <a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
  264. <template #prefix>
  265. <a-icon type="global" />
  266. </template>
  267. </a-custom-statistic>
  268. </a-col>
  269. <a-col :span="isMobile ? 24 : 12">
  270. <a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
  271. <template #prefix>
  272. <a-icon type="global" />
  273. </template>
  274. </a-custom-statistic>
  275. </a-col>
  276. </a-row>
  277. </a-card>
  278. </a-col>
  279. <a-col :sm="24" :lg="12">
  280. <a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
  281. <a-row :gutter="isMobile ? [8,8] : 0">
  282. <a-col :span="12">
  283. <a-custom-statistic title="TCP" :value="status.tcpCount">
  284. <template #prefix>
  285. <a-icon type="swap" />
  286. </template>
  287. </a-custom-statistic>
  288. </a-col>
  289. <a-col :span="12">
  290. <a-custom-statistic title="UDP" :value="status.udpCount">
  291. <template #prefix>
  292. <a-icon type="swap" />
  293. </template>
  294. </a-custom-statistic>
  295. </a-col>
  296. </a-row>
  297. </a-card>
  298. </a-col>
  299. </a-row>
  300. </template>
  301. </transition>
  302. </a-spin>
  303. </a-layout-content>
  304. </a-layout>
  305. <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
  306. :closable="true" @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
  307. <a-collapse default-active-key="1">
  308. <a-collapse-panel key="1" header='Xray'>
  309. <a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.xraySwitchClickDesk" }}'
  310. show-icon></a-alert>
  311. <a-list class="ant-version-list w-100" bordered>
  312. <a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
  313. <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
  314. <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`"
  315. @click="switchV2rayVersion(version)"></a-radio>
  316. </a-list-item>
  317. </a-list>
  318. </a-collapse-panel>
  319. <a-collapse-panel key="2" header='Geofiles'>
  320. <a-list class="ant-version-list w-100" bordered>
  321. <a-list-item class="ant-version-list-item"
  322. v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
  323. <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
  324. <a-icon type="reload" @click="updateGeofile(file)" class="mr-8" />
  325. </a-list-item>
  326. </a-list>
  327. <div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n
  328. "pages.index.geofilesUpdateAll" }}</a-button></div>
  329. </a-collapse-panel>
  330. </a-collapse>
  331. </a-modal>
  332. <a-modal id="log-modal" v-model="logModal.visible" :closable="true" @cancel="() => logModal.visible = false"
  333. :class="themeSwitcher.currentTheme" width="800px" footer="">
  334. <template slot="title">
  335. {{ i18n "pages.index.logs" }}
  336. <a-icon :spin="logModal.loading" type="sync" class="va-middle ml-10" :disabled="logModal.loading"
  337. @click="openLogs()">
  338. </a-icon>
  339. </template>
  340. <a-form layout="inline">
  341. <a-form-item class="mr-05">
  342. <a-input-group compact>
  343. <a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }" @change="openLogs()"
  344. :dropdown-class-name="themeSwitcher.currentTheme">
  345. <a-select-option value="10">10</a-select-option>
  346. <a-select-option value="20">20</a-select-option>
  347. <a-select-option value="50">50</a-select-option>
  348. <a-select-option value="100">100</a-select-option>
  349. <a-select-option value="500">500</a-select-option>
  350. </a-select>
  351. <a-select size="small" v-model="logModal.level" :style="{ width: '95px' }" @change="openLogs()"
  352. :dropdown-class-name="themeSwitcher.currentTheme">
  353. <a-select-option value="debug">Debug</a-select-option>
  354. <a-select-option value="info">Info</a-select-option>
  355. <a-select-option value="notice">Notice</a-select-option>
  356. <a-select-option value="warning">Warning</a-select-option>
  357. <a-select-option value="err">Error</a-select-option>
  358. </a-select>
  359. </a-input-group>
  360. </a-form-item>
  361. <a-form-item>
  362. <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
  363. </a-form-item>
  364. <a-form-item style="float: right;">
  365. <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
  366. </a-form-item>
  367. </a-form>
  368. <div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
  369. </a-modal>
  370. <a-modal id="xraylog-modal" v-model="xraylogModal.visible" :closable="true"
  371. @cancel="() => xraylogModal.visible = false" :class="themeSwitcher.currentTheme" width="80vw" footer="">
  372. <template slot="title">
  373. {{ i18n "pages.index.logs" }}
  374. <a-icon :spin="xraylogModal.loading" type="sync" class="va-middle ml-10" :disabled="xraylogModal.loading"
  375. @click="openXrayLogs()">
  376. </a-icon>
  377. </template>
  378. <a-form layout="inline">
  379. <a-form-item class="mr-05">
  380. <a-input-group compact>
  381. <a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }" @change="openXrayLogs()"
  382. :dropdown-class-name="themeSwitcher.currentTheme">
  383. <a-select-option value="10">10</a-select-option>
  384. <a-select-option value="20">20</a-select-option>
  385. <a-select-option value="50">50</a-select-option>
  386. <a-select-option value="100">100</a-select-option>
  387. <a-select-option value="500">500</a-select-option>
  388. </a-select>
  389. </a-input-group>
  390. </a-form-item>
  391. <a-form-item label="Filter:">
  392. <a-input size="small" v-model="xraylogModal.filter" @keyup.enter="openXrayLogs()"></a-input>
  393. </a-form-item>
  394. <a-form-item>
  395. <a-checkbox v-model="xraylogModal.showDirect" @change="openXrayLogs()">Direct</a-checkbox>
  396. <a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
  397. <a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
  398. </a-form-item>
  399. <a-form-item style="float: right;">
  400. <a-button type="primary" icon="download" @click="downloadXrayLogs"></a-button>
  401. </a-form-item>
  402. </a-form>
  403. <div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
  404. </a-modal>
  405. <a-modal id="backup-modal" v-model="backupModal.visible" title='{{ i18n "pages.index.backupTitle" }}' :closable="true"
  406. footer="" :class="themeSwitcher.currentTheme">
  407. <a-list class="ant-backup-list w-100" bordered>
  408. <a-list-item class="ant-backup-list-item">
  409. <a-list-item-meta>
  410. <template #title>{{ i18n "pages.index.exportDatabase" }}</template>
  411. <template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
  412. </a-list-item-meta>
  413. <a-button @click="exportDatabase()" type="primary" icon="download" />
  414. </a-list-item>
  415. <a-list-item class="ant-backup-list-item">
  416. <a-list-item-meta>
  417. <template #title>{{ i18n "pages.index.importDatabase" }}</template>
  418. <template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
  419. </a-list-item-meta>
  420. <a-button @click="importDatabase()" type="primary" icon="upload" />
  421. </a-list-item>
  422. </a-list>
  423. </a-modal>
  424. <!-- CPU History Modal -->
  425. <a-modal id="cpu-history-modal" v-model="cpuHistoryModal.visible" :closable="true"
  426. @cancel="() => cpuHistoryModal.visible = false" :class="themeSwitcher.currentTheme" width="900px" footer="">
  427. <template slot="title">
  428. CPU History
  429. <a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
  430. @change="fetchCpuHistoryBucket">
  431. <a-select-option :value="2">2m</a-select-option>
  432. <a-select-option :value="30">30m</a-select-option>
  433. <a-select-option :value="60">1h</a-select-option>
  434. <a-select-option :value="120">2h</a-select-option>
  435. <a-select-option :value="180">3h</a-select-option>
  436. <a-select-option :value="300">5h</a-select-option>
  437. </a-select>
  438. </template>
  439. <div style="padding:16px">
  440. <sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
  441. :stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
  442. :max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
  443. <div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div>
  444. </div>
  445. </a-modal>
  446. </a-layout>
  447. {{template "page/body_scripts" .}}
  448. {{template "component/aSidebar" .}}
  449. {{template "component/aThemeSwitch" .}}
  450. {{template "component/aCustomStatistic" .}}
  451. {{template "modals/textModal"}}
  452. <script>
  453. // Tiny Sparkline component using an inline SVG polyline
  454. Vue.component('sparkline', {
  455. props: {
  456. data: { type: Array, required: true },
  457. // viewBox width for drawing space; SVG width will be 100% of container
  458. vbWidth: { type: Number, default: 320 },
  459. height: { type: Number, default: 80 },
  460. stroke: { type: String, default: '#008771' },
  461. strokeWidth: { type: Number, default: 2 },
  462. maxPoints: { type: Number, default: 120 },
  463. showGrid: { type: Boolean, default: true },
  464. gridColor: { type: String, default: 'rgba(0,0,0,0.1)' },
  465. fillOpacity: { type: Number, default: 0.15 },
  466. showMarker: { type: Boolean, default: true },
  467. markerRadius: { type: Number, default: 2.8 },
  468. // New opts for axes/labels/tooltip
  469. labels: { type: Array, default: () => [] }, // same length as data for x labels (e.g., timestamps)
  470. showAxes: { type: Boolean, default: false },
  471. yTickStep: { type: Number, default: 25 }, // percent ticks
  472. tickCountX: { type: Number, default: 4 },
  473. paddingLeft: { type: Number, default: 32 },
  474. paddingRight: { type: Number, default: 6 },
  475. paddingTop: { type: Number, default: 6 },
  476. paddingBottom: { type: Number, default: 20 },
  477. showTooltip: { type: Boolean, default: false },
  478. },
  479. data() {
  480. return {
  481. hoverIdx: -1,
  482. }
  483. },
  484. computed: {
  485. viewBoxAttr() {
  486. return '0 0 ' + this.vbWidth + ' ' + this.height
  487. },
  488. drawWidth() {
  489. return Math.max(1, this.vbWidth - this.paddingLeft - this.paddingRight)
  490. },
  491. drawHeight() {
  492. return Math.max(1, this.height - this.paddingTop - this.paddingBottom)
  493. },
  494. nPoints() {
  495. return Math.min(this.data.length, this.maxPoints)
  496. },
  497. dataSlice() {
  498. const n = this.nPoints
  499. if (n === 0) return []
  500. return this.data.slice(this.data.length - n)
  501. },
  502. labelsSlice() {
  503. const n = this.nPoints
  504. if (!this.labels || this.labels.length === 0 || n === 0) return []
  505. const start = Math.max(0, this.labels.length - n)
  506. return this.labels.slice(start)
  507. },
  508. pointsArr() {
  509. const n = this.nPoints
  510. if (n === 0) return []
  511. const slice = this.dataSlice
  512. const max = 100
  513. const w = this.drawWidth
  514. const h = this.drawHeight
  515. const dx = n > 1 ? w / (n - 1) : 0
  516. return slice.map((v, i) => {
  517. const x = Math.round(this.paddingLeft + i * dx)
  518. const y = Math.round(this.paddingTop + (h - (Math.max(0, Math.min(100, v)) / max) * h))
  519. return [x, y]
  520. })
  521. },
  522. points() {
  523. return this.pointsArr.map(p => `${p[0]},${p[1]}`).join(' ')
  524. },
  525. areaPath() {
  526. if (this.pointsArr.length === 0) return ''
  527. const first = this.pointsArr[0]
  528. const last = this.pointsArr[this.pointsArr.length - 1]
  529. const line = this.points
  530. // Close to bottom to create an area fill
  531. return `M ${first[0]},${this.paddingTop + this.drawHeight} L ${line.replace(/ /g, ' L ')} L ${last[0]},${this.paddingTop + this.drawHeight} Z`
  532. },
  533. gridLines() {
  534. if (!this.showGrid) return []
  535. const h = this.drawHeight
  536. const w = this.drawWidth
  537. // draw at 25%, 50%, 75%
  538. return [0, 0.25, 0.5, 0.75, 1]
  539. .map(r => Math.round(this.paddingTop + h * r))
  540. .map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
  541. },
  542. lastPoint() {
  543. if (this.pointsArr.length === 0) return null
  544. return this.pointsArr[this.pointsArr.length - 1]
  545. },
  546. yTicks() {
  547. if (!this.showAxes) return []
  548. const step = Math.max(1, this.yTickStep)
  549. const ticks = []
  550. for (let p = 0; p <= 100; p += step) {
  551. const y = Math.round(this.paddingTop + (this.drawHeight - (p / 100) * this.drawHeight))
  552. ticks.push({ y, label: `${p}%` })
  553. }
  554. return ticks
  555. },
  556. xTicks() {
  557. if (!this.showAxes) return []
  558. const labels = this.labelsSlice
  559. const n = this.nPoints
  560. const m = Math.max(2, this.tickCountX)
  561. const ticks = []
  562. if (n === 0) return ticks
  563. const w = this.drawWidth
  564. const dx = n > 1 ? w / (n - 1) : 0
  565. const positions = []
  566. for (let i = 0; i < m; i++) {
  567. const idx = Math.round((i * (n - 1)) / (m - 1))
  568. positions.push(idx)
  569. }
  570. positions.forEach(idx => {
  571. const label = labels[idx] != null ? String(labels[idx]) : String(idx)
  572. const x = Math.round(this.paddingLeft + idx * dx)
  573. ticks.push({ x, label })
  574. })
  575. return ticks
  576. },
  577. },
  578. methods: {
  579. onMouseMove(evt) {
  580. if (!this.showTooltip || this.pointsArr.length === 0) return
  581. const rect = evt.currentTarget.getBoundingClientRect()
  582. const px = evt.clientX - rect.left
  583. // translate to viewBox space
  584. const x = (px / rect.width) * this.vbWidth
  585. const n = this.nPoints
  586. const dx = n > 1 ? this.drawWidth / (n - 1) : 0
  587. const idx = Math.max(0, Math.min(n - 1, Math.round((x - this.paddingLeft) / (dx || 1))))
  588. this.hoverIdx = idx
  589. },
  590. onMouseLeave() {
  591. this.hoverIdx = -1
  592. },
  593. fmtHoverText() {
  594. const labels = this.labelsSlice
  595. const idx = this.hoverIdx
  596. if (idx < 0 || idx >= this.dataSlice.length) return ''
  597. const raw = Math.max(0, Math.min(100, Number(this.dataSlice[idx] || 0)))
  598. const val = Number.isFinite(raw) ? raw.toFixed(2) : raw
  599. const lab = labels[idx] != null ? labels[idx] : ''
  600. return `${val}%${lab ? ' • ' + lab : ''}`
  601. },
  602. },
  603. template: `
  604. <svg width="100%" :height="height" :viewBox="viewBoxAttr" preserveAspectRatio="none" class="idx-cpu-history-svg"
  605. @mousemove="onMouseMove" @mouseleave="onMouseLeave">
  606. <defs>
  607. <linearGradient id="spkGrad" x1="0" y1="0" x2="0" y2="1">
  608. <stop offset="0%" :stop-color="stroke" :stop-opacity="fillOpacity"/>
  609. <stop offset="100%" :stop-color="stroke" stop-opacity="0"/>
  610. </linearGradient>
  611. </defs>
  612. <g v-if="showGrid">
  613. <line v-for="(g,i) in gridLines" :key="i" :x1="g.x1" :y1="g.y1" :x2="g.x2" :y2="g.y2" :stroke="gridColor" stroke-width="1" class="cpu-grid-line" />
  614. </g>
  615. <g v-if="showAxes">
  616. <!-- Y ticks/labels -->
  617. <g v-for="(t,i) in yTicks" :key="'y'+i">
  618. <text class="cpu-grid-y-text" :x="Math.max(0, paddingLeft - 4)" :y="t.y + 4" text-anchor="end" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
  619. </g>
  620. <!-- X ticks/labels -->
  621. <g v-for="(t,i) in xTicks" :key="'x'+i">
  622. <text class="cpu-grid-x-text" :x="t.x" :y="paddingTop + drawHeight + 22" text-anchor="middle" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
  623. </g>
  624. </g>
  625. <path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
  626. <polyline :points="points" fill="none" :stroke="stroke" :stroke-width="strokeWidth" stroke-linecap="round" stroke-linejoin="round"/>
  627. <circle v-if="showMarker && lastPoint" :cx="lastPoint[0]" :cy="lastPoint[1]" :r="markerRadius" :fill="stroke" />
  628. <!-- Hover marker/tooltip -->
  629. <g v-if="showTooltip && hoverIdx >= 0">
  630. <line class="cpu-grid-h-line" :x1="pointsArr[hoverIdx][0]" :x2="pointsArr[hoverIdx][0]" :y1="paddingTop" :y2="paddingTop + drawHeight" stroke="rgba(0,0,0,0.2)" stroke-width="1" />
  631. <circle :cx="pointsArr[hoverIdx][0]" :cy="pointsArr[hoverIdx][1]" r="3.5" :fill="stroke" />
  632. <text class="cpu-grid-text" :x="pointsArr[hoverIdx][0]" :y="paddingTop + 12" text-anchor="middle" font-size="11" fill="rgba(0,0,0,0.8)" v-text="fmtHoverText()"></text>
  633. </g>
  634. </svg>
  635. `,
  636. })
  637. class CurTotal {
  638. constructor(current, total) {
  639. this.current = current;
  640. this.total = total;
  641. }
  642. get percent() {
  643. if (this.total === 0) {
  644. return 0;
  645. }
  646. return NumberFormatter.toFixed(this.current / this.total * 100, 2);
  647. }
  648. get color() {
  649. const percent = this.percent;
  650. if (percent < 80) {
  651. return '#008771'; // Green
  652. } else if (percent < 90) {
  653. return "#f37b24"; // Orange
  654. } else {
  655. return "#cf3c3c"; // Red
  656. }
  657. }
  658. }
  659. class Status {
  660. constructor(data) {
  661. this.cpu = new CurTotal(0, 0);
  662. this.cpuCores = 0;
  663. this.logicalPro = 0;
  664. this.cpuSpeedMhz = 0;
  665. this.disk = new CurTotal(0, 0);
  666. this.loads = [0, 0, 0];
  667. this.mem = new CurTotal(0, 0);
  668. this.netIO = { up: 0, down: 0 };
  669. this.netTraffic = { sent: 0, recv: 0 };
  670. this.publicIP = { ipv4: 0, ipv6: 0 };
  671. this.swap = new CurTotal(0, 0);
  672. this.tcpCount = 0;
  673. this.udpCount = 0;
  674. this.uptime = 0;
  675. this.appUptime = 0;
  676. this.appStats = { threads: 0, mem: 0, uptime: 0 };
  677. this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" };
  678. if (data == null) {
  679. return;
  680. }
  681. this.cpu = new CurTotal(data.cpu, 100);
  682. this.cpuCores = data.cpuCores;
  683. this.logicalPro = data.logicalPro;
  684. this.cpuSpeedMhz = data.cpuSpeedMhz;
  685. this.disk = new CurTotal(data.disk.current, data.disk.total);
  686. this.loads = data.loads.map(load => NumberFormatter.toFixed(load, 2));
  687. this.mem = new CurTotal(data.mem.current, data.mem.total);
  688. this.netIO = data.netIO;
  689. this.netTraffic = data.netTraffic;
  690. this.publicIP = data.publicIP;
  691. this.swap = new CurTotal(data.swap.current, data.swap.total);
  692. this.tcpCount = data.tcpCount;
  693. this.udpCount = data.udpCount;
  694. this.uptime = data.uptime;
  695. this.appUptime = data.appUptime;
  696. this.appStats = data.appStats;
  697. this.xray = data.xray;
  698. switch (this.xray.state) {
  699. case 'running':
  700. this.xray.color = "green";
  701. this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}';
  702. break;
  703. case 'stop':
  704. this.xray.color = "orange";
  705. this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}';
  706. break;
  707. case 'error':
  708. this.xray.color = "red";
  709. this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}';
  710. break;
  711. default:
  712. this.xray.color = "gray";
  713. this.xray.stateMsg = '{{ i18n "pages.index.xrayStatusUnknown" }}';
  714. break;
  715. }
  716. }
  717. }
  718. const versionModal = {
  719. visible: false,
  720. versions: [],
  721. show(versions) {
  722. this.visible = true;
  723. this.versions = versions;
  724. },
  725. hide() {
  726. this.visible = false;
  727. },
  728. };
  729. const logModal = {
  730. visible: false,
  731. logs: [],
  732. rows: 20,
  733. level: 'info',
  734. syslog: false,
  735. loading: false,
  736. show(logs) {
  737. this.visible = true;
  738. this.logs = logs;
  739. this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
  740. },
  741. formatLogs(logs) {
  742. let formattedLogs = '';
  743. const levels = ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR"];
  744. const levelColors = ["#3c89e8", "#008771", "#008771", "#f37b24", "#e04141", "#bcbcbc"];
  745. logs.forEach((log, index) => {
  746. let [data, message] = log.split(" - ", 2);
  747. const parts = data.split(" ")
  748. if (index > 0) formattedLogs += '<br>';
  749. if (parts.length === 3) {
  750. const d = parts[0];
  751. const t = parts[1];
  752. const level = parts[2];
  753. const levelIndex = levels.indexOf(level, levels) || 5;
  754. //formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
  755. formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
  756. formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
  757. } else {
  758. const levelIndex = levels.indexOf(data, levels) || 5;
  759. formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
  760. }
  761. if (message) {
  762. if (message.startsWith("XRAY:"))
  763. message = "<b>XRAY: </b>" + message.substring(5);
  764. else
  765. message = "<b>X-UI: </b>" + message;
  766. }
  767. formattedLogs += message ? ' - ' + message : '';
  768. });
  769. return formattedLogs;
  770. },
  771. hide() {
  772. this.visible = false;
  773. },
  774. };
  775. const xraylogModal = {
  776. visible: false,
  777. logs: [],
  778. rows: 20,
  779. showDirect: true,
  780. showBlocked: true,
  781. showProxy: true,
  782. loading: false,
  783. show(logs) {
  784. this.visible = true;
  785. this.logs = logs;
  786. this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
  787. },
  788. formatLogs(logs) {
  789. let formattedLogs = `
  790. <style>
  791. table {
  792. border-collapse: collapse;
  793. width: auto;
  794. }
  795. table td, table th {
  796. padding: 2px 15px;
  797. }
  798. </style>
  799. <table>
  800. <tr>
  801. <th>Date</th>
  802. <th>From</th>
  803. <th>To</th>
  804. <th>Inbound</th>
  805. <th>Outbound</th>
  806. <th>Email</th>
  807. </tr>
  808. `;
  809. logs.reverse().forEach((log, index) => {
  810. let outboundColor = '';
  811. if (log.Event === 1) {
  812. outboundColor = ' style="color: #e04141;"'; //red for blocked
  813. }
  814. else if (log.Event === 2) {
  815. outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
  816. }
  817. let text = ``;
  818. if (log.Email !== "") {
  819. text = `<td>${log.Email}</td>`;
  820. }
  821. formattedLogs += `
  822. <tr ${outboundColor}>
  823. <td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
  824. <td>${log.FromAddress}</td>
  825. <td>${log.ToAddress}</td>
  826. <td>${log.Inbound}</td>
  827. <td>${log.Outbound}</td>
  828. ${text}
  829. </tr>
  830. `;
  831. });
  832. return formattedLogs += "</table>";
  833. },
  834. hide() {
  835. this.visible = false;
  836. },
  837. };
  838. const backupModal = {
  839. visible: false,
  840. show() {
  841. this.visible = true;
  842. },
  843. hide() {
  844. this.visible = false;
  845. },
  846. };
  847. const app = new Vue({
  848. delimiters: ['[[', ']]'],
  849. el: '#app',
  850. mixins: [MediaQueryMixin],
  851. data: {
  852. themeSwitcher,
  853. loadingStates: {
  854. fetched: false,
  855. spinning: false
  856. },
  857. status: new Status(),
  858. cpuHistory: [], // small live widget history
  859. cpuHistoryLong: [], // aggregated points from backend
  860. cpuHistoryLabels: [],
  861. cpuHistoryModal: { visible: false, bucket: 2 },
  862. versionModal,
  863. logModal,
  864. xraylogModal,
  865. backupModal,
  866. loadingTip: '{{ i18n "loading"}}',
  867. showAlert: false,
  868. showIp: false,
  869. ipLimitEnable: false,
  870. },
  871. methods: {
  872. loading(spinning, tip = '{{ i18n "loading"}}') {
  873. this.loadingStates.spinning = spinning;
  874. this.loadingTip = tip;
  875. },
  876. async getStatus() {
  877. try {
  878. const msg = await HttpUtil.get('/panel/api/server/status');
  879. if (msg.success) {
  880. if (!this.loadingStates.fetched) {
  881. this.loadingStates.fetched = true;
  882. }
  883. this.setStatus(msg.obj, true);
  884. }
  885. } catch (e) {
  886. console.error("Failed to get status:", e);
  887. }
  888. },
  889. setStatus(data) {
  890. this.status = new Status(data);
  891. // Push CPU percent into history (clamped 0..100)
  892. const v = Math.max(0, Math.min(100, Number(data?.cpu ?? 0)))
  893. this.cpuHistory.push(v)
  894. const maxPoints = this.isMobile ? 60 : 120
  895. if (this.cpuHistory.length > maxPoints) {
  896. this.cpuHistory.splice(0, this.cpuHistory.length - maxPoints)
  897. }
  898. // If modal open, refresh current bucketed data
  899. if (this.cpuHistoryModal.visible) {
  900. this.fetchCpuHistoryBucket()
  901. }
  902. },
  903. openCpuHistory() {
  904. this.cpuHistoryModal.visible = true
  905. this.fetchCpuHistoryBucket()
  906. },
  907. async fetchCpuHistoryBucket() {
  908. const bucket = this.cpuHistoryModal.bucket || 2
  909. try {
  910. const msg = await HttpUtil.get(`/panel/api/server/cpuHistory/${bucket}`)
  911. if (msg.success && Array.isArray(msg.obj)) {
  912. const vals = []
  913. const labels = []
  914. for (const p of msg.obj) {
  915. const d = new Date(p.t * 1000)
  916. const hh = String(d.getHours()).padStart(2,'0')
  917. const mm = String(d.getMinutes()).padStart(2,'0')
  918. const ss = String(d.getSeconds()).padStart(2,'0')
  919. labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
  920. vals.push(Math.max(0, Math.min(100, p.cpu)))
  921. }
  922. this.cpuHistoryLabels = labels
  923. this.cpuHistoryLong = vals
  924. }
  925. } catch(e) {
  926. console.error('Failed to fetch bucketed cpu history', e)
  927. }
  928. },
  929. async openSelectV2rayVersion() {
  930. this.loading(true);
  931. const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
  932. this.loading(false);
  933. if (!msg.success) {
  934. return;
  935. }
  936. versionModal.show(msg.obj);
  937. },
  938. switchV2rayVersion(version) {
  939. this.$confirm({
  940. title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
  941. content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}'.replace('#version#', version),
  942. okText: '{{ i18n "confirm"}}',
  943. class: themeSwitcher.currentTheme,
  944. cancelText: '{{ i18n "cancel"}}',
  945. onOk: async () => {
  946. versionModal.hide();
  947. this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
  948. await HttpUtil.post(`/panel/api/server/installXray/${version}`);
  949. this.loading(false);
  950. },
  951. });
  952. },
  953. updateGeofile(fileName) {
  954. const isSingleFile = !!fileName;
  955. this.$confirm({
  956. title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
  957. content: isSingleFile
  958. ? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
  959. : '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
  960. okText: '{{ i18n "confirm"}}',
  961. class: themeSwitcher.currentTheme,
  962. cancelText: '{{ i18n "cancel"}}',
  963. onOk: async () => {
  964. versionModal.hide();
  965. this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
  966. const url = isSingleFile
  967. ? `/panel/api/server/updateGeofile/${fileName}`
  968. : `/panel/api/server/updateGeofile`;
  969. await HttpUtil.post(url);
  970. this.loading(false);
  971. },
  972. });
  973. },
  974. async stopXrayService() {
  975. this.loading(true);
  976. const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
  977. this.loading(false);
  978. if (!msg.success) {
  979. return;
  980. }
  981. },
  982. async restartXrayService() {
  983. this.loading(true);
  984. const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
  985. this.loading(false);
  986. if (!msg.success) {
  987. return;
  988. }
  989. },
  990. async openLogs() {
  991. logModal.loading = true;
  992. const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog });
  993. if (!msg.success) {
  994. return;
  995. }
  996. logModal.show(msg.obj);
  997. await PromiseUtil.sleep(500);
  998. logModal.loading = false;
  999. },
  1000. async openXrayLogs() {
  1001. xraylogModal.loading = true;
  1002. const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy });
  1003. if (!msg.success) {
  1004. return;
  1005. }
  1006. xraylogModal.show(msg.obj);
  1007. await PromiseUtil.sleep(500);
  1008. xraylogModal.loading = false;
  1009. },
  1010. downloadXrayLogs() {
  1011. if (!Array.isArray(this.xraylogModal.logs) || this.xraylogModal.logs.length === 0) {
  1012. FileManager.downloadTextFile('', 'x-ui.log');
  1013. return;
  1014. }
  1015. const lines = this.xraylogModal.logs.map(l => {
  1016. try {
  1017. const dt = l.DateTime ? new Date(l.DateTime) : null;
  1018. const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
  1019. const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
  1020. const eventText = eventMap[l.Event] || String(l.Event ?? '');
  1021. const emailPart = l.Email ? ` Email=${l.Email}` : '';
  1022. return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
  1023. } catch (e) {
  1024. return JSON.stringify(l);
  1025. }
  1026. }).join('\n');
  1027. FileManager.downloadTextFile(lines, 'x-ui.log');
  1028. },
  1029. async openConfig() {
  1030. this.loading(true);
  1031. const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
  1032. this.loading(false);
  1033. if (!msg.success) {
  1034. return;
  1035. }
  1036. txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
  1037. },
  1038. openBackup() {
  1039. backupModal.show();
  1040. },
  1041. exportDatabase() {
  1042. window.location = basePath + 'panel/api/server/getDb';
  1043. },
  1044. importDatabase() {
  1045. const fileInput = document.createElement('input');
  1046. fileInput.type = 'file';
  1047. fileInput.accept = '.db';
  1048. fileInput.addEventListener('change', async (event) => {
  1049. const dbFile = event.target.files[0];
  1050. if (dbFile) {
  1051. const formData = new FormData();
  1052. formData.append('db', dbFile);
  1053. backupModal.hide();
  1054. this.loading(true);
  1055. const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
  1056. headers: {
  1057. 'Content-Type': 'multipart/form-data',
  1058. }
  1059. });
  1060. this.loading(false);
  1061. if (!uploadMsg.success) {
  1062. return;
  1063. }
  1064. this.loading(true);
  1065. const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
  1066. this.loading(false);
  1067. if (restartMsg.success) {
  1068. this.loading(true);
  1069. await PromiseUtil.sleep(5000);
  1070. location.reload();
  1071. }
  1072. }
  1073. });
  1074. fileInput.click();
  1075. },
  1076. startPolling() {
  1077. // Fallback polling mechanism
  1078. const pollInterval = setInterval(async () => {
  1079. if (window.wsClient && window.wsClient.isConnected) {
  1080. clearInterval(pollInterval);
  1081. return;
  1082. }
  1083. try {
  1084. await this.getStatus();
  1085. } catch (e) {
  1086. console.error(e);
  1087. }
  1088. }, 2000);
  1089. },
  1090. },
  1091. async mounted() {
  1092. if (window.location.protocol !== "https:") {
  1093. this.showAlert = true;
  1094. }
  1095. const msg = await HttpUtil.post('/panel/setting/defaultSettings');
  1096. if (msg.success) {
  1097. this.ipLimitEnable = msg.obj.ipLimitEnable;
  1098. }
  1099. // Initial status fetch
  1100. await this.getStatus();
  1101. // Setup WebSocket for real-time updates
  1102. if (window.wsClient) {
  1103. window.wsClient.connect();
  1104. // Listen for status updates
  1105. window.wsClient.on('status', (payload) => {
  1106. this.setStatus(payload);
  1107. });
  1108. // Listen for Xray state changes
  1109. window.wsClient.on('xray_state', (payload) => {
  1110. if (this.status && this.status.xray) {
  1111. this.status.xray.state = payload.state;
  1112. this.status.xray.errorMsg = payload.errorMsg || '';
  1113. switch (payload.state) {
  1114. case 'running':
  1115. this.status.xray.color = "green";
  1116. this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}';
  1117. break;
  1118. case 'stop':
  1119. this.status.xray.color = "orange";
  1120. this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}';
  1121. break;
  1122. case 'error':
  1123. this.status.xray.color = "red";
  1124. this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}';
  1125. break;
  1126. }
  1127. }
  1128. });
  1129. // Notifications disabled - white notifications are not needed
  1130. // Fallback to polling if WebSocket fails
  1131. window.wsClient.on('error', () => {
  1132. console.warn('WebSocket connection failed, falling back to polling');
  1133. this.startPolling();
  1134. });
  1135. window.wsClient.on('disconnected', () => {
  1136. if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
  1137. console.warn('WebSocket reconnection failed, falling back to polling');
  1138. this.startPolling();
  1139. }
  1140. });
  1141. } else {
  1142. // Fallback to polling if WebSocket is not available
  1143. this.startPolling();
  1144. }
  1145. },
  1146. });
  1147. </script>
  1148. {{ template "page/body_end" .}}