import asyncio 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 from app.sample import Sample from app.utils.window import window router = APIRouter(prefix="/api", tags=["overview"]) @router.get("/overview") async def overview(request: Request): """Render the Overview tab fragment: current system state card. Takes the latest sample from the history store (an empty Sample when none exists yet), derives vram_pct when the collector left it unset, and adds interface / wifi data and uptime. Args: request: FastAPI request (app.state.store). Returns: The rendered overview.html as an HTMLResponse. """ 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): """Serve the ring buffer as chart data (JSON). The buffer is window-averaged via app.utils.window.window() down to at most `chart_max_points` points. Every key seen in any window gets avg/min/max arrays, and each array is padded with None for windows that lack the key (e.g. the GPU fields before a GPU is detected) so the arrays stay aligned with the ts array — the charts rely on that. Args: request: FastAPI request (app.state.store). Returns: JSON with ts (unix seconds) and series: key to {avg, min, max}. """ 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})