Initial commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
{% 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>
|
||||
<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 %}
|
||||
Reference in New Issue
Block a user