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

182 lines
6.0 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">
<h2 class="h4 mb-0">Attivita Recenti</h2>
<span class="pill">UTC</span>
</div>
<div class="table-responsive">
<table class="table table-dark table-borderless align-middle mb-0" id="logsTable">
<thead>
<tr>
<th>Ora</th>
<th>Livello</th>
<th>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) {
if (!summary || Object.keys(summary).length === 0) {
return 'Nessuna esecuzione';
}
const parts = [];
for (const [key, val] of Object.entries(summary)) {
parts.push(`${key}: ${val}`);
}
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)}</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 renderLogs(logs) {
const tbody = document.querySelector('#logsTable tbody');
tbody.innerHTML = '';
logs.slice(0, 120).forEach((entry) => {
const tr = document.createElement('tr');
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">${JSON.stringify(entry.details || {})}</td>
`;
tbody.appendChild(tr);
});
}
async function reload() {
try {
const data = await fetchStatus();
document.getElementById('serverTime').textContent = `Server UTC: ${fmtDate(data.server_time)}`;
renderJobs(data.jobs || []);
renderLogs(data.logs || []);
} 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>