Package Manager (new app): - Search, install, remove apt packages via web UI - Backend: apt-cache/dpkg-query/apt-get wrapper with input validation - Frontend: searchable package list with expandable detail panels Text Editor / File Viewer (new app): - Opens from file manager, supports text editing with line numbers - Image preview via authenticated blob URLs - Binary file info display with download option - Ctrl+S / Cmd+S save, dirty tracking, tab key support File Manager enhancements: - Toolbar: New File, New Folder, Upload, Delete, Refresh buttons - Context menu: New File/Folder options, Open in Editor - Double-click files to open in editor - Right-click empty area for create options Auto-update notification: - Backend checks git repo for new commits (fetch + compare) - One-click update: git pull + pip install + service restart - Toast notification in right panel with dismiss option - Polls every 30 minutes, retry logic for server restart Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
140 lines
4.3 KiB
Python
140 lines
4.3 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, 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",
|
|
)
|