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>
138 lines
4.2 KiB
Python
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",
|
|
)
|