251 lines
11 KiB
HTML
251 lines
11 KiB
HTML
{% 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 %}
|