From 27d6b598e822a748e8fcb985a3738d93b5c4582c Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Wed, 17 Jun 2026 12:02:50 +0200 Subject: [PATCH] feat: next run countdown in auto-cycle config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auto_cycle.py: next_run_at now set to future timestamp (now + interval) instead of current time — was always showing wrong value - Config.tsx: NextRunCountdown component shows live countdown (updates every second) + absolute local time, only visible when auto-cycle enabled Co-Authored-By: Claude Sonnet 4.6 --- backend/services/auto_cycle.py | 5 +++-- frontend/src/pages/Config.tsx | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py index a0b1cb4..443bf01 100644 --- a/backend/services/auto_cycle.py +++ b/backend/services/auto_cycle.py @@ -744,9 +744,10 @@ def _scheduler_loop(stop_event: threading.Event): interval_hours = 3.0 _current_status["interval_hours"] = interval_hours - next_run = datetime.utcnow().isoformat() + from datetime import timedelta + next_run = (datetime.utcnow() + timedelta(hours=interval_hours)).isoformat() _current_status["next_run_at"] = next_run - logger.info(f"[Scheduler] Next cycle in {interval_hours}h") + logger.info(f"[Scheduler] Next cycle in {interval_hours}h (at {next_run[:16]} UTC)") # Wait for the interval (or until stop is signalled) stop_event.wait(timeout=interval_hours * 3600) diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 64dc0cb..67157f0 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -39,6 +39,34 @@ const PROFILE_COLORS = [ { value: '#eab308', label: 'Jaune' }, ] +function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) { + const [remaining, setRemaining] = useState('') + + useEffect(() => { + function update() { + const diff = new Date(nextRunAt + 'Z').getTime() - Date.now() + if (diff <= 0) { setRemaining('Imminent…'); return } + const h = Math.floor(diff / 3_600_000) + const m = Math.floor((diff % 3_600_000) / 60_000) + const s = Math.floor((diff % 60_000) / 1_000) + setRemaining(h > 0 ? `${h}h ${m.toString().padStart(2,'0')}min` : `${m}min ${s.toString().padStart(2,'0')}s`) + } + update() + const id = setInterval(update, 1_000) + return () => clearInterval(id) + }, [nextRunAt]) + + const absTime = new Date(nextRunAt + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + + return ( +
+ + Prochain cycle dans {remaining} + ({absTime} heure locale) +
+ ) +} + function evNetLabel(evNet: number): { text: string; cls: string } { if (evNet > 0.05) return { text: `EV nette +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' } if (evNet >= -0.01) return { text: 'EV nette ≈ 0', cls: 'text-yellow-400' } @@ -692,7 +720,7 @@ export default function Config() { {cs?.last_cycle && ( -
+
Dernier cycle : {cs.last_cycle.started_at?.slice(0, 16)} UTC @@ -704,6 +732,9 @@ export default function Config() { Géo: {cs.last_cycle.geo_score} {cs.last_cycle.dominant_regime}
+ {cs.enabled && cs.next_run_at && !cs.running && ( + + )}
)}