feat: SAC 0.7.0 display_name columns and event severity overrides

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-01 10:21:43 +10:00
parent 8de5d14cfb
commit de29270a25
25 changed files with 850 additions and 222 deletions
+145 -1
View File
@@ -53,6 +53,47 @@
</form>
</section>
<section class="card settings-card">
<h2>Severity по типу события</h2>
<p class="settings-hint settings-hint-top">
Переопределение значимости для новых событий после ingest. Исходное severity агента сохраняется в
<code>details.severity_agent</code>. Пустое значение как прислал агент (см. «По умолчанию»).
</p>
<div class="settings-severity-toolbar">
<input v-model="severityFilter" type="search" placeholder="Фильтр по type…" />
<button type="button" :disabled="savingSeverity" @click="saveSeverityOverrides">
{{ savingSeverity ? "Сохранение" : "Сохранить severity" }}
</button>
</div>
<div class="settings-severity-table-wrap">
<table class="settings-severity-table">
<thead>
<tr>
<th>Тип события</th>
<th>По умолчанию</th>
<th>Effective</th>
</tr>
</thead>
<tbody>
<tr v-for="row in filteredSeverityRows" :key="row.event_type">
<td><code>{{ row.event_type }}</code></td>
<td>{{ row.default_severity }}</td>
<td>
<select v-model="row.selected">
<option value="">по умолчанию агента</option>
<option value="info">info</option>
<option value="warning">warning</option>
<option value="high">high</option>
<option value="critical">critical</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<p class="settings-meta">Записей: {{ filteredSeverityRows.length }} / {{ severityRows.length }}</p>
</section>
<section class="card settings-card">
<h2>Telegram</h2>
<form class="settings-form" @submit.prevent="saveTelegram">
@@ -250,11 +291,22 @@ interface NotificationPolicy {
source: string;
}
interface SeverityOverrideRowApi {
event_type: string;
default_severity: string;
override_severity: string | null;
}
interface SeverityOverrideRowUi extends SeverityOverrideRowApi {
selected: string;
}
const loading = ref(true);
const savingPolicy = ref(false);
const savingTelegram = ref(false);
const savingWebhook = ref(false);
const savingEmail = ref(false);
const savingSeverity = ref(false);
const testingTelegram = ref(false);
const testingWebhook = ref(false);
const testingEmail = ref(false);
@@ -264,6 +316,8 @@ const telegramLoaded = ref<TelegramSettings | null>(null);
const webhookLoaded = ref<WebhookSettings | null>(null);
const emailLoaded = ref<EmailSettings | null>(null);
const policyLoaded = ref<NotificationPolicy | null>(null);
const severityRows = ref<SeverityOverrideRowUi[]>([]);
const severityFilter = ref("");
const policyForm = reactive({
min_severity: "warning",
@@ -323,6 +377,58 @@ const mailToPlaceholder = computed(() =>
emailLoaded.value?.mail_to_hint ? `оставить ${emailLoaded.value.mail_to_hint}` : "admin@example.com",
);
const filteredSeverityRows = computed(() => {
const q = severityFilter.value.trim().toLowerCase();
if (!q) return severityRows.value;
return severityRows.value.filter((row) => row.event_type.toLowerCase().includes(q));
});
function mapSeverityRows(items: SeverityOverrideRowApi[]): SeverityOverrideRowUi[] {
return items.map((row) => ({
...row,
selected: row.override_severity ?? "",
}));
}
async function loadSeverityOverrides() {
const data = await apiFetch<{ items: SeverityOverrideRowApi[] }>(
"/api/v1/settings/notifications/severity-overrides",
);
severityRows.value = mapSeverityRows(data.items);
}
async function saveSeverityOverrides() {
savingSeverity.value = true;
error.value = "";
success.value = "";
try {
const overrides: Record<string, string | null> = {};
for (const row of severityRows.value) {
const prev = row.override_severity ?? "";
const next = row.selected.trim();
if (next === prev) continue;
overrides[row.event_type] = next || null;
}
if (Object.keys(overrides).length === 0) {
error.value = "Нет изменений severity для сохранения";
return;
}
const data = await apiFetch<{ items: SeverityOverrideRowApi[] }>(
"/api/v1/settings/notifications/severity-overrides",
{
method: "PUT",
body: JSON.stringify({ overrides }),
},
);
severityRows.value = mapSeverityRows(data.items);
success.value = "Severity по типам событий сохранён.";
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка сохранения severity";
} finally {
savingSeverity.value = false;
}
}
async function load() {
loading.value = true;
error.value = "";
@@ -357,6 +463,7 @@ async function load() {
emailForm.mail_to = "";
emailForm.smtp_starttls = data.email.smtp_starttls;
emailForm.smtp_ssl = data.email.smtp_ssl;
await loadSeverityOverrides();
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка загрузки";
} finally {
@@ -522,7 +629,7 @@ onMounted(load);
<style scoped>
.settings-page {
max-width: 42rem;
max-width: 52rem;
}
.settings-intro {
@@ -614,4 +721,41 @@ onMounted(load);
.success {
color: #6dd196;
}
.settings-severity-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
align-items: center;
}
.settings-severity-toolbar input[type="search"] {
flex: 1;
min-width: 12rem;
}
.settings-severity-table-wrap {
max-height: 24rem;
overflow: auto;
border: 1px solid #2a3441;
border-radius: 6px;
}
.settings-severity-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.settings-severity-table th,
.settings-severity-table td {
text-align: left;
padding: 0.4rem 0.5rem;
border-bottom: 1px solid #2a3441;
}
.settings-severity-table select {
min-width: 11rem;
}
</style>