216 lines
7.7 KiB
HTML
216 lines
7.7 KiB
HTML
<!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">▶ Sincronizzazione in corso…</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;
|
|
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>
|
|
|
|
<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);
|
|
});
|
|
}
|
|
|
|
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,"'").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;
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
} catch (err) {
|
|
document.getElementById('serverTime').textContent = `Errore: ${err.message}`;
|
|
}
|
|
}
|
|
|
|
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();
|
|
setInterval(reload, 5000);
|
|
</script>
|
|
</body>
|
|
</html>
|