feat: implement collection runs tracking and error acknowledgment features

This commit is contained in:
ludal committed 2026-09-21 13:45:58 +02:00
1 parent c39364eda4
commit 34903492f6
14 files changed
+484 -21

No files matched your search

+181
View File
@@ -0,0 +1,181 @@
// collecte.js — page "Collecte" : historique des exécutions de tâches de
// collecte (cron quotidien + backfill historique), détail des erreurs par
// exécution, et acquittement manuel.
let currentRuns = [];
async function loadRuns() {
const res = await fetch("/api/collection-runs");
if (!res.ok) throw new Error(`API collection-runs: HTTP ${res.status}`);
return res.json();
}
function runTypeLabel(type) {
return type === "backfill" ? "Historique (backfill)" : "Journalière";
}
function formatDateTime(iso) {
if (!iso) return "—";
return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(iso));
}
function formatDuration(startIso, endIso) {
if (!startIso || !endIso) return "—";
const seconds = Math.max(0, Math.round((new Date(endIso) - new Date(startIso)) / 1000));
if (seconds < 60) return `${seconds} s`;
return `${Math.floor(seconds / 60)} min ${seconds % 60} s`;
}
function statusBadge(run) {
if (run.tenants_error === 0) {
return `<span class="status-label"><span class="status-dot ok"></span>OK</span>`;
}
if (run.acknowledged_at) {
return `<span class="status-label"><span class="status-dot warning"></span>Erreurs (acquittées)</span>`;
}
return `<span class="status-label"><span class="status-dot error"></span>Erreurs</span>`;
}
function renderRunsTable() {
const wrap = document.getElementById("runs-table-wrap");
if (currentRuns.length === 0) {
wrap.innerHTML = `<div class="empty-state">Aucune exécution enregistrée pour l'instant. Lance <code>collect.py</code> ou <code>backfill_historical.py</code>.</div>`;
return;
}
const rows = currentRuns
.map(
(r) => `
<tr class="clickable" data-id="${r.id}">
<td>${runTypeLabel(r.run_type)}</td>
<td>${formatDateTime(r.started_at)}</td>
<td>${formatDuration(r.started_at, r.finished_at)}</td>
<td class="num">${r.tenants_ok}</td>
<td class="num">${r.tenants_error}</td>
<td>${statusBadge(r)}</td>
</tr>`
)
.join("");
wrap.innerHTML = `
<table class="data-table">
<thead>
<tr>
<th>Type</th>
<th>Démarrée</th>
<th>Durée</th>
<th class="num">OK</th>
<th class="num">Erreurs</th>
<th>Statut</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>`;
wrap.querySelectorAll("tr.clickable").forEach((tr) => {
tr.addEventListener("click", () => openRunDrawer(Number(tr.dataset.id)));
});
}
function openRunDrawer(runId) {
const run = currentRuns.find((r) => r.id === runId);
if (!run) return;
document.getElementById("drawer-title").textContent = `${runTypeLabel(run.run_type)} — ${formatDateTime(run.started_at)}`;
document.getElementById("drawer-sub").textContent = `${run.tenants_ok} OK, ${run.tenants_error} en erreur`;
document.getElementById("run-drawer").classList.add("open");
document.getElementById("run-drawer").setAttribute("aria-hidden", "false");
document.getElementById("drawer-backdrop").classList.add("open");
if (run.tenants_error === 0) {
document.getElementById("drawer-body").innerHTML = `<div class="empty-state">Aucune erreur sur cette exécution.</div>`;
return;
}
document.getElementById("drawer-body").innerHTML = `<div class="skeleton">Chargement…</div>`;
fetch(`/api/collection-runs/${runId}/errors`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => renderDrawerContent(run, data.errors))
.catch((e) => {
document.getElementById("drawer-body").innerHTML =
`<div class="banner error"><strong>Erreur de chargement</strong> : ${escapeHtml(e.message)}</div>`;
});
}
function renderDrawerContent(run, errors) {
const bodyEl = document.getElementById("drawer-body");
const list = errors
.map(
(e) => `
<div class="resource-row" style="flex-direction:column; align-items:flex-start; gap:4px;">
<div class="resource-name">
<span class="provider-badge ${e.provider}">${providerLabel(e.provider)}</span>
${escapeHtml(e.tenant_name)} — ${escapeHtml(e.period_start)}
</div>
<div class="resource-pct">${escapeHtml(e.error_message || "erreur inconnue")}</div>
</div>`
)
.join("");
const ackBlock = run.acknowledged_at
? `<p class="chart-card-sub" style="margin: 0 0 14px;">Acquittée le ${formatDateTime(run.acknowledged_at)}</p>`
: `<button type="button" class="btn" id="ack-button" style="margin-bottom: 14px;">Acquitter cette erreur</button>`;
bodyEl.innerHTML = `${ackBlock}${list}`;
const btn = document.getElementById("ack-button");
if (btn) {
btn.addEventListener("click", () => acknowledgeRun(run.id));
}
}
async function acknowledgeRun(runId) {
const btn = document.getElementById("ack-button");
if (btn) btn.disabled = true;
try {
const res = await fetch(`/api/collection-runs/${runId}/acknowledge`, { method: "POST" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
closeRunDrawer();
await init();
refreshCollecteAlert();
} catch (e) {
document.getElementById("drawer-body").innerHTML =
`<div class="banner error"><strong>Erreur</strong> : ${escapeHtml(e.message)}</div>`;
}
}
function closeRunDrawer() {
document.getElementById("run-drawer").classList.remove("open");
document.getElementById("run-drawer").setAttribute("aria-hidden", "true");
document.getElementById("drawer-backdrop").classList.remove("open");
}
function setupDrawer() {
document.getElementById("drawer-close").addEventListener("click", closeRunDrawer);
document.getElementById("drawer-backdrop").addEventListener("click", closeRunDrawer);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeRunDrawer();
});
}
async function init() {
try {
const data = await loadRuns();
currentRuns = data.runs;
renderRunsTable();
} catch (e) {
document.getElementById("error-banner").innerHTML = `
<div class="banner error"><strong>Erreur de chargement</strong> : ${escapeHtml(e.message)}</div>`;
document.getElementById("runs-table-wrap").innerHTML = "";
}
}
init();
setupDrawer();