feat: dashboard UI with real-time stats, light/dark mode, session metrics
This commit is contained in:
@@ -1,10 +1,83 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from io import StringIO
|
||||
|
||||
|
||||
class SyncSession:
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self.start_time = time.time()
|
||||
self.syncs_total = 0
|
||||
self.syncs_failed = 0
|
||||
self.syncs_skipped = 0
|
||||
self.total_added = 0
|
||||
self.total_updated = 0
|
||||
self.total_deleted = 0
|
||||
self.total_duration = 0.0
|
||||
self.total_latency_ms = 0.0
|
||||
self.non_skip_count = 0
|
||||
self.bandwidth_bytes = 0
|
||||
self.bandwidth_saved_bytes = 0
|
||||
self.history = deque(maxlen=50)
|
||||
|
||||
def record(self, ok: bool, duration: float, added: int, updated: int, deleted: int, skipped: bool, ics_latency_ms: float, msg: str, ics_download_size: int = 0) -> None:
|
||||
with self._lock:
|
||||
self.syncs_total += 1
|
||||
ts = time.strftime("%H:%M:%S", time.gmtime())
|
||||
if not ok:
|
||||
self.syncs_failed += 1
|
||||
if skipped:
|
||||
self.syncs_skipped += 1
|
||||
self.total_added += added
|
||||
self.total_updated += updated
|
||||
self.total_deleted += deleted
|
||||
if not skipped:
|
||||
self.non_skip_count += 1
|
||||
self.total_duration += duration
|
||||
self.total_latency_ms += ics_latency_ms
|
||||
if ics_download_size:
|
||||
self.bandwidth_bytes += ics_download_size
|
||||
self.history.append({
|
||||
"time": ts,
|
||||
"ok": ok,
|
||||
"duration": round(duration, 2),
|
||||
"added": added,
|
||||
"updated": updated,
|
||||
"deleted": deleted,
|
||||
"skipped": skipped,
|
||||
"latency_ms": round(ics_latency_ms),
|
||||
"msg": msg,
|
||||
})
|
||||
|
||||
def get_status(self) -> dict:
|
||||
with self._lock:
|
||||
uptime = time.time() - self.start_time
|
||||
avg_dur = round(self.total_duration / self.non_skip_count, 2) if self.non_skip_count > 0 else 0
|
||||
avg_lat = round(self.total_latency_ms / self.non_skip_count) if self.non_skip_count > 0 else 0
|
||||
return {
|
||||
"uptime_sec": round(uptime),
|
||||
"syncs_total": self.syncs_total,
|
||||
"syncs_failed": self.syncs_failed,
|
||||
"syncs_skipped": self.syncs_skipped,
|
||||
"total_added": self.total_added,
|
||||
"total_updated": self.total_updated,
|
||||
"total_deleted": self.total_deleted,
|
||||
"avg_duration": avg_dur,
|
||||
"avg_latency_ms": avg_lat,
|
||||
"bandwidth_bytes": self.bandwidth_bytes,
|
||||
"bandwidth_saved_bytes": self.bandwidth_saved_bytes,
|
||||
"history": list(self.history),
|
||||
}
|
||||
|
||||
def add_saved_bandwidth(self, bytes_saved: int) -> None:
|
||||
with self._lock:
|
||||
self.bandwidth_saved_bytes += bytes_saved
|
||||
|
||||
|
||||
class HealthServer:
|
||||
def __init__(self, port: int = 8081):
|
||||
self.port = port
|
||||
|
||||
Reference in New Issue
Block a user