feat: implement collection runs tracking and error acknowledgment features
This commit is contained in:
1 parent
c39364eda4
commit
34903492f6
14 files changed
+484
-21
No files matched your search
@@ -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();
|
||||
Reference in new issue
Block a user