Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-actions mb-2">
|
||||
<el-button size="small" @click="loadDashboard">Refresh</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="loadError"
|
||||
:title="loadError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
class="mb-2"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="isStale"
|
||||
title="Dashboard data is stale. Retrying automatically..."
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-alert
|
||||
v-for="risk in securityRisks"
|
||||
:key="risk.key"
|
||||
:title="risk.key"
|
||||
:type="risk.severity === 'critical' ? 'error' : risk.severity === 'warning' ? 'warning' : 'info'"
|
||||
:closable="risk.dismissible"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<el-row :gutter="10" class="mt-2">
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">CPU: {{ summary.system.cpuPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">RAM: {{ summary.system.memPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Disk: {{ summary.system.diskPercent }}%</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Peers: {{ summary.peers.total }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Online peers: {{ summary.peers.onlinePeers }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Online devices: {{ summary.peers.onlineDevices }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Today download: {{ formatBytes(summary.traffic.todayDownloadBytes || 0) }}</el-card></el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="6"><el-card shadow="never">Today upload: {{ formatBytes(summary.traffic.todayUploadBytes || 0) }}</el-card></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="mt-3">
|
||||
<template #header>Top peers (24h)</template>
|
||||
<el-table :data="topPeers" size="small">
|
||||
<el-table-column prop="name" label="Peer" />
|
||||
<el-table-column prop="download" label="Download">
|
||||
<template #default="scope">{{ formatBytes(scope.row.download || 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="upload" label="Upload">
|
||||
<template #default="scope">{{ formatBytes(scope.row.upload || 0) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total" label="Total">
|
||||
<template #default="scope">{{ formatBytes(scope.row.total || 0) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntervalFn } from "@vueuse/core";
|
||||
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTopPeersApi } from "@/api/dashboard";
|
||||
import { DashboardSummaryVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
|
||||
import { formatBytes } from "@/utils/byte";
|
||||
|
||||
const summary = ref<DashboardSummaryVo>({
|
||||
collectedAt: 0,
|
||||
system: { cpuPercent: 0, memUsedBytes: 0, memTotalBytes: 0, memPercent: 0, diskUsedBytes: 0, diskTotalBytes: 0, diskPercent: 0 },
|
||||
hysteria: { version: "-", running: false, apiReachable: false },
|
||||
peers: { total: 0, enabled: 0, disabled: 0, expired: 0, onlinePeers: 0, onlineDevices: 0 },
|
||||
traffic: { downloadBytes: 0, uploadBytes: 0, totalBytes: 0, todayDownloadBytes: 0, todayUploadBytes: 0, sinceResetDownloadBytes: 0, sinceResetUploadBytes: 0 },
|
||||
health: {
|
||||
collector: { status: "stale" },
|
||||
hysteria: { status: "ok" },
|
||||
},
|
||||
securityRisks: [],
|
||||
});
|
||||
const topPeers = ref<DashboardTopPeerVo[]>([]);
|
||||
const securityRisks = ref<SecurityRiskVo[]>([]);
|
||||
const loadError = ref("");
|
||||
const loading = ref(false);
|
||||
const lastSuccessAt = ref(0);
|
||||
|
||||
const staleThresholdMs = 90_000;
|
||||
const pollIntervalMs = 30_000;
|
||||
|
||||
const isStale = computed(() => {
|
||||
if (!lastSuccessAt.value) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - lastSuccessAt.value > staleThresholdMs;
|
||||
});
|
||||
|
||||
const loadDashboard = async () => {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const [summaryRes, topRes, secRes] = await Promise.all([
|
||||
dashboardSummaryApi(),
|
||||
dashboardTopPeersApi("24h", 10),
|
||||
dashboardSecurityApi(),
|
||||
]);
|
||||
summary.value = summaryRes.data;
|
||||
topPeers.value = topRes.data;
|
||||
securityRisks.value = secRes.data;
|
||||
lastSuccessAt.value = Date.now();
|
||||
loadError.value = "";
|
||||
} catch (error) {
|
||||
loadError.value = "Failed to refresh dashboard data";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
|
||||
() => {
|
||||
loadDashboard();
|
||||
},
|
||||
pollIntervalMs,
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
loadDashboard();
|
||||
startPolling();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDashboard();
|
||||
startPolling();
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user