Files

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 %}