// 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 = `collect.py ou import_historical.py pour peupler le dashboard.