atlus/backend/main.py
roberts 4631ebc07a Add network configuration to Settings (DHCP/static, WiFi scan/connect)
Full NetworkManager (nmcli) backed network management:
- Backend: new network.py router with endpoints for device status,
  connection config, IPv4 DHCP/static toggle, WiFi scan/connect/disconnect
- Frontend: interactive network settings UI with per-device config,
  WiFi network list with signal strength, inline password input
- Graceful 503 fallback to read-only psutil view when nmcli unavailable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:29:35 -05:00

138 lines
4.2 KiB
Python

"""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
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(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",
)