feat: unify SSH and Windows daily report layout in SAC and UI

Agent-style report body for SAC aggregation; shared stats cards; RDP ban note in docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 17:03:29 +10:00
parent 11195ae64f
commit 7ebbb9a9d1
8 changed files with 469 additions and 105 deletions
+12 -35
View File
@@ -1,12 +1,16 @@
<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 v-if="statEntries.length" class="report-stats">
<div v-for="item in statEntries" :key="item.key" class="report-stat">
<div class="report-stat-value">{{ item.value }}</div>
<div class="report-stat-label">{{ item.label }}</div>
</div>
</div>
<ul v-if="activeUserLines.length && !plainBody && !htmlContent" class="report-active-users">
<li v-for="(line, idx) in activeUserLines" :key="idx">{{ line }}</li>
</ul>
<div v-if="htmlContent" class="report-body-html" v-html="htmlContent" />
<pre v-else-if="plainBody" class="report-body-plain">{{ plainBody }}</pre>
@@ -14,7 +18,7 @@
<p v-else-if="summary && summary !== 'Отчёт за сутки'" class="report-fallback">{{ summary }}</p>
<p v-else class="report-empty">
Полный текст отчёта не сохранён (старая версия агента). Обновите ssh-monitor и дождитесь следующего
Полный текст отчёта не сохранён (старая версия агента). Обновите агент и дождитесь следующего
ежедневного отчёта.
</p>
</div>
@@ -23,11 +27,12 @@
<script setup lang="ts">
import { computed } from "vue";
import {
reportActiveUserLines,
reportBodyFromDetails,
reportHtmlFromDetails,
reportStatEntries,
reportStatsFromDetails,
sanitizeAgentHtml,
type DailyReportStats,
} from "../utils/reportDisplay";
const props = defineProps<{
@@ -36,37 +41,9 @@ const props = defineProps<{
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 stats = computed(() => reportStatsFromDetails(props.details, props.type));
const statEntries = computed(() => reportStatEntries(stats.value, props.type));
const activeUserLines = computed(() => reportActiveUserLines(stats.value, props.type));
const htmlContent = computed(() => {
const raw = reportHtmlFromDetails(props.details);