From 18c26e3cc24a522425d4d9be539b5eb339747869 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Wed, 18 Mar 2026 17:28:29 +0100 Subject: [PATCH] ++ fix --- app/main.py | 45 ++++++++++++++++++++++++++---- app/static/style.css | 60 ++++++++++++++++++++++++++++++++++++++++ app/templates/index.html | 39 +++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index ffb2e47..e34a769 100644 --- a/app/main.py +++ b/app/main.py @@ -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}", diff --git a/app/static/style.css b/app/static/style.css index b70698f..a0494cd 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -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 { diff --git a/app/templates/index.html b/app/templates/index.html index 9fc6054..5095f89 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -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 ? ` +
+
+ ${job.phase || 'in corso\u2026'} + ${done} / ${total}  (${pct}%) +
+
+
+ ` : (job.running ? `
${job.phase || 'in corso\u2026'}
` : ''); + + // File corrente + const fileHtml = job.running && job.current_file ? ` +
+ copia + ${escHtml(job.current_file)} +
+ ` : ''; col.innerHTML = `
@@ -123,6 +146,9 @@
Ultima fine: ${fmtDate(job.last_end)}
+ ${progressHtml} + ${fileHtml} +
${fmtSummary(job.last_summary, job.running)}
@@ -146,6 +172,10 @@ }); } + function escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } + 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