diff --git a/dashboard.py b/dashboard.py index 11520f0..0f36efb 100644 --- a/dashboard.py +++ b/dashboard.py @@ -1,15 +1,20 @@ import json import os import threading +import time from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn -from health import HealthServer, SyncSession +from health import SyncSession + + +class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): + daemon_threads = True class DashboardServer: - def __init__(self, port: int = 8082, health: HealthServer = None, session: SyncSession = None): + def __init__(self, port: int = 8082, session: SyncSession = None): self.port = port - self.health = health self.session = session self.server = None self.thread = None @@ -19,11 +24,15 @@ class DashboardServer: self._event_count = 0 self._backoff_min = 0 self._config = {} + self._last_sync = None + self._last_duration = 0.0 + self._last_success = None + self._last_latency_ms = 0 self._base_dir = os.path.dirname(os.path.abspath(__file__)) def start(self): handler = self._make_handler() - self.server = HTTPServer(("0.0.0.0", self.port), handler) + self.server = ThreadingHTTPServer(("0.0.0.0", self.port), handler) self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() @@ -53,6 +62,13 @@ class DashboardServer: with self._lock: self._backoff_min = n + def set_last_sync(self, last_sync, duration: float, success: bool, latency_ms: int = 0): + with self._lock: + self._last_sync = last_sync + self._last_duration = duration + self._last_success = success + self._last_latency_ms = latency_ms + def _get_status(self): with self._lock: syncing = self._syncing @@ -60,15 +76,10 @@ class DashboardServer: event_count = self._event_count backoff_min = self._backoff_min config = dict(self._config) - - last_sync = None - duration = 0.0 - last_success = None - if self.health: - with self.health.lock: - last_sync = self.health.last_sync - duration = self.health.last_sync_duration - last_success = self.health.last_sync_success + last_sync = self._last_sync + duration = self._last_duration + last_success = self._last_success + latency_ms = self._last_latency_ms status = "idle" if syncing: @@ -92,7 +103,7 @@ class DashboardServer: "status": status, "last_sync": last_sync.isoformat() if last_sync else None, "duration": duration, - "ics_latency_ms": ics_latency, + "ics_latency_ms": ics_latency if ics_latency else latency_ms, "event_count": event_count, "next_sync_in": next_sync_in, "session": session_data, diff --git a/docker-compose.yml b/docker-compose.yml index 0477a35..6670ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,6 @@ services: container_name: baikal-sync restart: always ports: - - "8081:8081" - "8082:8082" environment: - ICS_URL=${ICS_URL} diff --git a/static/dashboard.html b/static/dashboard.html index a5e34f4..2d31a41 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -333,6 +333,8 @@ header { lastPoll: null }; + let countdownRemaining = 0; + const $ = (sel) => document.querySelector(sel); function fmtBytes(b) { @@ -502,7 +504,7 @@ header { $("#stackedLegend").innerHTML = '
Added: ' + added + "
" + '
Updated: ' + updated + "
" + - '
Deleted: " + deleted + "
" + + '
Deleted: ' + deleted + "
" + '
Skipped: ' + skipped + "
"; } @@ -673,14 +675,12 @@ header { const fill = $("#progressFill"); if (!el || !fill) return; - let remaining = (state.data.next_sync_in || 0) - 1; - if (remaining < 0) remaining = 0; - el.textContent = fmtCountdown(remaining); + countdownRemaining = Math.max(0, countdownRemaining - 1); + el.textContent = fmtCountdown(countdownRemaining); const freq = state.data.config ? state.data.config.sync_frequency * 60 : 600; - const pct = freq > 0 ? Math.max(0, Math.min(100, ((freq - remaining) / freq) * 100)) : 100; + const pct = freq > 0 ? Math.max(0, Math.min(100, ((freq - countdownRemaining) / freq) * 100)) : 100; fill.style.width = pct + "%"; - state.data.next_sync_in = remaining; } async function poll() { @@ -689,6 +689,12 @@ header { if (!res.ok) throw new Error("HTTP " + res.status); state.data = await res.json(); state.lastPoll = Date.now(); + if (state.data.last_sync && state.data.config && state.data.config.sync_frequency) { + const syncEpoch = new Date(state.data.last_sync).getTime(); + const freqSec = state.data.config.sync_frequency * 60; + const now = Date.now(); + countdownRemaining = Math.max(0, Math.round(((syncEpoch + freqSec * 1000) - now) / 1000)); + } renderStatusBar(state.data); renderStats(state.data); renderStackedBar(state.data); diff --git a/sync_calendar.py b/sync_calendar.py index 7ce64de..4d8c11e 100644 --- a/sync_calendar.py +++ b/sync_calendar.py @@ -89,6 +89,7 @@ def sync_once( msg = "no changes (ETag)" session.record(True, duration, 0, 0, 0, True, 0, msg) health.update_status(datetime.now(timezone.utc), duration, True, 0) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, True, 0) dashboard.set_syncing(False) return True @@ -112,6 +113,7 @@ def sync_once( if ics_download_size: pass health.update_status(datetime.now(timezone.utc), duration, True, 0) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, True, ics_latency_ms) dashboard.set_syncing(False) return True @@ -141,6 +143,7 @@ def sync_once( for uid, h in ics_uids.items(): state.upsert_event(uid, h) health.update_status(datetime.now(timezone.utc), duration, True, len(ics_uids)) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, True, ics_latency_ms) dashboard.set_event_count(len(ics_uids)) dashboard.set_syncing(False) return True @@ -156,6 +159,7 @@ def sync_once( msg = f"first run, registered {len(to_add)} events" session.record(True, duration, len(to_add), 0, 0, False, ics_latency_ms, msg, ics_download_size) health.update_status(datetime.now(timezone.utc), duration, True, len(ics_uids)) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, True, ics_latency_ms) dashboard.set_event_count(len(ics_uids)) dashboard.set_syncing(False) return True @@ -181,6 +185,7 @@ def sync_once( msg = "calendar not found" session.record(False, duration, 0, 0, 0, False, ics_latency_ms, msg, ics_download_size) health.update_status(datetime.now(timezone.utc), duration, False, 0) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, False, ics_latency_ms) dashboard.set_syncing(False) return False @@ -226,6 +231,7 @@ def sync_once( session.record(True, duration, added, updated, deleted, False, ics_latency_ms, msg, ics_download_size) logger.info("Sync completed in %.1fs. Total events: %d", duration, total) health.update_status(datetime.now(timezone.utc), duration, True, total) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, True, ics_latency_ms) dashboard.set_event_count(total) dashboard.set_syncing(False) return True @@ -237,6 +243,7 @@ def sync_once( msg = str(exc)[:80] session.record(False, duration, 0, 0, 0, False, ics_latency_ms, msg, ics_download_size) health.update_status(datetime.now(timezone.utc), duration, False, 0) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, False, ics_latency_ms) dashboard.set_syncing(False) return False @@ -246,6 +253,7 @@ def sync_once( msg = str(exc)[:80] session.record(False, duration, 0, 0, 0, False, ics_latency_ms, msg, ics_download_size) health.update_status(datetime.now(timezone.utc), duration, False, 0) + dashboard.set_last_sync(datetime.now(timezone.utc), duration, False, ics_latency_ms) dashboard.set_syncing(False) return False @@ -267,7 +275,7 @@ def main(): logger.info("Health endpoint on :8081") session = SyncSession() - dashboard = DashboardServer(8082, health, session) + dashboard = DashboardServer(8082, session) dashboard.update_config({ "ics_url": config.ics_url, "baikal_url": config.baikal_url,