++ 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_status: str = "never"
|
||||||
last_summary: dict[str, Any] = field(default_factory=dict)
|
last_summary: dict[str, Any] = field(default_factory=dict)
|
||||||
running: bool = False
|
running: bool = False
|
||||||
|
current_file: str | None = None
|
||||||
|
files_total: int = 0
|
||||||
|
files_done: int = 0
|
||||||
|
phase: str = ""
|
||||||
|
|
||||||
|
|
||||||
job_configs: list[JobConfig] = []
|
job_configs: list[JobConfig] = []
|
||||||
@@ -279,6 +283,10 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
|
|||||||
state = job_states[job.name]
|
state = job_states[job.name]
|
||||||
state.running = True
|
state.running = True
|
||||||
state.last_start = started_at
|
state.last_start = started_at
|
||||||
|
state.current_file = None
|
||||||
|
state.files_total = 0
|
||||||
|
state.files_done = 0
|
||||||
|
state.phase = "elencazione file"
|
||||||
|
|
||||||
notify_gotify(
|
notify_gotify(
|
||||||
title=f"Sync started: {job.name}",
|
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")
|
paginator = s3_client.get_paginator("list_objects_v2")
|
||||||
|
|
||||||
expected_rel_paths: set[str] = set()
|
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):
|
for page in paginator.paginate(Bucket=job.bucket, Prefix=job.prefix):
|
||||||
contents = page.get("Contents", [])
|
contents = page.get("Contents", [])
|
||||||
@@ -324,13 +336,28 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
|
|||||||
s3_ts = obj["LastModified"].timestamp()
|
s3_ts = obj["LastModified"].timestamp()
|
||||||
|
|
||||||
if should_download(local_path, s3_size, s3_ts):
|
if should_download(local_path, s3_size, s3_ts):
|
||||||
s3_client.download_file(job.bucket, key, str(local_path))
|
pending_downloads.append((key, rel_key, local_path, s3_size, s3_ts))
|
||||||
os.utime(local_path, (s3_ts, s3_ts))
|
|
||||||
downloaded += 1
|
|
||||||
else:
|
else:
|
||||||
kept += 1
|
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:
|
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("*"):
|
for file_path in local_dir.rglob("*"):
|
||||||
if not file_path.is_file():
|
if not file_path.is_file():
|
||||||
continue
|
continue
|
||||||
@@ -354,8 +381,12 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
|
|||||||
state.last_end = ended_at
|
state.last_end = ended_at
|
||||||
state.last_status = "success"
|
state.last_status = "success"
|
||||||
state.last_summary = summary
|
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(
|
notify_gotify(
|
||||||
title=f"Sync completed: {job.name}",
|
title=f"Sync completed: {job.name}",
|
||||||
message=(
|
message=(
|
||||||
@@ -383,8 +414,12 @@ def sync_job(job: JobConfig, trigger: str = "scheduled") -> None:
|
|||||||
state.last_end = ended_at
|
state.last_end = ended_at
|
||||||
state.last_status = "failed"
|
state.last_status = "failed"
|
||||||
state.last_summary = summary
|
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(
|
notify_gotify(
|
||||||
title=f"Sync failed: {job.name}",
|
title=f"Sync failed: {job.name}",
|
||||||
message=f"error: {exc}",
|
message=f"error: {exc}",
|
||||||
|
|||||||
@@ -264,6 +264,66 @@ body {
|
|||||||
50% { opacity: 0.4; transform: scale(0.75); }
|
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) {
|
@media (max-width: 768px) {
|
||||||
.hero-panel,
|
.hero-panel,
|
||||||
.log-panel {
|
.log-panel {
|
||||||
|
|||||||
@@ -107,6 +107,29 @@
|
|||||||
col.className = 'col-12 col-lg-6';
|
col.className = 'col-12 col-lg-6';
|
||||||
|
|
||||||
const statusText = job.running ? 'in esecuzione' : job.last_status;
|
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 = `
|
col.innerHTML = `
|
||||||
<article class="job-card p-4">
|
<article class="job-card p-4">
|
||||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
<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>Ultima fine: ${fmtDate(job.last_end)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${progressHtml}
|
||||||
|
${fileHtml}
|
||||||
|
|
||||||
<div class="mt-3 summary-box mono small">${fmtSummary(job.last_summary, job.running)}</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">
|
||||||
@@ -146,6 +172,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
let prevLogTime = null;
|
let prevLogTime = null;
|
||||||
|
|
||||||
function renderLogs(logs) {
|
function renderLogs(logs) {
|
||||||
@@ -179,6 +209,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pollTimer = null;
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchStatus();
|
const data = await fetchStatus();
|
||||||
@@ -192,8 +224,13 @@
|
|||||||
} else {
|
} else {
|
||||||
dot.classList.add('d-none');
|
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) {
|
} catch (err) {
|
||||||
document.getElementById('serverTime').textContent = `Errore: ${err.message}`;
|
document.getElementById('serverTime').textContent = `Errore: ${err.message}`;
|
||||||
|
pollTimer = setTimeout(reload, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +246,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
setInterval(reload, 5000);
|
// il timer ricorsivo in reload() gestisce il polling adattivo
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user