import asyncio import math import socket import time from dataclasses import fields 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 from app.sample import Sample router = APIRouter(prefix="/api", tags=["overview"]) RowAgg = dict[str, float | int | None] def _window(snap: list[Sample], 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 f in fields(sample): if f.name == "ts": continue v = getattr(sample, f.name) if isinstance(v, (int, float)) and not isinstance(v, bool): vals.setdefault(f.name, []).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].ts, row)) return out @router.get("/overview") async def overview(request: Request): store = request.app.state.store s = store.latest() or Sample() mem_total = s.mem_total or 0 vram_total = s.vram_total or 0 vram_used = s.vram_used c = { "cpu": s.cpu, "cpu_temp": s.cpu_temp, "load1": s.load1, "load5": s.load5, "load15": s.load15, "mem_used": s.mem_used, "mem_total": mem_total, "mem_pct": s.mem_pct, "swap_used": s.swap_used, "swap_total": s.swap_total or 0, "swap_pct": s.swap_pct, "gpu": s.gpu, "gpu_name": s.gpu_name, "gpu_temp": s.gpu_temp, "vram_used": vram_used, "vram_total": vram_total, "vram_pct": s.vram_pct or ((vram_used / vram_total * 100) if (vram_total and vram_used is not None) else None), "battery": s.battery, "battery_status": s.battery_status, "ac_online": s.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})