++ fix
This commit is contained in:
45
app/main.py
45
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}",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,'&').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
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user