182 lines
6.2 KiB
JavaScript
182 lines
6.2 KiB
JavaScript
// 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();
|