++ fix grafico

This commit is contained in:
2026-03-18 17:22:33 +01:00
parent 405b8d24bd
commit 2a249e16d4
2 changed files with 125 additions and 15 deletions

View File

@@ -124,6 +124,37 @@ body {
padding: 0.55rem 0.7rem; padding: 0.55rem 0.7rem;
min-height: 42px; min-height: 42px;
word-break: break-word; word-break: break-word;
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.6rem;
align-items: center;
}
.summary-kv {
display: inline-flex;
align-items: center;
gap: 0.25rem;
background: rgba(255, 255, 255, 0.06);
border-radius: 6px;
padding: 0.15rem 0.45rem;
font-size: 0.78rem;
}
.summary-key {
color: #8ecae6;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.sync-in-progress {
color: #ffe2a3;
animation: pulse-text 1.4s ease-in-out infinite alternate;
}
@keyframes pulse-text {
from { opacity: 0.6; }
to { opacity: 1; }
} }
.status-chip { .status-chip {
@@ -182,12 +213,57 @@ body {
.table thead { .table thead {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 1;
background: #0b1e2d;
} }
.table tbody tr { .table tbody tr {
border-bottom: 1px solid rgba(255, 255, 255, 0.06); border-bottom: 1px solid rgba(255, 255, 255, 0.06);
} }
.log-scroll {
max-height: 420px;
overflow-y: auto;
overflow-x: auto;
border-radius: 10px;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
.log-scroll::-webkit-scrollbar {
width: 6px;
}
.log-scroll::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.2);
border-radius: 4px;
}
@keyframes flash-row {
0% { background: rgba(255, 183, 3, 0.25); }
100% { background: transparent; }
}
.row-new {
animation: flash-row 1.8s ease forwards;
}
.live-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: #2ec4b6;
flex-shrink: 0;
animation: dot-pulse 1.2s ease-in-out infinite;
}
@keyframes dot-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.75); }
}
@media (max-width: 768px) { @media (max-width: 768px) {
.hero-panel, .hero-panel,
.log-panel { .log-panel {

View File

@@ -35,16 +35,22 @@
<section class="log-panel mt-4 p-3 p-md-4"> <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 justify-content-between mb-3">
<h2 class="h4 mb-0">Attivita Recenti</h2> <div class="d-flex align-items-center gap-2">
<span class="pill">UTC</span> <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>
<div class="table-responsive"> <div class="log-scroll">
<table class="table table-dark table-borderless align-middle mb-0" id="logsTable"> <table class="table table-dark table-borderless align-middle mb-0" id="logsTable">
<thead> <thead>
<tr> <tr>
<th>Ora</th> <th style="width:160px">Ora</th>
<th>Livello</th> <th style="width:90px">Livello</th>
<th>Job</th> <th style="width:130px">Job</th>
<th>Messaggio</th> <th>Messaggio</th>
<th>Dettagli</th> <th>Dettagli</th>
</tr> </tr>
@@ -69,15 +75,20 @@
return new Date(value).toLocaleString(); return new Date(value).toLocaleString();
} }
function fmtSummary(summary) { function fmtSummary(summary, running) {
if (!summary || Object.keys(summary).length === 0) { if (running) {
return 'Nessuna esecuzione'; 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 = []; const parts = [];
for (const [key, val] of Object.entries(summary)) { for (const [key, val] of Object.entries(summary)) {
parts.push(`${key}: ${val}`); const label = labels[key] || key;
parts.push(`<span class="summary-kv"><span class="summary-key">${label}</span> ${val}</span>`);
} }
return parts.join(' | '); return parts.join('');
} }
function statusClass(status, running) { function statusClass(status, running) {
@@ -112,7 +123,7 @@
<div>Ultima fine: ${fmtDate(job.last_end)}</div> <div>Ultima fine: ${fmtDate(job.last_end)}</div>
</div> </div>
<div class="mt-3 summary-box mono small">${fmtSummary(job.last_summary)}</div> <div class="mt-3 summary-box mono small">${fmtSummary(job.last_summary, job.running)}</div>
<div class="mt-3 d-flex gap-2"> <div class="mt-3 d-flex gap-2">
<button class="btn btn-outline-light btn-sm" data-job="${job.name}">Avvia ora</button> <button class="btn btn-outline-light btn-sm" data-job="${job.name}">Avvia ora</button>
@@ -135,21 +146,37 @@
}); });
} }
let prevLogTime = null;
function renderLogs(logs) { function renderLogs(logs) {
const tbody = document.querySelector('#logsTable tbody'); const tbody = document.querySelector('#logsTable tbody');
tbody.innerHTML = ''; const entries = logs.slice(0, 200);
const newTopTime = entries.length > 0 ? entries[0].time : null;
const hasNew = newTopTime && newTopTime !== prevLogTime;
prevLogTime = newTopTime;
logs.slice(0, 120).forEach((entry) => { tbody.innerHTML = '';
entries.forEach((entry, i) => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
if (hasNew && i < 3) tr.classList.add('row-new');
tr.innerHTML = ` tr.innerHTML = `
<td class="mono small">${fmtDate(entry.time)}</td> <td class="mono small">${fmtDate(entry.time)}</td>
<td><span class="pill level-${entry.level.toLowerCase()}">${entry.level}</span></td> <td><span class="pill level-${entry.level.toLowerCase()}">${entry.level}</span></td>
<td class="mono small">${entry.job || '-'}</td> <td class="mono small">${entry.job || '-'}</td>
<td>${entry.message}</td> <td>${entry.message}</td>
<td class="mono small">${JSON.stringify(entry.details || {})}</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); tbody.appendChild(tr);
}); });
document.getElementById('logCount').textContent = entries.length + ' eventi';
if (hasNew) {
const scroll = document.querySelector('.log-scroll');
scroll.scrollTop = 0;
}
} }
async function reload() { async function reload() {
@@ -158,6 +185,13 @@
document.getElementById('serverTime').textContent = `Server UTC: ${fmtDate(data.server_time)}`; document.getElementById('serverTime').textContent = `Server UTC: ${fmtDate(data.server_time)}`;
renderJobs(data.jobs || []); renderJobs(data.jobs || []);
renderLogs(data.logs || []); 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) { } catch (err) {
document.getElementById('serverTime').textContent = `Errore: ${err.message}`; document.getElementById('serverTime').textContent = `Errore: ${err.message}`;
} }