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,