from typing import Any from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from app import journal from app.render import render router = APIRouter(prefix="/api", tags=["journal"]) @router.get("/journal") async def journal_view( _request: Request, level: str = "all", unit: str = "", search: str = "", cursor: str = "", hide_sudo: str = "", ): """Render the Journal tab fragment: a page of journal entries. Without a cursor it fetches 100 lines; with one (continuing a scroll) 200, then keeps the newest 400 for the template. level is validated against journal.LEVELS, and failures (RuntimeError/OSError from journalctl) are rendered as an error banner instead of a 500. Args: _request: FastAPI request (unused). level: "all" / "warn" / "err". unit: unit name filter, empty for none. search: free-text filter, empty for none. cursor: journal cursor to continue after, empty for none. hide_sudo: "on" to hide sudo's own log entries. Returns: The rendered journal.html as an HTMLResponse. """ if level not in journal.LEVELS: level = "all" lines = 200 if cursor else 100 error = None entries: list[dict[str, Any]] = [] next_cursor = "" try: entries, nc = await journal.tail( cursor or None, level, unit or None, search or None, lines, hide_sudo=(hide_sudo == "on"), ) next_cursor = nc or "" entries = entries[-400:] except (RuntimeError, OSError) as e: error = str(e)[:300] return HTMLResponse( render( "journal.html", entries=entries, next_cursor=next_cursor, level=level, unit=unit, search=search, error=error, ) )