243 lines
11 KiB
HTML
243 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<header class="mb-stack-lg">
|
|
<h1 class="text-headline-xl-mobile font-bold text-on-background md:text-headline-xl">Alerts</h1>
|
|
<p class="mt-1 text-body-base text-on-surface-variant">Filter, triage, acknowledge, resolve, or reopen deterministic DMARC alerts.</p>
|
|
</header>
|
|
|
|
<form class="alerts-filter-bar" method="get" id="alerts-filter-form">
|
|
<label>
|
|
<span class="label-caps">Domain</span>
|
|
<select name="domain">
|
|
<option value="">All domains</option>
|
|
{% for item in domains %}
|
|
<option value="{{ item }}" {{ "selected" if item == selected_domain else "" }}>{{ item }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span class="label-caps">Type</span>
|
|
<select name="alert_type">
|
|
<option value="">All types</option>
|
|
{% for item in alert_types %}
|
|
<option value="{{ item }}" {{ "selected" if item == selected_type else "" }}>{{ item }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span class="label-caps">Severity</span>
|
|
<select name="severity">
|
|
<option value="">All severities</option>
|
|
{% for item in severities %}
|
|
<option value="{{ item }}" {{ "selected" if item == selected_severity else "" }}>{{ item }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span class="label-caps">State</span>
|
|
<select name="status">
|
|
{% for item in ["open", "acknowledged", "resolved", ""] %}
|
|
<option value="{{ item }}" {{ "selected" if item == selected_status else "" }}>{{ item or "all states" }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
<span class="label-caps">Report From</span>
|
|
<input type="date" name="date_from" value="{{ selected_date_from }}">
|
|
</label>
|
|
<label>
|
|
<span class="label-caps">Report To</span>
|
|
<input type="date" name="date_to" value="{{ selected_date_to }}">
|
|
</label>
|
|
</form>
|
|
|
|
<section class="alerts-bulk-bar">
|
|
<div>
|
|
<strong id="alerts-selected-count">0 selected</strong>
|
|
<span class="dw-muted">{{ total }} alerts match current filters</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-stack-sm">
|
|
<button class="button-secondary js-bulk-alert-action" type="button" data-status="acknowledged" disabled>
|
|
<span class="material-symbols-outlined text-[18px]">done</span>
|
|
Acknowledge
|
|
</button>
|
|
<button class="button-secondary js-bulk-alert-action" type="button" data-status="resolved" disabled>
|
|
<span class="material-symbols-outlined text-[18px]">task_alt</span>
|
|
Resolve
|
|
</button>
|
|
<button class="button-secondary js-bulk-alert-action" type="button" data-status="open" disabled>
|
|
<span class="material-symbols-outlined text-[18px]">restart_alt</span>
|
|
Reopen
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="surface-card overflow-hidden">
|
|
{% for alert in alerts %}
|
|
{% set is_critical = alert.severity == "critical" %}
|
|
<article class="alert-row border-b border-outline-variant p-stack-md last:border-b-0 is-{{ alert.severity_class }}" data-alert-id="{{ alert.id }}" data-status="{{ alert.status }}">
|
|
<div class="flex flex-col items-start justify-between gap-stack-md xl:flex-row">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="mb-1 flex flex-wrap items-center gap-stack-sm">
|
|
<label class="alert-select">
|
|
<input class="js-alert-checkbox" type="checkbox" value="{{ alert.id }}">
|
|
<span></span>
|
|
</label>
|
|
<span class="status-chip chip-{{ alert.severity_class }}">{{ alert.severity }}</span>
|
|
<span class="status-chip js-alert-status {{ 'chip-pass' if alert.status == 'resolved' else ('chip-warning' if alert.status == 'acknowledged' else 'chip-fail') }}">{{ alert.status }}</span>
|
|
<span class="label-caps">• {{ alert.type }}</span>
|
|
</div>
|
|
<h2 class="text-headline-md font-semibold text-on-surface">{{ alert.title }}</h2>
|
|
<div class="mt-stack-sm flex flex-wrap items-center gap-stack-sm text-body-sm text-on-surface-variant">
|
|
<code class="rounded bg-surface-container px-2 py-0.5 font-mono text-data-mono text-secondary">{{ alert.domain }}</code>
|
|
<span>Report period: {{ alert.report_start | fmt_dt }}{% if alert.report_end and alert.report_end != alert.report_start %} to {{ alert.report_end | fmt_dt }}{% endif %}</span>
|
|
{% if alert.source_history %}
|
|
<span>{{ alert.source_history }}</span>
|
|
{% endif %}
|
|
{% if alert.report_db_id %}
|
|
<a class="dw-inline-link" href="/reports/{{ alert.report_db_id }}">Source report</a>
|
|
{% endif %}
|
|
</div>
|
|
{% if alert.published_policy_label or alert.receiver_action_label %}
|
|
<div class="mt-stack-sm flex flex-wrap items-center gap-stack-sm">
|
|
{% if alert.published_policy_label %}
|
|
<span class="status-chip chip-info">published {{ alert.published_policy_label }}</span>
|
|
{% endif %}
|
|
{% if alert.receiver_action_label %}
|
|
<span class="status-chip {{ 'chip-fail' if alert.receiver_action_label == 'receiver reject' else ('chip-warning' if alert.receiver_action_label == 'receiver quarantine' else 'chip-info') }}">{{ alert.receiver_action_label }}</span>
|
|
{% endif %}
|
|
{% if alert.policy_override_label %}
|
|
<span class="status-chip chip-warning">{{ alert.policy_override_label }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
<p class="mt-stack-md text-body-base text-on-surface-variant">{{ alert.llm_summary or alert.summary }}</p>
|
|
{% if alert.llm_recommended_action %}
|
|
<p class="mt-stack-sm text-body-sm italic text-secondary">{{ alert.llm_recommended_action }}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="alert-actions flex shrink-0 flex-wrap gap-stack-sm">
|
|
<button class="button-secondary js-alert-action" type="button" data-status="acknowledged" {{ "disabled" if alert.status == "acknowledged" else "" }}>
|
|
<span class="material-symbols-outlined text-[18px]">done</span>
|
|
Acknowledge
|
|
</button>
|
|
<button class="button-secondary js-alert-action" type="button" data-status="resolved" {{ "disabled" if alert.status == "resolved" else "" }}>
|
|
<span class="material-symbols-outlined text-[18px]">task_alt</span>
|
|
Resolve
|
|
</button>
|
|
<button class="button-secondary js-alert-action" type="button" data-status="open" {{ "disabled" if alert.status == "open" else "" }}>
|
|
<span class="material-symbols-outlined text-[18px]">restart_alt</span>
|
|
Reopen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
{% else %}
|
|
<div class="p-gutter text-on-surface-variant">No alerts match these filters.</div>
|
|
{% endfor %}
|
|
</section>
|
|
|
|
<div class="dw-table-footer alerts-pager">
|
|
<span>{{ ((page - 1) * page_size) + 1 if total else 0 }}-{{ [page * page_size, total] | min }} of {{ total }}</span>
|
|
<span class="dw-pager">
|
|
<a class="{{ 'is-disabled' if page <= 1 else '' }}" href="/alerts?page={{ page - 1 }}&domain={{ selected_domain }}&alert_type={{ selected_type }}&severity={{ selected_severity }}&status={{ selected_status }}&date_from={{ selected_date_from }}&date_to={{ selected_date_to }}"><span class="material-symbols-outlined">chevron_left</span></a>
|
|
<a class="{{ 'is-disabled' if page * page_size >= total else '' }}" href="/alerts?page={{ page + 1 }}&domain={{ selected_domain }}&alert_type={{ selected_type }}&severity={{ selected_severity }}&status={{ selected_status }}&date_from={{ selected_date_from }}&date_to={{ selected_date_to }}"><span class="material-symbols-outlined">chevron_right</span></a>
|
|
</span>
|
|
</div>
|
|
|
|
<script>
|
|
(() => {
|
|
const rows = Array.from(document.querySelectorAll(".alert-row"));
|
|
const boxes = Array.from(document.querySelectorAll(".js-alert-checkbox"));
|
|
const selectedCount = document.getElementById("alerts-selected-count");
|
|
const bulkButtons = Array.from(document.querySelectorAll(".js-bulk-alert-action"));
|
|
const filterForm = document.getElementById("alerts-filter-form");
|
|
let lastChecked = null;
|
|
|
|
filterForm.querySelectorAll("select,input").forEach((control) => {
|
|
control.addEventListener("change", () => {
|
|
const page = filterForm.querySelector("input[name='page']");
|
|
if (page) page.value = "1";
|
|
filterForm.requestSubmit();
|
|
});
|
|
});
|
|
|
|
const selectedIds = () => boxes.filter((box) => box.checked).map((box) => Number(box.value));
|
|
const refreshBulk = () => {
|
|
const count = selectedIds().length;
|
|
selectedCount.textContent = `${count} selected`;
|
|
bulkButtons.forEach((button) => { button.disabled = count === 0; });
|
|
};
|
|
|
|
boxes.forEach((box, index) => {
|
|
box.addEventListener("click", (event) => {
|
|
if ((event.shiftKey || (event.metaKey && event.shiftKey)) && lastChecked !== null) {
|
|
const start = Math.min(lastChecked, index);
|
|
const end = Math.max(lastChecked, index);
|
|
boxes.slice(start, end + 1).forEach((item) => { item.checked = box.checked; });
|
|
}
|
|
lastChecked = index;
|
|
refreshBulk();
|
|
});
|
|
});
|
|
|
|
rows.forEach((row) => {
|
|
row.addEventListener("click", (event) => {
|
|
if (event.target.closest("button,a,label,input")) return;
|
|
const box = row.querySelector(".js-alert-checkbox");
|
|
box.checked = !box.checked;
|
|
lastChecked = boxes.indexOf(box);
|
|
refreshBulk();
|
|
});
|
|
});
|
|
|
|
const applyStatus = (row, status) => {
|
|
row.dataset.status = status;
|
|
const chip = row.querySelector(".js-alert-status");
|
|
chip.textContent = status;
|
|
chip.className = `status-chip js-alert-status ${status === "resolved" ? "chip-pass" : (status === "acknowledged" ? "chip-warning" : "chip-fail")}`;
|
|
row.querySelectorAll(".js-alert-action").forEach((button) => {
|
|
button.disabled = button.dataset.status === status;
|
|
});
|
|
};
|
|
|
|
const postStatus = async (id, status) => {
|
|
const endpoint = status === "open" ? `/api/alerts/${id}/reopen` : `/api/alerts/${id}/${status === "resolved" ? "resolve" : "ack"}`;
|
|
const response = await fetch(endpoint, { method: "POST", headers: window.adminPostHeaders, credentials: "same-origin" });
|
|
if (!response.ok) throw new Error("Alert update failed.");
|
|
return response.json();
|
|
};
|
|
|
|
document.querySelectorAll(".js-alert-action").forEach((button) => {
|
|
button.addEventListener("click", async () => {
|
|
const row = button.closest(".alert-row");
|
|
button.disabled = true;
|
|
try {
|
|
await postStatus(row.dataset.alertId, button.dataset.status);
|
|
applyStatus(row, button.dataset.status);
|
|
} catch (error) {
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
bulkButtons.forEach((button) => {
|
|
button.addEventListener("click", async () => {
|
|
const ids = selectedIds();
|
|
if (!ids.length) return;
|
|
const response = await fetch("/api/alerts/bulk", {
|
|
method: "POST",
|
|
headers: { ...window.adminPostHeaders, "Content-Type": "application/json" },
|
|
credentials: "same-origin",
|
|
body: JSON.stringify({ ids, status: button.dataset.status }),
|
|
});
|
|
if (!response.ok) return;
|
|
rows.filter((row) => ids.includes(Number(row.dataset.alertId))).forEach((row) => applyStatus(row, button.dataset.status));
|
|
boxes.forEach((box) => { box.checked = false; });
|
|
refreshBulk();
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|