Files
2026-03-18 17:28:29 +01:00

253 lines
9.4 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Controllo Mirror S3</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/style.css" rel="stylesheet">
</head>
<body>
<div class="aurora aurora-a"></div>
<div class="aurora aurora-b"></div>
<main class="container py-4 position-relative">
<section class="hero-panel p-4 p-md-5 mb-4">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-3">
<div>
<p class="eyebrow">Mirror S3 su Locale</p>
<h1 class="mb-2">Dashboard Sync</h1>
<p class="lead mb-0">Controlla tutti i job, avvia sync manuali e consulta i log in tempo reale.</p>
</div>
<div class="text-md-end">
<button id="runAllBtn" class="btn btn-accent btn-lg">Avvia Tutto Ora</button>
<p class="small mt-2 mb-0" id="serverTime">loading...</p>
</div>
</div>
</section>
<section>
<div id="jobsGrid" class="row g-3"></div>
</section>
<section class="log-panel mt-4 p-3 p-md-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex align-items-center gap-2">
<h2 class="h4 mb-0">Attivita Recenti</h2>
<span id="liveIndicator" class="live-dot d-none"></span>
</div>
<div class="d-flex align-items-center gap-2">
<span id="logCount" class="pill">0 eventi</span>
<span class="pill">UTC</span>
</div>
</div>
<div class="log-scroll">
<table class="table table-dark table-borderless align-middle mb-0" id="logsTable">
<thead>
<tr>
<th style="width:160px">Ora</th>
<th style="width:90px">Livello</th>
<th style="width:130px">Job</th>
<th>Messaggio</th>
<th>Dettagli</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
</main>
<script>
async function fetchStatus() {
const response = await fetch('/api/status');
if (!response.ok) {
throw new Error('Impossibile caricare lo stato');
}
return response.json();
}
function fmtDate(value) {
if (!value) return '-';
return new Date(value).toLocaleString();
}
function fmtSummary(summary, running) {
if (running) {
return '<span class="sync-in-progress">&#9654; Sincronizzazione in corso&hellip;</span>';
}
if (!summary || Object.keys(summary).length === 0) {
return '<span class="text-secondary">Nessuna esecuzione ancora</span>';
}
const labels = { downloaded: 'scaricati', kept: 'invariati', deleted: 'eliminati', errors: 'errori', trigger: 'avviato da', error: 'errore' };
const parts = [];
for (const [key, val] of Object.entries(summary)) {
const label = labels[key] || key;
parts.push(`<span class="summary-kv"><span class="summary-key">${label}</span> ${val}</span>`);
}
return parts.join('');
}
function statusClass(status, running) {
if (running) return 'status-running';
if (status === 'success') return 'status-ok';
if (status === 'failed') return 'status-fail';
return 'status-idle';
}
function renderJobs(jobs) {
const grid = document.getElementById('jobsGrid');
grid.innerHTML = '';
jobs.forEach((job) => {
const col = document.createElement('div');
col.className = 'col-12 col-lg-6';
const statusText = job.running ? 'in esecuzione' : job.last_status;
// Barra di avanzamento (visibile solo durante il download)
const total = job.files_total || 0;
const done = job.files_done || 0;
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
const showProgress = job.running && total > 0;
const progressHtml = showProgress ? `
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="mono small progress-phase">${job.phase || 'in corso\u2026'}</span>
<span class="mono small">${done} / ${total}  <span class="text-secondary">(${pct}%)</span></span>
</div>
<div class="progress-track"><div class="progress-fill" style="width:${pct}%"></div></div>
</div>
` : (job.running ? `<div class="mt-3 mono small progress-phase">${job.phase || 'in corso\u2026'}</div>` : '');
// File corrente
const fileHtml = job.running && job.current_file ? `
<div class="mt-2 current-file-box mono">
<span class="current-file-label">copia</span>
<span class="current-file-name" title="${escHtml(job.current_file)}">${escHtml(job.current_file)}</span>
</div>
` : '';
col.innerHTML = `
<article class="job-card p-4">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<h3 class="h5 mb-1">${job.name}</h3>
<p class="mono small mb-1">bucket: ${job.bucket}</p>
<p class="mono small mb-0">locale: ${job.local_dir}</p>
</div>
<span class="status-chip ${statusClass(job.last_status, job.running)}">${statusText}</span>
</div>
<div class="mt-3 small text-secondary-emphasis">
<div>Ultimo avvio: ${fmtDate(job.last_start)}</div>
<div>Ultima fine: ${fmtDate(job.last_end)}</div>
</div>
${progressHtml}
${fileHtml}
<div class="mt-3 summary-box mono small">${fmtSummary(job.last_summary, job.running)}</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-outline-light btn-sm" data-job="${job.name}">Avvia ora</button>
</div>
</article>
`;
const runBtn = col.querySelector('button[data-job]');
runBtn.addEventListener('click', async () => {
runBtn.disabled = true;
try {
await fetch(`/api/run/${encodeURIComponent(job.name)}`, { method: 'POST' });
await reload();
} finally {
runBtn.disabled = false;
}
});
grid.appendChild(col);
});
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
let prevLogTime = null;
function renderLogs(logs) {
const tbody = document.querySelector('#logsTable tbody');
const entries = logs.slice(0, 200);
const newTopTime = entries.length > 0 ? entries[0].time : null;
const hasNew = newTopTime && newTopTime !== prevLogTime;
prevLogTime = newTopTime;
tbody.innerHTML = '';
entries.forEach((entry, i) => {
const tr = document.createElement('tr');
if (hasNew && i < 3) tr.classList.add('row-new');
tr.innerHTML = `
<td class="mono small">${fmtDate(entry.time)}</td>
<td><span class="pill level-${entry.level.toLowerCase()}">${entry.level}</span></td>
<td class="mono small">${entry.job || '-'}</td>
<td>${entry.message}</td>
<td class="mono small text-truncate" style="max-width:260px" title="${encodeURIComponent(JSON.stringify(entry.details || {})).replace(/'/g,"&apos;").replace(/%/g,'%')}">` +
JSON.stringify(entry.details || {}) +
`</td>
`;
tbody.appendChild(tr);
});
document.getElementById('logCount').textContent = entries.length + ' eventi';
if (hasNew) {
const scroll = document.querySelector('.log-scroll');
scroll.scrollTop = 0;
}
}
let pollTimer = null;
async function reload() {
try {
const data = await fetchStatus();
document.getElementById('serverTime').textContent = `Server UTC: ${fmtDate(data.server_time)}`;
renderJobs(data.jobs || []);
renderLogs(data.logs || []);
const anyRunning = (data.jobs || []).some(j => j.running);
const dot = document.getElementById('liveIndicator');
if (anyRunning) {
dot.classList.remove('d-none');
} else {
dot.classList.add('d-none');
}
// Polling veloce (1 s) durante sync attiva, lento (5 s) a riposo
const interval = anyRunning ? 1000 : 5000;
clearTimeout(pollTimer);
pollTimer = setTimeout(reload, interval);
} catch (err) {
document.getElementById('serverTime').textContent = `Errore: ${err.message}`;
pollTimer = setTimeout(reload, 5000);
}
}
document.getElementById('runAllBtn').addEventListener('click', async () => {
const btn = document.getElementById('runAllBtn');
btn.disabled = true;
try {
await fetch('/api/run-all', { method: 'POST' });
await reload();
} finally {
btn.disabled = false;
}
});
reload();
// il timer ricorsivo in reload() gestisce il polling adattivo
</script>
</body>
</html>