feat: dashboard real-time con aggiornamento incrementale

- Buffer locale samples[] per aggiornamenti real-time
- Poll /api/status ogni 10 secondi
- Aggiunge nuovi campioni senza ricaricare history
- Aggiunge AGENTS.md per istruzioni agenti OpenCode
- Aggiunge team agenti in .opencode/agents/
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-26 14:34:23 +02:00
parent e8ae3603e7
commit 6c8c05b13b
16 changed files with 1248 additions and 27 deletions
+69 -27
View File
@@ -288,6 +288,11 @@ def _build_dashboard_html() -> str:
const badge = document.getElementById('statusBadge');
const meta = document.getElementById('meta');
const ctx = document.getElementById('latencyChart').getContext('2d');
// Buffer locale per campioni - mantiene solo i dati nel grafico
let samples = [];
const MAX_VISIBLE_SAMPLES = 100; // Limite visualizzazione per performance
const chart = new Chart(ctx, {
type: 'line',
data: { labels: [], datasets: [
@@ -303,37 +308,74 @@ def _build_dashboard_html() -> str:
}
});
async function load() {
const [statusRes, historyRes] = await Promise.all([
fetch('/api/status'),
fetch('/api/history?hours=48')
]);
const status = await statusRes.json();
async function loadHistory() {
// Carica history iniziale una sola volta
const historyRes = await fetch('/api/history?hours=48');
const history = await historyRes.json();
if (status.success === true) {
badge.textContent = 'UP';
badge.className = 'badge ok';
} else if (status.success === false) {
badge.textContent = 'DOWN';
badge.className = 'badge ko';
} else {
badge.textContent = 'N/A';
badge.className = 'badge';
}
meta.textContent = `Campioni 48h: ${history.count} | Ultimo campione: ${status.timestamp ? new Date(status.timestamp * 1000).toLocaleString() : 'n/a'}`;
chart.data.labels = history.samples.map(s => new Date(s.timestamp * 1000).toLocaleTimeString());
chart.data.datasets[0].data = history.samples.map(s => s.latency_ms);
chart.data.datasets[1].data = history.samples.map(s => s.success ? 0 : 1);
chart.update();
samples = history.samples || [];
updateChart();
}
load().catch((err) => {
meta.textContent = `Errore caricamento dashboard: ${err}`;
function updateChart() {
// Limita i campioni visibili per performance
const visibleSamples = samples.slice(-MAX_VISIBLE_SAMPLES);
chart.data.labels = visibleSamples.map(s => {
const d = new Date(s.timestamp * 1000);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
});
chart.data.datasets[0].data = visibleSamples.map(s => s.latency_ms);
chart.data.datasets[1].data = visibleSamples.map(s => s.success ? 0 : 1);
chart.update('none'); // update senza animazione per performance
}
async function pollStatus() {
try {
const statusRes = await fetch('/api/status');
const status = await statusRes.json();
// Aggiorna badge
if (status.success === true) {
badge.textContent = 'UP';
badge.className = 'badge ok';
} else if (status.success === false) {
badge.textContent = 'DOWN';
badge.className = 'badge ko';
} else {
badge.textContent = 'N/A';
badge.className = 'badge';
}
// Aggiorna meta con timestamp corrente
meta.textContent = 'Ultimo aggiornamento: ' + new Date().toLocaleTimeString() + ' | Campioni: ' + samples.length;
// Aggiunge nuovo sample se non gia presente
if (status.timestamp && !samples.find(s => s.timestamp === status.timestamp)) {
samples.push({
timestamp: status.timestamp,
success: status.success,
latency_ms: status.latency_ms,
error_message: status.error_message
});
// Rimuove campioni piu vecchi di 48h
const cutoff = Math.floor(Date.now() / 1000) - (48 * 3600);
samples = samples.filter(s => s.timestamp >= cutoff);
updateChart();
}
} catch (err) {
meta.textContent = 'Errore: ' + err.message;
}
}
// Inizializzazione
loadHistory().catch((err) => {
meta.textContent = 'Errore caricamento history: ' + err;
});
setInterval(() => load().catch(() => {}), 30000);
// Poll ogni 10 secondi per aggiornamenti real-time
setInterval(pollStatus, 10000);
</script>
</body>
</html>