import json import os import threading import time from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn from health import SyncSession class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): daemon_threads = True class DashboardServer: def __init__(self, port: int = 8082, session: SyncSession = None): self.port = port self.session = session self.server = None self.thread = None self._lock = threading.Lock() self._syncing = False self._next_sync_in = 0 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 = ThreadingHTTPServer(("0.0.0.0", self.port), handler) self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() def stop(self): if self.server: self.server.shutdown() self.server = None self.thread = None def update_config(self, config_dict: dict): with self._lock: self._config = dict(config_dict) def set_syncing(self, syncing: bool): with self._lock: self._syncing = syncing def set_next_sync_in(self, seconds: int): with self._lock: self._next_sync_in = seconds def set_event_count(self, n: int): with self._lock: self._event_count = n def set_backoff_min(self, n: int): 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 next_sync_in = self._next_sync_in event_count = self._event_count backoff_min = self._backoff_min config = dict(self._config) last_sync = self._last_sync duration = self._last_duration last_success = self._last_success latency_ms = self._last_latency_ms status = "idle" if syncing: status = "syncing" elif last_success is False: status = "error" elif backoff_min > 0: status = "backoff" session_data = {} history = [] if self.session: session_data = self.session.get_status() history = session_data.get("history", []) ics_latency = 0.0 if self.session and self.session.non_skip_count > 0: ics_latency = round(self.session.total_latency_ms / self.session.non_skip_count) return { "status": status, "last_sync": last_sync.isoformat() if last_sync else None, "duration": duration, "ics_latency_ms": ics_latency if ics_latency else latency_ms, "event_count": event_count, "next_sync_in": next_sync_in, "session": session_data, "history": history, "config": config, } def _make_handler(self): server_self = self base_dir = server_self._base_dir class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/api/status": self._handle_api(server_self) elif self.path == "/": self._handle_dashboard(base_dir) else: self.send_response(404) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"error": "not found"}).encode()) def _handle_api(self, srv): data = srv._get_status() body = json.dumps(data).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(body) def _handle_dashboard(self, base): html_path = os.path.join(base, "static", "dashboard.html") try: with open(html_path, "r", encoding="utf-8") as f: content = f.read() body = content.encode("utf-8") self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(body) except FileNotFoundError: self.send_response(404) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"error": "dashboard not found"}).encode()) def log_message(self, format, *args): pass return Handler