import asyncio import socket from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from app.config import get_settings from app.plugins import PLUGINS, ROUTERS as PLUGIN_ROUTERS from app.render import BASE, render from app.routers import disks, overview, plugins, processes, services from app.routers import journal as journal_router from app.sampling import sampler_loop from app.state import HistoryStore @asynccontextmanager async def lifespan(app: FastAPI): """Start shared state and run plugin lifecycle hooks around the app. Startup: stores settings and the history ring buffer on `app.state`, opens every plugin (a plugin `open()` failure is ignored, not fatal), and spawns the background sampler task. Shutdown: cancels the sampler task and closes every plugin. Args: app: the FastAPI instance. Yields: Control to the ASGI app for the server's lifetime. """ settings = get_settings() app.state.settings = settings app.state.store = HistoryStore(maxlen=settings.history_maxlen) for p in PLUGINS: try: await p.open() except Exception: # noqa pass task = asyncio.create_task(sampler_loop(app.state.store, settings.sample_interval)) yield _ = task.cancel() try: await task except asyncio.CancelledError: pass for p in PLUGINS: try: await p.close() except Exception: # noqa pass async def index(): """Serve the single-page dashboard shell at "/". The shell only holds the tab bar and containers; each tab polls its own `/api/*` endpoint for content, so this renders once and never again. Returns: The rendered `index.html` as an HTMLResponse. """ return HTMLResponse(render("index.html", hostname=socket.gethostname())) def create_app() -> FastAPI: """Build the FastAPI application. Wires up the `/static` mount, the six core routers (overview, disks, processes, journal, services, plugins), and the routers contributed by each plugin (see `app/plugins/__init__.py`). Returns: The configured FastAPI instance. """ app = FastAPI(title="Dashboard", lifespan=lifespan) app.mount("/static", StaticFiles(directory=BASE / "static"), name="static") for r in (overview.router, disks.router, processes.router, journal_router.router, services.router, plugins.router): app.include_router(r) for r in PLUGIN_ROUTERS: app.include_router(r) _ = app.get("/", include_in_schema=False)(index) return app app = create_app()