diff --git a/frontend/src/composables/useStatus.js b/frontend/src/composables/useStatus.js new file mode 100644 index 00000000..9098e848 --- /dev/null +++ b/frontend/src/composables/useStatus.js @@ -0,0 +1,43 @@ +import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'; + +import { HttpUtil } from '@/utils'; +import { Status } from '@/models/status.js'; + +const POLL_INTERVAL_MS = 2000; + +// Polls /panel/api/server/status and exposes a reactive Status object +// + a `fetched` flag so consumers can show a spinner before the first +// successful fetch. +// +// WebSocket integration is intentionally deferred to a later sub-phase. +// Polling at 2s is the same fallback the legacy panel falls back to +// when its websocket link drops, so we're shipping the proven path +// first and adding the websocket on top later. +export function useStatus() { + const status = shallowRef(new Status()); + const fetched = ref(false); + let timer = null; + + async function refresh() { + try { + const msg = await HttpUtil.get('/panel/api/server/status'); + if (msg?.success) { + status.value = new Status(msg.obj); + if (!fetched.value) fetched.value = true; + } + } catch (e) { + console.error('Failed to get status:', e); + } + } + + onMounted(() => { + refresh(); + timer = window.setInterval(refresh, POLL_INTERVAL_MS); + }); + + onBeforeUnmount(() => { + if (timer != null) window.clearInterval(timer); + }); + + return { status, fetched, refresh }; +} diff --git a/frontend/src/models/status.js b/frontend/src/models/status.js new file mode 100644 index 00000000..5d2c671f --- /dev/null +++ b/frontend/src/models/status.js @@ -0,0 +1,76 @@ +import { NumberFormatter } from '@/utils'; + +export class CurTotal { + constructor(current, total) { + this.current = current; + this.total = total; + } + + get percent() { + if (this.total === 0) return 0; + return NumberFormatter.toFixed((this.current / this.total) * 100, 2); + } + + get color() { + const p = this.percent; + if (p < 80) return '#008771'; // green + if (p < 90) return '#f37b24'; // orange + return '#cf3c3c'; // red + } +} + +const XRAY_STATE_COLORS = { + running: 'green', + stop: 'orange', + error: 'red', +}; + +const XRAY_STATE_MESSAGES = { + running: 'Xray is running', + stop: 'Xray is stopped', + error: 'Xray error', +}; + +export 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 }; + this.xray = { state: 'stop', stateMsg: '', errorMsg: '', version: '', color: '' }; + + 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 ?? 0, data.disk?.total ?? 0); + this.loads = (data.loads || [0, 0, 0]).map((v) => NumberFormatter.toFixed(v, 2)); + this.mem = new CurTotal(data.mem?.current ?? 0, data.mem?.total ?? 0); + this.netIO = data.netIO ?? this.netIO; + this.netTraffic = data.netTraffic ?? this.netTraffic; + this.publicIP = data.publicIP ?? this.publicIP; + this.swap = new CurTotal(data.swap?.current ?? 0, data.swap?.total ?? 0); + this.tcpCount = data.tcpCount ?? 0; + this.udpCount = data.udpCount ?? 0; + this.uptime = data.uptime ?? 0; + this.appUptime = data.appUptime ?? 0; + this.appStats = data.appStats ?? this.appStats; + this.xray = { ...this.xray, ...(data.xray || {}) }; + this.xray.color = XRAY_STATE_COLORS[this.xray.state] ?? 'gray'; + this.xray.stateMsg = XRAY_STATE_MESSAGES[this.xray.state] ?? 'Unknown'; + } +} diff --git a/frontend/src/pages/index/IndexPage.vue b/frontend/src/pages/index/IndexPage.vue index bbbb0e06..f44b965c 100644 --- a/frontend/src/pages/index/IndexPage.vue +++ b/frontend/src/pages/index/IndexPage.vue @@ -1,26 +1,30 @@