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