This commit is contained in:
2026-03-18 17:28:29 +01:00
parent 2a249e16d4
commit 18c26e3cc2
3 changed files with 138 additions and 6 deletions

View File

@@ -51,6 +51,10 @@ class JobState:
last_status: str = "never"
last_summary: dict[str, Any] = field(default_factory=dict)
running: bool = False
current_file: str | None = None
files_total: int = 0
files_done: int = 0
phase: str = ""
job_configs: list[JobConfig] = []
@@ -279,6 +283,10 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
state = job_states[job.name]
state.running = True
state.last_start = started_at
state.current_file = None
state.files_total = 0
state.files_done = 0
state.phase = "elencazione file"
notify_gotify(
title=f"Sync started: {job.name}",
@@ -299,6 +307,10 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
paginator = s3_client.get_paginator("list_objects_v2")
expected_rel_paths: set[str] = set()
pending_downloads: list[tuple[str, str, Path, int, float]] = []
with status_lock:
job_states[job.name].phase = "elencazione file"
for page in paginator.paginate(Bucket=job.bucket, Prefix=job.prefix):
contents = page.get("Contents", [])
@@ -324,13 +336,28 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
s3_ts = obj["LastModified"].timestamp()
if should_download(local_path, s3_size, s3_ts):
s3_client.download_file(job.bucket, key, str(local_path))
os.utime(local_path, (s3_ts, s3_ts))
downloaded += 1
pending_downloads.append((key, rel_key, local_path, s3_size, s3_ts))
else:
kept += 1
with status_lock:
job_states[job.name].files_total = len(pending_downloads)
job_states[job.name].files_done = 0
job_states[job.name].phase = "download"
for key, rel_key, local_path, s3_size, s3_ts in pending_downloads:
with status_lock:
job_states[job.name].current_file = rel_key
s3_client.download_file(job.bucket, key, str(local_path))
os.utime(local_path, (s3_ts, s3_ts))
downloaded += 1
with status_lock:
job_states[job.name].files_done = downloaded
if job.delete_local_extras:
with status_lock:
job_states[job.name].phase = "pulizia file extra"
job_states[job.name].current_file = None
for file_path in local_dir.rglob("*"):
if not file_path.is_file():
continue
@@ -354,8 +381,12 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
state.last_end = ended_at
state.last_status = "success"
state.last_summary = summary
state.current_file = None
state.files_total = 0
state.files_done = 0
state.phase = ""
add_log("info", "Sync completed", job.name, summary)
add_log("info", "Sync completata", job.name, summary)
notify_gotify(
title=f"Sync completed: {job.name}",
message=(
@@ -383,8 +414,12 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
state.last_end = ended_at
state.last_status = "failed"
state.last_summary = summary
state.current_file = None
state.files_total = 0
state.files_done = 0
state.phase = ""
add_log("error", "Sync failed", job.name, {"error": str(exc)})
add_log("error", "Sync fallita", job.name, {"error": str(exc)})
notify_gotify(
title=f"Sync failed: {job.name}",
message=f"error: {exc}",

View File

@@ -264,6 +264,66 @@ body {
50% { opacity: 0.4; transform: scale(0.75); }
}
.progress-track {
height: 6px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.1);
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--ok), #4de8dd);
transition: width 0.4s ease;
min-width: 4px;
}
.progress-phase {
color: #ffe2a3;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.current-file-box {
display: flex;
align-items: baseline;
gap: 0.5rem;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 183, 3, 0.25);
border-radius: 8px;
padding: 0.35rem 0.6rem;
font-size: 0.78rem;
overflow: hidden;
}
.current-file-label {
color: #ffb703;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.6px;
flex-shrink: 0;
}
.current-file-name {
color: #e0f7fa;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* scorre il nome da destra a sinistra se troppo lungo */
animation: marquee-file 12s linear infinite;
display: inline-block;
max-width: 100%;
}
@keyframes marquee-file {
0% { transform: translateX(0); }
40% { transform: translateX(0); }
90% { transform: translateX(calc(-100% + 200px)); }
100% { transform: translateX(0); }
}
@media (max-width: 768px) {
.hero-panel,
.log-panel {

View File

@@ -107,6 +107,29 @@
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">
@@ -123,6 +146,9 @@
<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">
@@ -146,6 +172,10 @@
});
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
let prevLogTime = null;
function renderLogs(logs) {
@@ -179,6 +209,8 @@
}
}
let pollTimer = null;
async function reload() {
try {
const data = await fetchStatus();
@@ -192,8 +224,13 @@
} 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);
}
}
@@ -209,7 +246,7 @@
});
reload();
setInterval(reload, 5000);
// il timer ricorsivo in reload() gestisce il polling adattivo
</script>
</body>
</html>