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:
@@ -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);
|
||||
|
||||
@@ -308,6 +308,19 @@ pre {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.report-active-users {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.report-active-users li {
|
||||
padding: 0.2rem 0;
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.report-body-html {
|
||||
line-height: 1.55;
|
||||
font-size: 0.95rem;
|
||||
|
||||
@@ -16,24 +16,121 @@ export function sanitizeAgentHtml(html: string): string {
|
||||
}
|
||||
|
||||
export interface DailyReportStats {
|
||||
successful_ssh?: number;
|
||||
failed_ssh?: number;
|
||||
platform?: "ssh" | "windows";
|
||||
successful_logins?: number;
|
||||
failed_logins?: number;
|
||||
sudo_commands?: number;
|
||||
active_bans?: number;
|
||||
top_failed_ips?: string[];
|
||||
active_users?: string[];
|
||||
/** legacy */
|
||||
successful_ssh?: number;
|
||||
failed_ssh?: number;
|
||||
rdp_success?: number;
|
||||
rdp_failed?: number;
|
||||
unique_users?: string[];
|
||||
active_sessions?: number;
|
||||
active_sessions_rdp?: number;
|
||||
unique_users?: string[];
|
||||
}
|
||||
|
||||
export interface NormalizedReportStat {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export function isDailyReportType(type: string): boolean {
|
||||
return type === "report.daily.ssh" || type === "report.daily.rdp";
|
||||
}
|
||||
|
||||
export function reportStatsFromDetails(details: Record<string, unknown> | null): DailyReportStats | null {
|
||||
export function reportPlatform(type: string): "ssh" | "windows" {
|
||||
return type === "report.daily.rdp" ? "windows" : "ssh";
|
||||
}
|
||||
|
||||
/** Приводит stats агента/SAC к единым полям для карточки. */
|
||||
export function normalizeReportStats(
|
||||
stats: DailyReportStats | null,
|
||||
type: string,
|
||||
): DailyReportStats | null {
|
||||
if (!stats) return null;
|
||||
const platform = stats.platform ?? reportPlatform(type);
|
||||
const out: DailyReportStats = { ...stats, platform };
|
||||
|
||||
if (platform === "ssh") {
|
||||
out.successful_logins = stats.successful_logins ?? stats.successful_ssh ?? 0;
|
||||
out.failed_logins = stats.failed_logins ?? stats.failed_ssh ?? 0;
|
||||
out.sudo_commands = stats.sudo_commands ?? 0;
|
||||
} else {
|
||||
out.successful_logins = stats.successful_logins ?? stats.rdp_success ?? 0;
|
||||
out.failed_logins = stats.failed_logins ?? stats.rdp_failed ?? 0;
|
||||
if (!out.active_users?.length && stats.unique_users?.length) {
|
||||
out.active_users = stats.unique_users.map((u) =>
|
||||
u.startsWith("👤") ? u : `👤 ${u}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
out.active_bans = stats.active_bans ?? 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function reportStatsFromDetails(
|
||||
details: Record<string, unknown> | null,
|
||||
type = "report.daily.ssh",
|
||||
): DailyReportStats | null {
|
||||
if (!details || typeof details.stats !== "object" || details.stats === null) {
|
||||
return null;
|
||||
}
|
||||
return details.stats as DailyReportStats;
|
||||
return normalizeReportStats(details.stats as DailyReportStats, type);
|
||||
}
|
||||
|
||||
export function reportStatEntries(
|
||||
stats: DailyReportStats | null,
|
||||
type: string,
|
||||
): NormalizedReportStat[] {
|
||||
const s = normalizeReportStats(stats, type);
|
||||
if (!s) return [];
|
||||
const platform = s.platform ?? reportPlatform(type);
|
||||
const entries: NormalizedReportStat[] = [
|
||||
{
|
||||
key: "successful",
|
||||
label: platform === "ssh" ? "Успешных SSH" : "Успешных RDP",
|
||||
value: s.successful_logins ?? 0,
|
||||
},
|
||||
{
|
||||
key: "failed",
|
||||
label: platform === "ssh" ? "Неудачных SSH" : "Неудачных RDP",
|
||||
value: s.failed_logins ?? 0,
|
||||
},
|
||||
{ key: "bans", label: "Активных банов", value: s.active_bans ?? 0 },
|
||||
];
|
||||
if (platform === "ssh") {
|
||||
entries.splice(2, 0, {
|
||||
key: "sudo",
|
||||
label: "Команд sudo",
|
||||
value: s.sudo_commands ?? 0,
|
||||
});
|
||||
}
|
||||
const users = s.active_users ?? [];
|
||||
if (users.length) {
|
||||
entries.push({
|
||||
key: "active_users",
|
||||
label: "Активные пользователи",
|
||||
value: users.length,
|
||||
});
|
||||
} else if (typeof s.active_sessions === "number" && s.active_sessions > 0) {
|
||||
entries.push({
|
||||
key: "active_sessions",
|
||||
label: "Сессий (SSH)",
|
||||
value: s.active_sessions,
|
||||
});
|
||||
} else if (typeof s.active_sessions_rdp === "number" && s.active_sessions_rdp > 0) {
|
||||
entries.push({
|
||||
key: "active_sessions_rdp",
|
||||
label: "Сессий (RDP)",
|
||||
value: s.active_sessions_rdp,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function reportHtmlFromDetails(details: Record<string, unknown> | null): string | null {
|
||||
@@ -47,3 +144,12 @@ export function reportBodyFromDetails(details: Record<string, unknown> | null):
|
||||
const body = details.report_body;
|
||||
return typeof body === "string" && body.trim() ? body : null;
|
||||
}
|
||||
|
||||
export function reportActiveUserLines(
|
||||
stats: DailyReportStats | null,
|
||||
type: string,
|
||||
): string[] {
|
||||
const s = normalizeReportStats(stats, type);
|
||||
if (!s?.active_users?.length) return [];
|
||||
return s.active_users;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user