// app.js — page globale : KPIs, graphique mensuel AWS/Azure (cliquable, // multi-sélection), table des tenants. let allTenants = []; let currentMonths = []; let currentProvidersTotals = { aws: [], azure: [] }; let monthlyChart = null; const selectedMonths = new Set(); async function loadOverview() { const res = await fetch("/api/overview?months=12"); if (!res.ok) throw new Error(`API overview: HTTP ${res.status}`); return res.json(); } function renderKpis(data) { document.getElementById("kpi-total").textContent = formatEUR(data.kpis.total_current_month); document.getElementById("kpi-aws").textContent = formatEUR(data.kpis.total_aws_current_month); document.getElementById("kpi-azure").textContent = formatEUR(data.kpis.total_azure_current_month); const errorCount = data.kpis.tenants_error.length; const errEl = document.getElementById("kpi-errors"); errEl.textContent = String(errorCount); errEl.style.color = errorCount > 0 ? "var(--status-critical)" : ""; const banner = document.getElementById("error-banner"); if (errorCount > 0) { const names = data.kpis.tenants_error .map((t) => `${providerLabel(t.provider)} / ${escapeHtml(t.name)}`) .join(", "); banner.innerHTML = ` `; } else { banner.innerHTML = ""; } } function dimmed(hex) { return Chart.helpers.color(hex).alpha(0.3).rgbString(); } function renderMonthlyChart() { const labels = currentMonths.map(monthLabel); const surface = cssVar("--surface-1"); const awsColor = cssVar("--provider-aws"); const azureColor = cssVar("--provider-azure"); const colorFor = (base) => (ctx) => { if (selectedMonths.size === 0) return base; return selectedMonths.has(currentMonths[ctx.dataIndex]) ? base : dimmed(base); }; if (monthlyChart) { monthlyChart.destroy(); monthlyChart = null; } const ctx = document.getElementById("monthly-chart").getContext("2d"); monthlyChart = new Chart(ctx, { type: "bar", data: { labels, datasets: [ { label: "AWS", data: currentProvidersTotals.aws, backgroundColor: colorFor(awsColor), borderColor: surface, borderWidth: { top: 2, right: 0, bottom: 0, left: 0 }, borderSkipped: false, borderRadius: { topLeft: 4, topRight: 4 }, stack: "total", maxBarThickness: 24, }, { label: "Azure", data: currentProvidersTotals.azure, backgroundColor: colorFor(azureColor), borderColor: surface, borderWidth: { top: 2, right: 0, bottom: 0, left: 0 }, borderSkipped: false, stack: "total", maxBarThickness: 24, }, ], }, options: { ...chartBaseOptions(), onHover: (evt, elements) => { evt.native.target.style.cursor = elements.length ? "pointer" : "default"; }, onClick: (evt, elements) => { if (!elements.length) return; const monthKey = currentMonths[elements[0].index]; if (selectedMonths.has(monthKey)) { selectedMonths.delete(monthKey); } else { selectedMonths.add(monthKey); } // Ne pas détruire ce chart depuis son propre handler onClick — Chart.js // est encore en train de dispatcher l'événement. On reporte au tick // suivant pour éviter de corrompre ses listeners internes. setTimeout(() => { renderMonthlyChart(); renderTenantsTable(filteredTenants()); }, 0); }, plugins: { ...chartBaseOptions().plugins, tooltip: { ...chartBaseOptions().plugins.tooltip, callbacks: { label: (item) => `${item.dataset.label} : ${formatEUR(item.parsed.y)}`, }, }, }, scales: { x: { ...chartBaseOptions().scales.x, stacked: true }, y: { ...chartBaseOptions().scales.y, stacked: true }, }, }, }); } function statusInfo(tenant) { if (tenant.status === "ok") return { cls: "ok", label: "OK" }; if (tenant.status === "error") return { cls: "error", label: "Erreur" }; return { cls: "unknown", label: "Pas encore collecté" }; } function selectedMonthIndexes() { return currentMonths .map((mk, i) => (selectedMonths.has(mk) ? i : -1)) .filter((i) => i >= 0); } function renderSelectionNote() { const el = document.getElementById("month-selection-note"); if (selectedMonths.size === 0) { el.innerHTML = ""; return; } const labels = currentMonths.filter((mk) => selectedMonths.has(mk)).map(monthLabel).join(", "); el.innerHTML = ` Sélection : ${labels} `; document.getElementById("clear-selection").addEventListener("click", () => { selectedMonths.clear(); renderMonthlyChart(); renderSelectionNote(); renderTenantsTable(filteredTenants()); }); } function renderTenantsTable(tenants) { const wrap = document.getElementById("tenants-table-wrap"); renderSelectionNote(); if (tenants.length === 0) { wrap.innerHTML = `
Aucun tenant collecté pour l'instant. Lance collect.py ou import_historical.py pour peupler le dashboard.
`; return; } const indexes = selectedMonthIndexes(); const selectionMode = indexes.length > 0; const rows = tenants .map((t) => { const st = statusInfo(t); if (selectionMode) { const amount = indexes.reduce((sum, i) => sum + (t.monthly[i] || 0), 0); return ` ${providerLabel(t.provider)} ${escapeHtml(t.name)} ${formatEUR(amount)} ${st.label} `; } const delta = formatPct(t.delta_pct); return ` ${providerLabel(t.provider)} ${escapeHtml(t.name)} ${formatEUR(t.current_month)} ${formatEUR(t.previous_month)} ${delta ? `${deltaArrow(t.delta_pct)} ${delta}` : "—"} ${st.label} `; }) .join(""); const header = selectionMode ? `TenantMontant (sélection)Statut collecte` : ` Tenant Mois en cours Mois précédent Évolution Statut collecte `; wrap.innerHTML = ` ${header}${rows}
`; wrap.querySelectorAll("tr.clickable").forEach((tr) => { tr.addEventListener("click", () => { window.location.href = `/tenant/${tr.dataset.provider}/${encodeURIComponent(tr.dataset.name)}`; }); }); } function filteredTenants() { const q = document.getElementById("tenant-search").value.trim().toLowerCase(); if (!q) return allTenants; return allTenants.filter((t) => t.name.toLowerCase().includes(q)); } function setupSearch() { document.getElementById("tenant-search").addEventListener("input", () => { renderTenantsTable(filteredTenants()); }); } async function init() { try { const data = await loadOverview(); allTenants = data.tenants; currentMonths = data.months; currentProvidersTotals = data.providers_totals; renderKpis(data); renderMonthlyChart(); renderTenantsTable(allTenants); setupSearch(); } catch (e) { document.getElementById("error-banner").innerHTML = ` `; document.getElementById("tenants-table-wrap").innerHTML = ""; } } init();