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