import asyncio from fastapi import APIRouter from fastapi.responses import HTMLResponse from app.collect import procs as proc_col from app.render import render router = APIRouter(prefix="/api", tags=["processes"]) SORT_KEYS = ("pid", "name", "cpu", "rss", "mem_pct", "io_read", "io_write", "gpu") @router.get("/processes") async def processes(q: str = "", sort: str = "cpu", order: str = "desc"): """Render the Processes tab fragment: filterable, sortable process table. The full sample is taken in a worker thread, then optionally filtered by substring match on name or exact match on pid. Sorting is done with None values last (the tuple key trick); at most 300 rows are rendered. Invalid sort/order values fall back to cpu/desc. Args: q: search filter, empty for all. sort: column to sort by, one of SORT_KEYS. order: "asc" or "desc". Returns: The rendered processes.html as an HTMLResponse. """ if sort not in SORT_KEYS: sort = "cpu" if order not in ("asc", "desc"): order = "desc" procs = await asyncio.to_thread(proc_col.sample) if q: ql = q.lower() procs = [p for p in procs if ql in p["name"].lower() or str(p["pid"]) == ql] reverse = order == "desc" try: procs.sort(key=lambda p: (p[sort] is None, p[sort]), reverse=reverse) except (KeyError, TypeError): pass return HTMLResponse( render( "processes.html", procs=procs[:300], total=len(procs), q=q, sort=sort, order=order, ) )