feat: reports page with formatted daily report cards

This commit is contained in:
PTah
2026-05-27 14:15:15 +10:00
parent 04301ab359
commit 61c9f9b673
10 changed files with 453 additions and 49 deletions
@@ -0,0 +1,77 @@
<template>
<div class="report-card">
<div v-if="statsEntries.length" class="report-stats">
<div v-for="item in statsEntries" :key="item.key" class="report-stat">
<div class="report-stat-value">{{ item.value }}</div>
<div class="report-stat-label">{{ item.label }}</div>
</div>
</div>
<div v-if="htmlContent" class="report-body-html" v-html="htmlContent" />
<pre v-else-if="plainBody" class="report-body-plain">{{ plainBody }}</pre>
<p v-else-if="summary && summary !== 'Отчёт за сутки'" class="report-fallback">{{ summary }}</p>
<p v-else class="report-empty">
Полный текст отчёта не сохранён (старая версия агента). Обновите ssh-monitor и дождитесь следующего
ежедневного отчёта.
</p>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import {
reportBodyFromDetails,
reportHtmlFromDetails,
reportStatsFromDetails,
sanitizeAgentHtml,
type DailyReportStats,
} from "../utils/reportDisplay";
const props = defineProps<{
type: string;
summary: string;
details: Record<string, unknown> | null;
}>();
const stats = computed(() => reportStatsFromDetails(props.details));
const statLabels: Record<keyof DailyReportStats, string> = {
successful_ssh: "Успешных SSH",
failed_ssh: "Неудачных SSH",
sudo_commands: "Sudo",
active_bans: "Активных банов",
active_sessions: "Сессий (SSH)",
active_sessions_rdp: "Сессий (RDP)",
};
const statsEntries = computed(() => {
const s = stats.value;
if (!s) return [];
const out: { key: string; label: string; value: string | number }[] = [];
for (const [key, label] of Object.entries(statLabels)) {
const k = key as keyof DailyReportStats;
const v = s[k];
if (typeof v === "number") {
out.push({ key, label, value: v });
}
}
if (Array.isArray(s.unique_users) && s.unique_users.length) {
out.push({
key: "unique_users",
label: "Уникальные логины",
value: s.unique_users.length,
});
}
return out;
});
const htmlContent = computed(() => {
const raw = reportHtmlFromDetails(props.details);
return raw ? sanitizeAgentHtml(raw) : null;
});
const plainBody = computed(() => reportBodyFromDetails(props.details));
</script>