From a6a4149c29f21024052a97fb8a81eb4e93ec40c8 Mon Sep 17 00:00:00 2001 From: Smittix Date: Tue, 24 Feb 2026 18:40:23 +0000 Subject: [PATCH] Fix WeFax auto-scheduler: prevent silent timer death and connect SSE Timer threads now log on fire and catch all exceptions so scheduled captures never die silently. Frontend connects SSE when the scheduler is enabled (not only on manual Start) and polls /wefax/status every 10s as a fallback so the UI stays in sync with auto-fired captures. Co-Authored-By: Claude Opus 4.6 --- static/js/modes/wefax.js | 48 +++++++++++++++++++++++++++++++++++++++- utils/wefax_scheduler.py | 23 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/static/js/modes/wefax.js b/static/js/modes/wefax.js index 8bfdc7b..d9ae178 100644 --- a/static/js/modes/wefax.js +++ b/static/js/modes/wefax.js @@ -17,6 +17,8 @@ var WeFax = (function () { selectedStation: null, pollTimer: null, countdownInterval: null, + schedulerPollTimer: null, + schedulerEnabled: false, }; // ---- Scope state ---- @@ -59,6 +61,7 @@ var WeFax = (function () { disconnectSSE(); stopScope(); stopCountdownTimer(); + stopSchedulerPoll(); if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; @@ -210,7 +213,9 @@ var WeFax = (function () { state.running = false; updateButtons(false); setStatus('Stopping...'); - disconnectSSE(); + if (!state.schedulerEnabled) { + disconnectSSE(); + } fetch('/wefax/stop', { method: 'POST' }) .then(function (r) { return r.json(); }) @@ -983,6 +988,11 @@ var WeFax = (function () { var sidebar = document.getElementById('wefaxSidebarAutoSchedule'); if (strip) strip.checked = !!data.enabled; if (sidebar) sidebar.checked = !!data.enabled; + state.schedulerEnabled = !!data.enabled; + if (data.enabled) { + connectSSE(); + startSchedulerPoll(); + } }) .catch(function () { /* ignore */ }); } @@ -1024,6 +1034,9 @@ var WeFax = (function () { if (data.status === 'ok') { setStatus('Auto-capture enabled — ' + (data.scheduled_count || 0) + ' broadcasts scheduled'); syncSchedulerCheckboxes(true); + state.schedulerEnabled = true; + connectSSE(); + startSchedulerPoll(); } else { setStatus('Scheduler error: ' + (data.message || 'unknown')); syncSchedulerCheckboxes(false); @@ -1041,6 +1054,11 @@ var WeFax = (function () { .then(function () { setStatus('Auto-capture disabled'); syncSchedulerCheckboxes(false); + state.schedulerEnabled = false; + stopSchedulerPoll(); + if (!state.running) { + disconnectSSE(); + } }) .catch(function (err) { console.error('WeFax scheduler disable error:', err); @@ -1055,6 +1073,34 @@ var WeFax = (function () { } } + function startSchedulerPoll() { + stopSchedulerPoll(); + state.schedulerPollTimer = setInterval(function () { + fetch('/wefax/status') + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.running && !state.running) { + state.running = true; + updateButtons(true); + setStatus('Auto-capture in progress...'); + connectSSE(); + } else if (!data.running && state.running) { + state.running = false; + updateButtons(false); + loadImages(); + } + }) + .catch(function () { /* ignore poll errors */ }); + }, 10000); + } + + function stopSchedulerPoll() { + if (state.schedulerPollTimer) { + clearInterval(state.schedulerPollTimer); + state.schedulerPollTimer = null; + } + } + function syncSchedulerCheckboxes(enabled) { var strip = document.getElementById('wefaxStripAutoSchedule'); var sidebar = document.getElementById('wefaxSidebarAutoSchedule'); diff --git a/utils/wefax_scheduler.py b/utils/wefax_scheduler.py index 6877717..3f8d9e5 100644 --- a/utils/wefax_scheduler.py +++ b/utils/wefax_scheduler.py @@ -296,6 +296,11 @@ class WeFaxScheduler: sb._timer.daemon = True sb._timer.start() + logger.info( + "Scheduled capture: %s at %s UTC (fires in %.0fs)", + content, utc_time, delay, + ) + self._broadcasts.append(sb) logger.info( @@ -314,6 +319,24 @@ class WeFaxScheduler: self._refresh_timer.start() def _execute_capture(self, sb: ScheduledBroadcast) -> None: + """Execute capture for a scheduled broadcast (with error guard).""" + logger.info("Timer fired for broadcast: %s at %s", sb.content, sb.utc_time) + try: + self._execute_capture_inner(sb) + except Exception: + logger.exception( + "Unhandled exception in scheduled capture: %s at %s", + sb.content, sb.utc_time, + ) + sb.status = 'skipped' + self._emit_event({ + 'type': 'schedule_capture_skipped', + 'broadcast': sb.to_dict(), + 'reason': 'error', + 'detail': 'internal error — see server logs', + }) + + def _execute_capture_inner(self, sb: ScheduledBroadcast) -> None: """Execute capture for a scheduled broadcast.""" if not self._enabled or sb.status != 'scheduled': return