Files
s3_to_folder_sync/app/templates/index.html
2026-03-18 17:22:33 +01:00

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">&#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;
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,"&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;
}
}
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>