Initial commit

This commit is contained in:
2026-05-16 12:05:36 -03:00
parent 0ce972a361
commit e82cee97a7
65 changed files with 9051 additions and 5 deletions
+229
View File
@@ -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 %}
+50
View File
@@ -0,0 +1,50 @@
<!doctype html>
<html class="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ request.app.title }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@450&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<script>
window.adminPostHeaders = { "X-Requested-With": "XMLHttpRequest" };
</script>
<link rel="stylesheet" href="/static/app.css?v=9">
</head>
<body>
<header class="dw-topbar">
<div class="dw-topbar-inner">
<div class="dw-brand-row">
<a class="dw-brand" href="/">DMARC Sentinel</a>
{% set path = request.url.path %}
<nav class="dw-nav">
<a class="dw-nav-link {{ 'is-active' if path == '/' else '' }}" href="/">Overview</a>
<a class="dw-nav-link {{ 'is-active' if path.startswith('/alerts') else '' }}" href="/alerts">Alerts</a>
<a class="dw-nav-link {{ 'is-active' if path.startswith('/inboxes') else '' }}" href="/inboxes">Inboxes</a>
<a class="dw-nav-link {{ 'is-active' if path.startswith('/settings') else '' }}" href="/settings">Settings</a>
</nav>
</div>
<nav class="dw-top-actions dw-mobile-actions">
<a class="dw-icon-button" href="/settings" aria-label="Settings">
<span class="material-symbols-outlined">settings</span>
</a>
</nav>
</div>
</header>
<main class="dw-main">
{% block content %}{% endblock %}
</main>
<footer class="dw-footer">
<div class="dw-footer-inner">
<div class="dw-system-status">
<span class="dw-status-dot"></span>
<span>System Operational</span>
</div>
<div class="dw-footer-code">DMARC Sentinel</div>
</div>
</footer>
</body>
</html>
+230
View File
@@ -0,0 +1,230 @@
{% extends "base.html" %}
{% block content %}
<header class="dw-page-header">
<h1>{{ domain }}</h1>
<p>Domain telemetry and alert evidence.</p>
</header>
<section class="dw-metrics-grid" aria-label="Domain metrics">
<article class="dw-metric-card">
<span class="dw-kicker">Messages</span>
<strong>{{ metrics.messages }}</strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">SPF Aligned</span>
<strong>{{ metrics.spf_aligned }} <small>{{ metrics.spf_rate }}</small></strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">DKIM Aligned</span>
<strong>{{ metrics.dkim_aligned }} <small>{{ metrics.dkim_rate }}</small></strong>
</article>
<article class="dw-metric-card dw-metric-card-critical">
<span class="dw-kicker">Unknown Sources</span>
<strong class="dw-danger-value">{{ metrics.unknown_sources }}</strong>
</article>
</section>
<section class="dw-domain-summary-section">
<h2 class="dw-sidebar-kicker">Latest LLM Posture Summary</h2>
<article class="dw-summary-card dw-domain-summary-card">
<div class="dw-summary-rail"></div>
<div class="dw-summary-copy">
{% set summary_parts = summary.split("Actions:", 1) %}
<div>{{ summary_parts[0] }}</div>
{% if summary_parts | length > 1 %}
<div class="dw-recommendations">
<span>Recommended Actions</span>
<ul>
{% for action in summary_parts[1].replace(".", "").split(";") %}
{% if action.strip() %}
<li><span class="material-symbols-outlined">task_alt</span>{{ action.strip() }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</article>
</section>
<section class="dw-domain-main-grid">
<div class="dw-domain-main-column" id="source-panel">
<h2 class="dw-panel-title">Top Observed IPs</h2>
<div class="dw-table-card">
<table class="dw-table">
<thead>
<tr>
<th title="DMARC aggregate source_ip: the IP observed by the reporting receiver. It may be a relay, forwarder, gateway, or direct sender.">Observed IP</th>
<th>Count</th>
<th>DKIM Domains</th>
<th>Known</th>
<th>DMARC</th>
</tr>
</thead>
<tbody>
{% for row in records %}
<tr>
<td><code>{{ row.source_ip }}</code></td>
<td>{{ row.count }}</td>
<td class="dw-muted">{{ row.dkim_domains }}</td>
<td class="dw-muted">{{ row.known_sender_name or "unknown" }}</td>
<td>
<span class="dw-chip {{ 'dw-chip-pass' if row.dmarc_pass else 'dw-chip-fail' }}">{{ "pass" if row.dmarc_pass else "fail" }}</span>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="dw-muted">No observed IP records yet.</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="5">
<div class="dw-table-footer">
<span>Showing {{ records | length }} observed IPs</span>
<span class="dw-pager">
<a hx-get="/domains/{{ domain }}?source_page={{ source_page - 1 }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page }}" hx-select="#source-panel" hx-target="#source-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if source_page <= 1 else '' }}" href="/domains/{{ domain }}?source_page={{ source_page - 1 }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page }}"><span class="material-symbols-outlined">chevron_left</span></a>
<a hx-get="/domains/{{ domain }}?source_page={{ source_page + 1 }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page }}" hx-select="#source-panel" hx-target="#source-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if source_page * source_page_size >= source_total else '' }}" href="/domains/{{ domain }}?source_page={{ source_page + 1 }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page }}"><span class="material-symbols-outlined">chevron_right</span></a>
</span>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<aside class="dw-domain-alert-column" id="domain-alert-panel">
<h2 class="dw-panel-title">Open Alerts <span class="dw-muted">({{ alert_total }})</span></h2>
<div class="dw-alert-feed">
{% for alert in alerts %}
<a class="dw-alert-item is-{{ alert.severity_class }}" href="{{ '/reports/' ~ alert.report_db_id if alert.report_db_id else '/alerts?domain=' ~ domain }}">
<span class="dw-alert-row">
<span>{{ alert.severity }}</span>
<time>{{ (alert.report_end or alert.report_start or alert.report_time) | fmt_dt }}</time>
</span>
<strong>{{ alert.title }}</strong>
<p>{{ alert.llm_summary or alert.summary }}</p>
</a>
{% else %}
<div class="dw-alert-empty">No open alerts.</div>
{% endfor %}
</div>
{% if alert_total > alert_page_size %}
<div class="dw-table-footer">
<span>{{ ((alert_page - 1) * alert_page_size) + 1 }}-{{ [alert_page * alert_page_size, alert_total] | min }} of {{ alert_total }}</span>
<span class="dw-pager">
<a hx-get="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page - 1 }}&report_page={{ report_page }}&trend_page={{ trend_page }}" hx-select="#domain-alert-panel" hx-target="#domain-alert-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if alert_page <= 1 else '' }}" href="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page - 1 }}&report_page={{ report_page }}&trend_page={{ trend_page }}"><span class="material-symbols-outlined">chevron_left</span></a>
<a hx-get="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page + 1 }}&report_page={{ report_page }}&trend_page={{ trend_page }}" hx-select="#domain-alert-panel" hx-target="#domain-alert-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if alert_page * alert_page_size >= alert_total else '' }}" href="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page + 1 }}&report_page={{ report_page }}&trend_page={{ trend_page }}"><span class="material-symbols-outlined">chevron_right</span></a>
</span>
</div>
{% endif %}
</aside>
</section>
<section class="dw-domain-lower-grid">
<div id="trend-panel">
<h2 class="dw-panel-title">Daily DMARC and Volume Trend</h2>
<div class="dw-table-card">
<table class="dw-table dw-compact-table">
<thead>
<tr>
<th>Date</th>
<th>Messages</th>
<th>Pass</th>
<th>Fail</th>
</tr>
</thead>
<tbody>
{% for stat in stats %}
<tr>
<td>{{ stat.date | fmt_date }}</td>
<td>{{ stat.total_messages }}</td>
<td class="dw-success-text">{{ stat.dmarc_pass_count }}</td>
<td class="dw-danger-text">{{ stat.dmarc_fail_count }}</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="dw-muted">No daily stats yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if trend_total > trend_page_size %}
<div class="dw-table-footer">
<span>{{ ((trend_page - 1) * trend_page_size) + 1 }}-{{ [trend_page * trend_page_size, trend_total] | min }} of {{ trend_total }}</span>
<span class="dw-pager">
<a hx-get="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page - 1 }}" hx-select="#trend-panel" hx-target="#trend-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if trend_page <= 1 else '' }}" href="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page - 1 }}"><span class="material-symbols-outlined">chevron_left</span></a>
<a hx-get="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page + 1 }}" hx-select="#trend-panel" hx-target="#trend-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if trend_page * trend_page_size >= trend_total else '' }}" href="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page }}&trend_page={{ trend_page + 1 }}"><span class="material-symbols-outlined">chevron_right</span></a>
</span>
</div>
{% endif %}
</div>
<div>
<h2 class="dw-panel-title">Top Report Organizations</h2>
<div class="dw-list-card">
{% for org, count in reporters %}
<div class="dw-list-row">
<span>{{ org or "unknown" }}</span>
<code>{{ count }}</code>
</div>
{% else %}
<div class="dw-list-empty">No reporting organizations yet.</div>
{% endfor %}
</div>
</div>
<div>
<h2 class="dw-panel-title">Disposition and Sender Mix</h2>
<div class="dw-list-card">
{% for disposition, count in dispositions %}
<div class="dw-list-row">
<span>{{ disposition or "none" }}</span>
<code>{{ count }}</code>
</div>
{% endfor %}
{% for known, count in known_unknown %}
<div class="dw-list-row">
<span>{{ "Known senders" if known else "Unknown senders" }}</span>
<code>{{ count }}</code>
</div>
{% else %}
{% if not dispositions %}
<div class="dw-list-empty">No disposition data yet.</div>
{% endif %}
{% endfor %}
</div>
</div>
</section>
<section class="dw-reports-section" id="reports-panel">
<h2 class="dw-panel-title">Recent Reports</h2>
<div class="dw-report-list">
{% for report in reports %}
<a href="/reports/{{ report.id }}" class="dw-report-row">
<span class="material-symbols-outlined">description</span>
<span class="dw-report-copy">
<strong>{{ report.org_name or "unknown" }} · {{ report.report_id or report.id }}</strong>
<code>{{ report.date_begin | fmt_dt }}{% if report.date_end %} to {{ report.date_end | fmt_dt }}{% endif %}</code>
</span>
<span class="material-symbols-outlined">arrow_forward_ios</span>
</a>
{% else %}
<div class="dw-list-empty">No reports imported for this domain yet.</div>
{% endfor %}
</div>
{% if report_total > report_page_size %}
<div class="dw-table-footer">
<span>{{ ((report_page - 1) * report_page_size) + 1 }}-{{ [report_page * report_page_size, report_total] | min }} of {{ report_total }}</span>
<span class="dw-pager">
<a hx-get="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page - 1 }}&trend_page={{ trend_page }}" hx-select="#reports-panel" hx-target="#reports-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if report_page <= 1 else '' }}" href="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page - 1 }}&trend_page={{ trend_page }}"><span class="material-symbols-outlined">chevron_left</span></a>
<a hx-get="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page + 1 }}&trend_page={{ trend_page }}" hx-select="#reports-panel" hx-target="#reports-panel" hx-swap="outerHTML" class="{{ 'is-disabled' if report_page * report_page_size >= report_total else '' }}" href="/domains/{{ domain }}?source_page={{ source_page }}&alert_page={{ alert_page }}&report_page={{ report_page + 1 }}&trend_page={{ trend_page }}"><span class="material-symbols-outlined">chevron_right</span></a>
</span>
</div>
{% endif %}
</section>
{% endblock %}
+299
View File
@@ -0,0 +1,299 @@
{% 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">Inboxes</h1>
<p class="mt-1 text-body-base text-on-surface-variant">Polling health and manual import controls.</p>
</header>
<section class="surface-card divide-y divide-outline-variant overflow-hidden">
{% for inbox in inboxes %}
{% set job = jobs.get(inbox.inbox_id) if jobs else none %}
{% set running = job and job.status in ["queued", "running"] %}
{% set skipped = skipped_payloads.get(inbox.inbox_id, []) if skipped_payloads else [] %}
<article class="p-stack-md" id="inbox-row-{{ inbox.inbox_id }}">
<div class="dw-inbox-row">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-stack-sm">
<h2 class="text-headline-md font-semibold text-on-background">{{ inbox.label }}</h2>
<code class="rounded bg-surface-container px-2 py-0.5 font-mono text-data-mono text-secondary">{{ inbox.inbox_id }}</code>
{% set status_label = "running" if running else ("disabled" if not inbox.enabled else ("error" if inbox.last_error else "ready")) %}
<span id="inbox-status-chip-{{ inbox.inbox_id }}" class="status-chip {{ 'chip-warning' if running or not inbox.enabled else ('chip-fail' if inbox.last_error else 'chip-pass') }}">{{ status_label }}</span>
</div>
<p class="mt-stack-sm text-body-sm text-on-surface-variant">{{ inbox.domain }} · {{ inbox.folder }} · {{ inbox.recipient }}</p>
<div class="dw-inbox-meta mt-stack-md">
<div><span class="label-caps block">Last Check</span><span id="inbox-last-check-{{ inbox.inbox_id }}">{{ inbox.last_check_at | fmt_dt }}</span></div>
<div><span class="label-caps block">Last Success</span><span id="inbox-last-success-{{ inbox.inbox_id }}">{{ inbox.last_success_at | fmt_dt }}</span></div>
<div><span class="label-caps block">New Messages</span><span id="inbox-new-messages-{{ inbox.inbox_id }}">{{ inbox.last_new_messages }}</span></div>
<div><span class="label-caps block">Imported</span><span id="inbox-imported-{{ inbox.inbox_id }}">{{ inbox.last_reports_imported }}</span></div>
</div>
<p id="inbox-last-error-{{ inbox.inbox_id }}" class="mt-stack-md border-l-4 border-l-error bg-error-container p-stack-sm text-body-sm text-on-error-container {{ '' if inbox.last_error else 'hidden' }}">{{ inbox.last_error or "" }}</p>
</div>
<div class="dw-inbox-work">
<div class="dw-inbox-actions">
<a class="button-secondary" href="/domains/{{ inbox.domain }}">
<span class="material-symbols-outlined text-[18px]">visibility</span>
View Domain
</a>
<button class="button-secondary js-inbox-action" type="button" data-action="process-now" data-inbox-id="{{ inbox.inbox_id }}" {% if not inbox.enabled or running %}disabled{% endif %}>
<span class="material-symbols-outlined text-[18px]">sync</span>
Process Now
</button>
<button class="button-secondary js-inbox-action" type="button" data-action="backlog" data-inbox-id="{{ inbox.inbox_id }}" {% if not inbox.enabled or running %}disabled{% endif %}>
<span class="material-symbols-outlined text-[18px]">manage_search</span>
Backlog Scan
</button>
</div>
<div class="dw-inbox-job">
<div class="inbox-action-result {{ 'is-running' if running else '' }}" id="inbox-action-result-{{ inbox.inbox_id }}" data-job-id="{{ job.id if job else '' }}" role="status" aria-live="polite">
{% if running %}
{{ job.processed_messages }} of {{ job.scanned_messages or "?" }} scanned · {{ job.valid_reports_imported }} imported
{% endif %}
</div>
<div class="inbox-progress {{ 'is-active' if running else '' }}" id="inbox-progress-{{ inbox.inbox_id }}">
<span style="width: {{ job.progress_percent if job and job.progress_percent is not none else 100 }}%;"></span>
</div>
</div>
</div>
<div class="inbox-duplicate-list {{ 'hidden' if not skipped else '' }}" id="inbox-duplicate-list-{{ inbox.inbox_id }}">
{% if skipped %}
<details>
<summary>
<span>Skipped report payloads</span>
<strong>{{ skipped|length }}</strong>
</summary>
<table class="inbox-duplicate-table">
<thead>
<tr>
<th>Reason</th>
<th>Reporter</th>
<th>Report ID</th>
<th>Report Date</th>
<th>Skipped IMAP UID</th>
</tr>
</thead>
<tbody>
{% for item in skipped %}
<tr>
<td>{{ item.reason.replace("_", " ") }}</td>
<td>{{ item.reporting_org or "unknown" }}</td>
<td>{{ item.report_identifier or ("DB #" ~ item.existing_report_id) }}</td>
<td>{{ item.report_date | fmt_date }}</td>
<td>{{ item.imap_uid }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
</div>
</div>
</article>
{% else %}
<div class="p-gutter text-on-surface-variant">No inboxes are configured.</div>
{% endfor %}
</section>
<script>
(() => {
const formatDate = (value) => {
if (!value) return "";
const parts = value.split("-");
return parts.length === 3 ? `${parts[2]}/${parts[1]}/${parts[0]}` : value;
};
const summarize = (data) => {
const imported = data.valid_reports_imported ?? 0;
const duplicateMessages = data.duplicate_messages_skipped ?? 0;
const duplicateReports = data.duplicate_reports_skipped ?? 0;
const rejected = data.rejected_messages ?? 0;
const failed = data.failed_messages ?? 0;
const notImported = [];
if (duplicateMessages) notImported.push(`${duplicateMessages} messages already processed earlier`);
if (duplicateReports) notImported.push(`${duplicateReports} duplicate report payloads`);
if (rejected) notImported.push(`${rejected} rejected by validation/guardrails`);
const skipped = notImported.length ? ` Not imported: ${notImported.join(", ")}.` : "";
return `Done: ${data.scanned_messages ?? 0} scanned, ${data.candidate_messages ?? 0} candidate messages, ${imported} new reports imported.${skipped} Failures: ${failed}.`;
};
const text = (id, value) => {
const element = document.getElementById(id);
if (element) {
element.textContent = value;
}
};
const formatDateTime = (value, fallback = "never") => {
if (!value) return fallback;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const pad = (item) => String(item).padStart(2, "0");
return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const escapeHtml = (value) => String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
const renderDuplicateReports = (inboxId, samples = []) => {
const list = document.getElementById(`inbox-duplicate-list-${inboxId}`);
if (!list) return;
if (!samples.length) {
list.classList.add("hidden");
list.innerHTML = "";
return;
}
const rows = samples.map((item) => {
const reportLabel = item.existing_report_id || `DB #${item.existing_report_db_id}`;
const messageLabel = item.duplicate_message_uid || item.duplicate_message_id || "unknown message";
return `
<tr>
<td>duplicate report payload</td>
<td>${escapeHtml(item.reporting_org || "unknown")}</td>
<td>${escapeHtml(reportLabel)}</td>
<td>${escapeHtml(item.report_date ? formatDate(item.report_date) : "unknown")}</td>
<td>${escapeHtml(messageLabel)}</td>
</tr>`;
}).join("");
const wasOpen = list.querySelector("details")?.open || false;
list.classList.remove("hidden");
list.innerHTML = `
<details ${wasOpen ? "open" : ""}>
<summary>
<span>Skipped report payloads</span>
<strong>${samples.length}</strong>
</summary>
<table class="inbox-duplicate-table">
<thead>
<tr>
<th>Reason</th>
<th>Reporter</th>
<th>Report ID</th>
<th>Report Date</th>
<th>Skipped IMAP UID</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</details>`;
};
const renderInboxStatus = async (inboxId, keepRunning = false) => {
const response = await fetch(`/api/admin/inboxes/${encodeURIComponent(inboxId)}/status`, { credentials: "same-origin" });
if (!response.ok) {
return;
}
const status = await response.json();
text(`inbox-last-check-${inboxId}`, formatDateTime(status.last_check_at));
text(`inbox-last-success-${inboxId}`, formatDateTime(status.last_success_at));
text(`inbox-new-messages-${inboxId}`, status.last_new_messages ?? 0);
text(`inbox-imported-${inboxId}`, status.last_reports_imported ?? 0);
const chip = document.getElementById(`inbox-status-chip-${inboxId}`);
if (chip) {
const label = keepRunning ? "running" : (!status.enabled ? "disabled" : (status.last_error ? "error" : "ready"));
chip.textContent = label;
chip.className = `status-chip ${keepRunning || !status.enabled ? "chip-warning" : (status.last_error ? "chip-fail" : "chip-pass")}`;
}
const error = document.getElementById(`inbox-last-error-${inboxId}`);
if (error) {
error.textContent = status.last_error || "";
error.classList.toggle("hidden", !status.last_error);
}
};
document.querySelectorAll(".js-inbox-action").forEach((button) => {
button.addEventListener("click", async () => {
const inboxId = button.dataset.inboxId;
const result = document.getElementById(`inbox-action-result-${inboxId}`);
const action = button.dataset.action;
const endpoint = action === "backlog" ? "/api/admin/import-jobs/backlog" : "/api/admin/import-jobs/process-now";
const payload = action === "backlog"
? { inbox_id: inboxId, limit: 200 }
: { inbox_id: inboxId, mode: "new", limit: 200 };
const original = button.innerHTML;
const progress = document.getElementById(`inbox-progress-${inboxId}`);
button.disabled = true;
button.innerHTML = `<span class="material-symbols-outlined text-[18px]">sync</span>Running`;
result.className = "inbox-action-result is-running";
result.textContent = "Starting import job...";
progress.classList.add("is-active");
try {
const response = await fetch(endpoint, {
method: "POST",
headers: { ...window.adminPostHeaders, "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) {
throw new Error(typeof data.detail === "string" ? data.detail : JSON.stringify(data.detail || data));
}
pollJob(data.id, inboxId);
} catch (error) {
result.className = "inbox-action-result is-error";
result.textContent = error.message || "Processing failed.";
button.disabled = false;
button.innerHTML = original;
}
});
});
const renderJob = (job, inboxId) => {
const result = document.getElementById(`inbox-action-result-${inboxId}`);
const progress = document.getElementById(`inbox-progress-${inboxId}`);
const fill = progress.querySelector("span");
const running = job.status === "queued" || job.status === "running";
const buttons = document.querySelectorAll(`.js-inbox-action[data-inbox-id="${inboxId}"]`);
buttons.forEach((button) => { button.disabled = running; });
progress.classList.toggle("is-active", running || job.status === "succeeded");
fill.style.width = `${job.progress_percent ?? (running ? 100 : 0)}%`;
progress.classList.toggle("is-indeterminate", running && job.progress_percent === null);
text(`inbox-last-check-${inboxId}`, formatDateTime(job.started_at, "running"));
text(`inbox-new-messages-${inboxId}`, job.scanned_messages ?? 0);
text(`inbox-imported-${inboxId}`, job.valid_reports_imported ?? 0);
if (running) {
result.className = "inbox-action-result is-running";
result.textContent = `${job.processed_messages} of ${job.scanned_messages || "?"} scanned · ${job.valid_reports_imported} imported · ${job.duplicate_reports_skipped ?? 0} duplicate reports · ${job.duplicate_messages_skipped ?? 0} already processed · ${job.rejected_messages ?? 0} rejected · ${job.alerts_created} alerts`;
renderDuplicateReports(inboxId, job.duplicate_report_samples || []);
const chip = document.getElementById(`inbox-status-chip-${inboxId}`);
if (chip) {
chip.textContent = "running";
chip.className = "status-chip chip-warning";
}
} else if (job.status === "succeeded") {
result.className = "inbox-action-result is-success";
result.textContent = summarize(job);
renderDuplicateReports(inboxId, job.duplicate_report_samples || []);
} else if (job.status === "failed") {
result.className = "inbox-action-result is-error";
result.textContent = job.error || "Processing failed.";
renderDuplicateReports(inboxId, []);
}
return running;
};
const pollJob = async (jobId, inboxId) => {
const response = await fetch(`/api/admin/import-jobs/${jobId}`, { credentials: "same-origin" });
const job = await response.json();
if (renderJob(job, inboxId)) {
window.setTimeout(() => pollJob(jobId, inboxId), 2000);
} else {
renderInboxStatus(inboxId);
}
};
document.querySelectorAll(".inbox-action-result[data-job-id]").forEach((result) => {
if (result.dataset.jobId) {
const inboxId = result.id.replace("inbox-action-result-", "");
pollJob(result.dataset.jobId, inboxId);
}
});
})();
</script>
{% endblock %}
+250
View File
@@ -0,0 +1,250 @@
{% extends "base.html" %}
{% block content %}
<header class="dw-page-header">
<h1>Operational Overview</h1>
<p>Deterministic detection with LLM-assisted reporting.</p>
</header>
<section class="dw-overview-filter" aria-label="Traffic filters">
<div class="dw-chart-controls">
<select id="traffic-period" aria-label="Traffic period">
<option value="all" selected>All reports</option>
<option value="24h">24h</option>
<option value="7d">7d</option>
<option value="30d">30d</option>
<option value="365d">Year</option>
<option value="custom">Custom</option>
</select>
<input id="traffic-from" type="date" aria-label="Traffic from date">
<input id="traffic-to" type="date" aria-label="Traffic to date">
<select id="traffic-domain" aria-label="Traffic domain">
<option value="">All domains</option>
{% for domain in domains %}
<option value="{{ domain }}">{{ domain }}</option>
{% endfor %}
</select>
</div>
</section>
<section class="dw-metrics-grid" aria-label="Operational metrics">
<a class="dw-metric-card dw-metric-link" id="monitored-domains-card" href="/inboxes">
<span class="dw-kicker">Monitored Domains</span>
<strong id="metric-domains">{{ data.domains }}</strong>
<small id="metric-domain-target">View inboxes</small>
</a>
<article class="dw-metric-card">
<span class="dw-kicker">DMARC Reports</span>
<strong id="metric-reports">{{ data.reports_today }}</strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">Reported Emails</span>
<strong id="metric-messages">{{ data.messages_today }}</strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">DMARC Pass Rate</span>
<strong id="metric-pass-rate" class="{{ 'dw-success-value' if data.dmarc_pass_rate_value is none or data.dmarc_pass_rate_value >= 95 else ('dw-warning-value' if data.dmarc_pass_rate_value >= 80 else 'dw-danger-value') }}">{{ data.dmarc_pass_rate }}</strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">Passing Emails</span>
<strong id="metric-pass-count">{{ data.dmarc_pass_count }}</strong>
</article>
<article class="dw-metric-card dw-metric-card-critical">
<span class="dw-kicker">Failed Emails</span>
<strong id="metric-fail-count">{{ data.dmarc_fail_count }}</strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">Unknown Sources</span>
<strong id="metric-unknown">{{ data.unknown_sources }}</strong>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">Last Successful Check</span>
<code>{{ data.last_check | fmt_dt }}</code>
</article>
</section>
<section class="dw-chart-card dw-overview-chart">
<div class="dw-card-head">
<h3>Traffic Distribution <span id="traffic-period-label">{{ traffic_label }}</span></h3>
<div class="dw-legend">
<span><i class="dw-dot-valid"></i>Valid</span>
<span><i class="dw-dot-failed"></i>Failed</span>
</div>
</div>
<div class="dw-bars" id="traffic-bars" aria-label="Traffic distribution for imported reports in the selected period">
{% for bucket in traffic %}
<a href="/alerts?status=&date_from={{ bucket.date_from }}&date_to={{ bucket.date_to }}" title="{{ bucket.label }} · {{ bucket.total }} messages, {{ bucket.failed }} failed" style="height: {{ [bucket.height, 3] | max }}%;" aria-label="Show alerts for {{ bucket.label }}">
<span class="dw-bar-valid" style="flex-grow: {{ bucket.valid }};"></span>
<span class="dw-bar-failed" style="flex-grow: {{ bucket.failed }};"></span>
</a>
{% else %}
<span style="height: 3%;"></span>
{% endfor %}
</div>
</section>
<section class="dw-overview-summary">
<div class="dw-section-heading">
<span class="material-symbols-outlined dw-filled-icon">auto_awesome</span>
<h2 id="summary-title">Portfolio DMARC posture</h2>
<button class="button-secondary dw-summary-run" type="button" id="run-daily-summary">
<span class="material-symbols-outlined text-[18px]">play_arrow</span>
Generate Digest
</button>
</div>
<article class="dw-summary-card">
<span class="dw-ai-label">AI Assisted</span>
<div class="dw-summary-copy">
{% set summary_parts = data.summary.split("Actions:", 1) %}
<div>{{ summary_parts[0] }}</div>
{% if summary_parts | length > 1 %}
<div class="dw-recommendations">
<span>Recommended Actions</span>
<ul>
{% for action in summary_parts[1].replace(".", "").split(";") %}
{% if action.strip() %}
<li><span class="material-symbols-outlined">task_alt</span>{{ action.strip() }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="inbox-action-result" id="daily-summary-result" role="status" aria-live="polite"></div>
</article>
</section>
<script>
(() => {
const period = document.getElementById("traffic-period");
const domain = document.getElementById("traffic-domain");
const bars = document.getElementById("traffic-bars");
const periodLabel = document.getElementById("traffic-period-label");
const summaryButton = document.getElementById("run-daily-summary");
const summaryResult = document.getElementById("daily-summary-result");
const summaryTitle = document.getElementById("summary-title");
const dateFrom = document.getElementById("traffic-from");
const dateTo = document.getElementById("traffic-to");
const summaryCopy = document.querySelector(".dw-summary-copy");
const monitoredDomainsCard = document.getElementById("monitored-domains-card");
const metricDomains = document.getElementById("metric-domains");
const metricDomainTarget = document.getElementById("metric-domain-target");
const metricReports = document.getElementById("metric-reports");
const metricMessages = document.getElementById("metric-messages");
const metricPassRate = document.getElementById("metric-pass-rate");
const metricPassCount = document.getElementById("metric-pass-count");
const metricFailCount = document.getElementById("metric-fail-count");
const metricUnknown = document.getElementById("metric-unknown");
const scopeText = (periodLabelValue) => `${periodLabelValue || period.options[period.selectedIndex].text} · ${domain.value || "All domains"}`;
const renderDomainCard = (totalDomains) => {
if (domain.value) {
monitoredDomainsCard.href = `/domains/${encodeURIComponent(domain.value)}`;
metricDomains.textContent = "1";
metricDomainTarget.textContent = domain.value;
} else {
monitoredDomainsCard.href = "/inboxes";
metricDomains.textContent = totalDomains;
metricDomainTarget.textContent = "View inboxes";
}
};
const render = (buckets) => {
bars.innerHTML = "";
if (!buckets.length) {
const bar = document.createElement("span");
bar.style.height = "3%";
bars.appendChild(bar);
return;
}
buckets.forEach((bucket) => {
const bar = document.createElement("a");
bar.style.height = `${Math.max(bucket.height, 3)}%`;
bar.title = `${bucket.label} · ${bucket.total} messages, ${bucket.failed} failed`;
bar.href = `/alerts?status=&date_from=${encodeURIComponent(bucket.date_from)}&date_to=${encodeURIComponent(bucket.date_to)}`;
bar.setAttribute("aria-label", `Show alerts for ${bucket.label}`);
const valid = document.createElement("span");
valid.className = "dw-bar-valid";
valid.style.flexGrow = bucket.valid || 0;
const failed = document.createElement("span");
failed.className = "dw-bar-failed";
failed.style.flexGrow = bucket.failed || 0;
bar.appendChild(valid);
bar.appendChild(failed);
bars.appendChild(bar);
});
};
const passClass = (value) => value === null || value >= 95 ? "dw-success-value" : (value >= 80 ? "dw-warning-value" : "dw-danger-value");
const formatDate = (value) => {
if (!value) return "";
const parts = value.split("-");
return parts.length === 3 ? `${parts[2]}/${parts[1]}/${parts[0]}` : value;
};
const renderSummary = (plain) => {
const parts = (plain || "").split("Actions:");
summaryCopy.innerHTML = "";
const body = document.createElement("div");
body.textContent = parts[0] || "";
summaryCopy.appendChild(body);
if (parts.length > 1) {
const rec = document.createElement("div");
rec.className = "dw-recommendations";
rec.innerHTML = "<span>Recommended Actions</span>";
const list = document.createElement("ul");
parts[1].replace(/\.$/, "").split(";").map((item) => item.trim()).filter(Boolean).forEach((item) => {
const li = document.createElement("li");
li.innerHTML = '<span class="material-symbols-outlined">task_alt</span>';
li.appendChild(document.createTextNode(item));
list.appendChild(li);
});
rec.appendChild(list);
summaryCopy.appendChild(rec);
}
};
const refresh = async () => {
const params = new URLSearchParams({ period: period.value });
if (domain.value) params.set("domain", domain.value);
if (period.value === "custom") {
if (dateFrom.value) params.set("date_from", dateFrom.value);
if (dateTo.value) params.set("date_to", dateTo.value);
}
const response = await fetch(`/api/overview?${params}`, { credentials: "same-origin" });
if (response.ok) {
const data = await response.json();
const label = scopeText(data.period_label);
periodLabel.textContent = label;
metricReports.textContent = data.metrics.reports_today;
metricMessages.textContent = data.metrics.messages_today;
metricPassCount.textContent = data.metrics.dmarc_pass_count;
metricFailCount.textContent = data.metrics.dmarc_fail_count;
metricUnknown.textContent = data.metrics.unknown_sources;
renderDomainCard(data.metrics.domains);
metricPassRate.textContent = data.metrics.dmarc_pass_rate;
metricPassRate.className = passClass(data.metrics.dmarc_pass_rate_value);
summaryTitle.textContent = domain.value ? `${domain.value} DMARC posture` : "Portfolio DMARC posture";
renderSummary(data.metrics.summary || "");
render(data.buckets || []);
}
};
period.addEventListener("change", refresh);
domain.addEventListener("change", refresh);
dateFrom.addEventListener("change", refresh);
dateTo.addEventListener("change", refresh);
summaryButton.addEventListener("click", async () => {
summaryButton.disabled = true;
summaryResult.className = "inbox-action-result is-running";
summaryResult.textContent = "Generating digest...";
try {
const response = await fetch("/api/admin/scheduler/daily-summary", { method: "POST", headers: window.adminPostHeaders, credentials: "same-origin" });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || "Digest generation failed.");
summaryResult.className = "inbox-action-result is-success";
summaryResult.textContent = "Digest generated.";
await refresh();
} catch (error) {
summaryResult.className = "inbox-action-result is-error";
summaryResult.textContent = error.message || "Digest generation failed.";
} finally {
summaryButton.disabled = false;
}
});
})();
</script>
{% endblock %}
+84
View File
@@ -0,0 +1,84 @@
{% 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">Report {{ report.id }}</h1>
<p class="mt-1 text-body-base text-on-surface-variant">{{ report.domain }} · {{ report.org_name or "unknown organization" }}</p>
</header>
<section class="mb-stack-lg grid grid-cols-1 gap-gutter md:grid-cols-2 xl:grid-cols-4">
<div class="metric-card">
<span class="label-caps">Report Org</span>
<span class="text-body-base font-bold">{{ report.org_name or "unknown" }}</span>
</div>
<div class="metric-card">
<span class="label-caps">Report ID</span>
<span class="break-all font-mono text-data-mono">{{ report.report_id or report.id }}</span>
</div>
<div class="metric-card">
<span class="label-caps">Date Range</span>
<span class="font-mono text-data-mono">{{ report.date_begin | fmt_dt }}<br>{{ report.date_end | fmt_dt }}</span>
</div>
<div class="metric-card">
<span class="label-caps">Published Policy</span>
<span class="font-mono text-data-mono">p={{ report.policy_p }}, sp={{ report.policy_sp }}, pct={{ report.policy_pct }}</span>
</div>
</section>
<section class="mb-stack-lg">
<h2 class="mb-stack-md text-headline-md font-semibold">Alerts From This Report</h2>
<div class="dw-alert-feed">
{% for alert in alerts %}
<a class="dw-alert-item is-{{ alert.severity_class }}" href="/alerts?domain={{ report.domain }}&alert_type={{ alert.type }}">
<span class="dw-alert-row">
<span>{{ alert.severity }}</span>
<time>{{ alert.status }}</time>
</span>
<strong>{{ alert.title }}</strong>
<p>{{ alert.llm_summary or alert.summary }}</p>
</a>
{% else %}
<div class="dw-alert-empty">No alerts are linked to this report.</div>
{% endfor %}
</div>
</section>
<section>
<h2 class="mb-stack-md text-headline-md font-semibold">Records</h2>
<div class="surface-card overflow-hidden">
<div class="data-table-wrap">
<table class="data-table">
<thead>
<tr>
<th title="DMARC aggregate source_ip: the IP observed by the reporting receiver. It may be a relay, forwarder, gateway, or direct sender.">Observed IP</th>
<th>Count</th>
<th>SPF</th>
<th>DKIM</th>
<th>DMARC</th>
<th>Known Sender</th>
<th>Applied Policy</th>
<th>Policy Override</th>
</tr>
</thead>
<tbody>
{% for row in report.records %}
<tr>
<td class="font-mono text-data-mono">{{ row.source_ip }}</td>
<td>{{ row.count }}</td>
<td title="{{ row.spf_auth_tooltip }}"><span class="status-chip {{ 'chip-pass' if row.policy_spf == 'pass' else 'chip-fail' }}">{{ row.policy_spf or "none" }}</span></td>
<td title="{{ row.dkim_auth_tooltip }}"><span class="status-chip {{ 'chip-pass' if row.policy_dkim == 'pass' else 'chip-fail' }}">{{ row.policy_dkim or "none" }}</span></td>
<td><span class="status-chip {{ 'chip-pass' if row.dmarc_pass else 'chip-fail' }}">{{ "pass" if row.dmarc_pass else "fail" }}</span></td>
<td><span class="status-chip {{ 'chip-pass' if row.known_sender_name else 'chip-info' }}" title="{{ row.known_sender_name or 'No configured sender matched this observed IP/authentication evidence.' }}">{{ row.known_sender_name or "unknown" }}</span></td>
<td><span class="status-chip {{ 'chip-pass' if not row.disposition or row.disposition == 'none' else ('chip-warning' if row.disposition == 'quarantine' else 'chip-fail') }}">{{ row.disposition or "none" }}</span></td>
<td><span class="status-chip {{ 'chip-info' if row.reason_type else 'chip-pass' }}" title="{{ row.reason_comment or 'No policy override reason reported.' }}">{{ row.reason_type or "none" }}</span></td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-on-surface-variant">No records found in this report.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}
+247
View File
@@ -0,0 +1,247 @@
{% extends "base.html" %}
{% block content %}
{% set env_items = env_status.items() | list %}
{% set missing_env = env_items | selectattr("1", "equalto", false) | list %}
<header class="dw-page-header dw-settings-header">
<div>
<h1>Settings</h1>
<p>Read-only runtime configuration and operational posture.</p>
</div>
<code>{{ config_path }}</code>
</header>
<section class="dw-settings-metrics" aria-label="Settings summary">
<article class="dw-metric-card">
<span class="dw-kicker">Application</span>
<strong>{{ settings.app.name }}</strong>
<span class="dw-card-note">{{ settings.app.base_url }}</span>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">Polling</span>
<strong>{{ settings.app.poll_interval_minutes }} min</strong>
<span class="dw-card-note">{{ settings.app.timezone }}</span>
</article>
<article class="dw-metric-card">
<span class="dw-kicker">LLM</span>
<strong>{{ settings.llm.model }}</strong>
<span class="dw-card-note">{{ settings.llm.provider }}</span>
</article>
<article class="dw-metric-card {{ 'dw-metric-card-critical' if missing_env else '' }}">
<span class="dw-kicker">Environment</span>
<strong>{{ env_items | length - missing_env | length }}/{{ env_items | length }}</strong>
<span class="dw-card-note">{{ missing_env | length }} missing</span>
</article>
</section>
<section class="dw-settings-board">
<section class="dw-settings-panel">
<h2 class="dw-panel-title">Runtime</h2>
<div class="dw-info-card">
<div class="dw-info-row">
<span>Database</span>
<code>{{ settings.app.database_url }}</code>
</div>
<div class="dw-info-row">
<span>Log Level</span>
<strong>{{ settings.app.log_level }}</strong>
</div>
<div class="dw-info-row">
<span>Max Attachment Size</span>
<strong>{{ settings.app.max_attachment_decompressed_mb }} MB</strong>
</div>
<div class="dw-info-row">
<span>Max Reports Per Poll</span>
<strong>{{ settings.app.max_reports_per_poll }}</strong>
</div>
</div>
</section>
<section class="dw-settings-panel">
<h2 class="dw-panel-title">Inboxes</h2>
<div class="dw-inbox-grid">
{% for inbox in settings.inboxes %}
<article class="dw-settings-card">
<div class="dw-settings-card-head">
<div>
<h3>{{ inbox.label }}</h3>
<code>{{ inbox.id }}</code>
</div>
<span class="dw-chip {{ 'dw-chip-pass' if inbox.enabled else 'dw-chip-warning' }}">{{ "Enabled" if inbox.enabled else "Disabled" }}</span>
</div>
<div class="dw-info-list">
<div><span>Domain</span><strong>{{ inbox.domain }}</strong></div>
<div><span>Folder</span><strong>{{ inbox.folder }}</strong></div>
<div><span>Recipient</span><strong>{{ inbox.recipient }}</strong></div>
<div><span>IMAP</span><strong>{{ inbox.imap_host }}:{{ inbox.imap_port }} · {{ "SSL" if inbox.imap_ssl else "plain" }}</strong></div>
</div>
</article>
{% else %}
<div class="dw-list-empty">No inboxes configured.</div>
{% endfor %}
</div>
</section>
<section class="dw-settings-panel dw-settings-panel-wide">
<h2 class="dw-panel-title">Known Senders</h2>
<div class="dw-sender-domain-grid">
{% for domain, senders in settings.known_senders.items() %}
<article class="dw-settings-card dw-sender-domain">
<div class="dw-sender-domain-head">
<h3>{{ domain }}</h3>
<span>{{ senders | length }} senders</span>
</div>
<div class="dw-sender-list">
{% for sender in senders %}
<article class="dw-sender-row">
<div>
<strong>{{ sender.name }}</strong>
<code>{{ sender.id }}</code>
</div>
<div class="dw-sender-values">
<div>
<span>IP ranges</span>
<ul>
{% for item in sender.ip_allowlist %}
<li><code>{{ item }}</code></li>
{% else %}
<li class="dw-muted">None</li>
{% endfor %}
</ul>
</div>
<div>
<span>DKIM domains</span>
<ul>
{% for item in sender.dkim_domains %}
<li><code>{{ item }}</code></li>
{% else %}
<li class="dw-muted">None</li>
{% endfor %}
</ul>
</div>
<div>
<span>SPF domains</span>
<ul>
{% for item in sender.spf_domains %}
<li><code>{{ item }}</code></li>
{% else %}
<li class="dw-muted">None</li>
{% endfor %}
</ul>
</div>
</div>
</article>
{% endfor %}
</div>
</article>
{% else %}
<div class="dw-list-empty">No known senders configured.</div>
{% endfor %}
</div>
</section>
<section class="dw-settings-panel">
<h2 class="dw-panel-title">Security</h2>
<div class="dw-list-card">
<div class="dw-list-row">
<span>Dashboard Basic Auth</span>
<span class="dw-chip {{ 'dw-chip-pass' if settings.security.dashboard_auth_enabled else 'dw-chip-warning' }}">{{ "Enabled" if settings.security.dashboard_auth_enabled else "Disabled" }}</span>
</div>
<div class="dw-list-row">
<span>Homepage Token</span>
<span class="dw-chip {{ 'dw-chip-pass' if settings.security.api_token_required else 'dw-chip-warning' }}">{{ "Required" if settings.security.api_token_required else "Not required" }}</span>
</div>
<div class="dw-list-row">
<span>Email Alerts</span>
<span class="dw-chip {{ 'dw-chip-pass' if settings.alerts.email.enabled else 'dw-chip-warning' }}">{{ "Enabled" if settings.alerts.email.enabled else "Disabled" }}</span>
</div>
</div>
</section>
<section class="dw-settings-panel">
<h2 class="dw-panel-title">LLM Data Controls</h2>
<div class="dw-list-card">
<div class="dw-list-row">
<span>Alert Explanations</span>
<span class="dw-chip {{ 'dw-chip-pass' if settings.llm.generate_alert_explanations else 'dw-chip-warning' }}">{{ "On" if settings.llm.generate_alert_explanations else "Off" }}</span>
</div>
<div class="dw-list-row">
<span>Daily Summary</span>
<span class="dw-chip {{ 'dw-chip-pass' if settings.llm.generate_daily_summary else 'dw-chip-warning' }}">{{ "On" if settings.llm.generate_daily_summary else "Off" }}</span>
</div>
<div class="dw-list-row">
<span>Weekly Summary</span>
<span class="dw-chip {{ 'dw-chip-pass' if settings.llm.generate_weekly_summary else 'dw-chip-warning' }}">{{ "On" if settings.llm.generate_weekly_summary else "Off" }}</span>
</div>
<div class="dw-list-row">
<span>Raw XML to LLM</span>
<span class="dw-chip {{ 'dw-chip-warning' if settings.llm.send_raw_xml_to_llm else 'dw-chip-pass' }}">{{ "On" if settings.llm.send_raw_xml_to_llm else "Off" }}</span>
</div>
<div class="dw-list-row">
<span>Raw Email to LLM</span>
<span class="dw-chip {{ 'dw-chip-warning' if settings.llm.send_raw_email_to_llm else 'dw-chip-pass' }}">{{ "On" if settings.llm.send_raw_email_to_llm else "Off" }}</span>
</div>
<div class="dw-list-row">
<span>System Prompt</span>
<code>{{ settings.llm.system_prompt_path }}</code>
</div>
<div class="dw-list-row">
<span>Alert Prompt</span>
<code>{{ settings.llm.alert_prompt_path }}</code>
</div>
<div class="dw-list-row">
<span>Digest Prompt</span>
<code>{{ settings.llm.digest_prompt_path }}</code>
</div>
</div>
</section>
<section class="dw-settings-panel">
<h2 class="dw-panel-title">Alert Thresholds</h2>
<div class="dw-list-card">
{% for name, value in settings.alerts.thresholds.model_dump().items() %}
<div class="dw-list-row">
<span>{{ name.replace("_", " ") }}</span>
<code>{{ value }}</code>
</div>
{% endfor %}
</div>
</section>
</section>
<section class="dw-settings-env">
<div class="dw-sidebar-head">
<h2>LLM Prompts</h2>
<span class="dw-kicker">Read From Disk</span>
</div>
<div class="dw-prompt-grid">
{% for prompt in prompts %}
<article class="dw-prompt-card">
<div class="dw-settings-card-head">
<div>
<h3>{{ prompt.label }}</h3>
<code>{{ prompt.path }}</code>
</div>
<span class="dw-chip {{ 'dw-chip-pass' if prompt.exists else 'dw-chip-warning' }}">{{ "Loaded" if prompt.exists else "Fallback" }}</span>
</div>
<pre>{{ prompt.content or "Using built-in fallback prompt." }}</pre>
</article>
{% endfor %}
</div>
</section>
<section class="dw-settings-env">
<div class="dw-sidebar-head">
<h2>Environment</h2>
<span class="dw-kicker">{{ missing_env | length }} Missing</span>
</div>
<div class="dw-env-grid">
{% for name, present in env_items | sort %}
<div class="dw-env-item {{ 'is-missing' if not present else '' }}">
<code>{{ name }}</code>
<span class="dw-chip {{ 'dw-chip-pass' if present else 'dw-chip-fail' }}">{{ "Set" if present else "Missing" }}</span>
</div>
{% endfor %}
</div>
</section>
{% endblock %}