"""Atlus — main FastAPI application entry point.""" import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from backend.auth import authenticate_user, create_token, logout from backend.config import FRONTEND_DIR, HOST, PORT from backend.routers import stats, terminal, files, services, processes, settings, network, packages, updates from backend.routers.plugins import asi_bridge logging.basicConfig( level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%H:%M:%S", ) log = logging.getLogger("atlus") # --------------------------------------------------------------------------- # Lifespan — start/stop background tasks # --------------------------------------------------------------------------- @asynccontextmanager async def lifespan(app: FastAPI): log.info("Atlus starting on %s:%d", HOST, PORT) # Start stats broadcaster broadcaster = asyncio.create_task(stats.stats_broadcaster()) yield broadcaster.cancel() try: await broadcaster except asyncio.CancelledError: pass log.info("Atlus shutdown complete") # --------------------------------------------------------------------------- # App # --------------------------------------------------------------------------- app = FastAPI( title="Atlus", description="Web-based desktop environment for headless SBCs", version="0.1.0", lifespan=lifespan, ) # --------------------------------------------------------------------------- # Auth endpoints (not behind auth — they *create* auth) # --------------------------------------------------------------------------- class LoginRequest(BaseModel): username: str password: str @app.post("/api/auth/login") async def login(req: LoginRequest): if not authenticate_user(req.username, req.password): raise HTTPException(401, "Invalid credentials") token, jti = create_token(req.username) return {"token": token, "username": req.username} @app.post("/api/auth/logout") async def logout_endpoint(request: Request): auth = request.headers.get("Authorization", "") if auth.startswith("Bearer "): logout(auth[7:]) return {"ok": True} # --------------------------------------------------------------------------- # API routers # --------------------------------------------------------------------------- app.include_router(stats.router) app.include_router(terminal.router) app.include_router(files.router) app.include_router(services.router) app.include_router(processes.router) app.include_router(settings.router) app.include_router(network.router) app.include_router(packages.router) app.include_router(updates.router) app.include_router(asi_bridge.router) # --------------------------------------------------------------------------- # Frontend — served as static files, SPA-style routing # --------------------------------------------------------------------------- # Mount static assets (CSS, JS, images) app.mount("/css", StaticFiles(directory=str(FRONTEND_DIR / "css")), name="css") app.mount("/js", StaticFiles(directory=str(FRONTEND_DIR / "js")), name="js") app.mount("/assets", StaticFiles(directory=str(FRONTEND_DIR / "assets")), name="assets") @app.get("/") async def serve_login(): return FileResponse(str(FRONTEND_DIR / "index.html")) @app.get("/desktop") async def serve_desktop(): return FileResponse(str(FRONTEND_DIR / "desktop.html")) # --------------------------------------------------------------------------- # Error handler # --------------------------------------------------------------------------- @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): log.exception("Unhandled error: %s", exc) return JSONResponse( status_code=500, content={"detail": "Internal server error"}, ) # --------------------------------------------------------------------------- # CLI entry point # --------------------------------------------------------------------------- if __name__ == "__main__": import uvicorn uvicorn.run( "backend.main:app", host=HOST, port=PORT, reload=False, log_level="info", )