Files
baikal-sync/dashboard.py
T

158 lines
5.2 KiB
Python

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