import asyncio import math import socket import time from typing import Any import psutil from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse, JSONResponse from app.collect import net as net_col from app.config import get_settings from app.render import render, uptime_str router = APIRouter(prefix="/api", tags=["overview"]) RowAgg = dict[str, float | int | None] def _window( snap: list[tuple[float, dict[str, float | int | None]]], max_points: int ) -> list[tuple[float, dict[str, RowAgg]]]: n = len(snap) w = max(1, math.ceil(n / max_points)) out: list[tuple[float, dict[str, RowAgg]]] = [] for start in range(0, n, w): chunk = snap[start : start + w] vals: dict[str, list[int | float]] = {} for _, sample in chunk: for k, v in sample.items(): if isinstance(v, (int, float)) and not isinstance(v, bool): vals.setdefault(k, []).append(v) row: dict[str, RowAgg] = {} for k, lst in vals.items(): ints = all(isinstance(v, int) for v in lst) avg = sum(lst) / len(lst) row[k] = { "avg": round(avg) if ints else round(avg, 1), "min": min(lst) if ints else round(min(lst), 1), "max": max(lst) if ints else round(max(lst), 1), } out.append((chunk[-1][0], row)) return out @router.get("/overview") async def overview(request: Request): store = request.app.state.store s: dict[str, Any] = store.latest() or {} mem_total = s.get("mem_total") or 0 vram_total = s.get("vram_total") or 0 vram_used = s.get("vram_used") c = { "cpu": s.get("cpu"), "cpu_temp": s.get("cpu_temp"), "load1": s.get("load1"), "load5": s.get("load5"), "load15": s.get("load15"), "mem_used": s.get("mem_used"), "mem_total": mem_total, "mem_pct": s.get("mem_pct"), "swap_used": s.get("swap_used"), "swap_total": s.get("swap_total") or 0, "swap_pct": s.get("swap_pct"), "gpu": s.get("gpu"), "gpu_name": s.get("gpu_name"), "gpu_temp": s.get("gpu_temp"), "vram_used": vram_used, "vram_total": vram_total, "vram_pct": s.get("vram_pct") or ((vram_used / vram_total * 100) if (vram_total and vram_used is not None) else None), "battery": s.get("battery"), "battery_status": s.get("battery_status"), "ac_online": s.get("ac_online"), "uptime": uptime_str(time.time() - psutil.boot_time()), "hostname": socket.gethostname(), "cores": psutil.cpu_count(logical=True) or 1, **await asyncio.to_thread(net_col.sample), } return HTMLResponse(render("overview.html", c=c)) @router.get("/history") async def history(request: Request): snap = _window(request.app.state.store.snapshot(), get_settings().chart_max_points) ts = [round(t, 1) for t, _ in snap] keys: set[str] = set() for _, row in snap: keys.update(row) series: dict[str, dict[str, list[Any]]] = { k: {"avg": [], "min": [], "max": []} for k in keys } for _, row in snap: for k in keys: agg = row.get(k) for m in ("avg", "min", "max"): series[k][m].append(agg[m] if agg else None) return JSONResponse({"ts": ts, "series": series})