feat: implement full backend + frontend server detail, settings, and create server pages
Backend: - Complete FastAPI backend with 42+ REST endpoints (auth, servers, config, players, bans, missions, mods, games, system) - Game adapter architecture with Arma 3 as first-class adapter - WebSocket real-time events for status, metrics, logs, players - Background thread system (process monitor, metrics, log tail, RCon poller) - Fernet encryption for sensitive config fields at rest - JWT auth with admin/viewer roles, bcrypt password hashing - SQLite with WAL mode, parameterized queries, migration system - APScheduler cleanup jobs for logs, metrics, events Frontend: - Server Detail page with 7 tabs (overview, config, players, bans, missions, mods, logs) - Settings page with password change and admin user management - Create Server wizard (4-step; known bug: silent validation failure) - New hooks: useServerDetail, useAuth, useGames - New components: ServerHeader, ConfigEditor, PlayerTable, BanTable, MissionList, ModList, LogViewer, PasswordChange, UserManager - WebSocket onEvent callback for real-time log accumulation - 120 unit tests passing (Vitest + React Testing Library) Docs: - Added .gitignore, CLAUDE.md, README.md - Updated FRONTEND.md, ARCHITECTURE.md with current implementation state - Added .env.example for backend configuration Known issues: - Create Server form: "Next" buttons don't validate before advancing, causing silent submit failure when fields are invalid - Config sub-tabs need UX redesign for non-technical users
This commit is contained in:
186
backend/main.py
Normal file
186
backend/main.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
FastAPI application factory.
|
||||
Entry point: uvicorn main:app --reload
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import queue
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from config import settings
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.log_level.upper(), logging.INFO),
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Startup + shutdown logic."""
|
||||
# ── Startup ──
|
||||
logger.info("Starting Languard...")
|
||||
|
||||
# 1. Init DB and run migrations
|
||||
from database import get_engine, run_migrations
|
||||
engine = get_engine()
|
||||
run_migrations(engine)
|
||||
|
||||
# 2. Register adapters
|
||||
from adapters import initialize_adapters
|
||||
initialize_adapters()
|
||||
|
||||
# 3. Create WebSocket manager (asyncio-only)
|
||||
from core.websocket.manager import WebSocketManager
|
||||
ws_manager = WebSocketManager()
|
||||
app.state.ws_manager = ws_manager
|
||||
|
||||
# 4. Create global broadcast queue and BroadcastThread
|
||||
broadcast_queue = queue.Queue(maxsize=1000)
|
||||
app.state.broadcast_queue = broadcast_queue
|
||||
|
||||
from core.websocket.broadcast_thread import BroadcastThread
|
||||
loop = asyncio.get_event_loop()
|
||||
broadcast_thread = BroadcastThread(
|
||||
event_queue=broadcast_queue,
|
||||
ws_manager=ws_manager,
|
||||
loop=loop,
|
||||
)
|
||||
broadcast_thread.start()
|
||||
app.state.broadcast_thread = broadcast_thread
|
||||
|
||||
# 5. Create ThreadRegistry
|
||||
from core.threads.thread_registry import ThreadRegistry
|
||||
from core.servers.process_manager import ProcessManager
|
||||
from adapters.registry import GameAdapterRegistry
|
||||
|
||||
process_manager = ProcessManager.get()
|
||||
thread_registry = ThreadRegistry(
|
||||
process_manager=process_manager,
|
||||
adapter_registry=GameAdapterRegistry,
|
||||
global_broadcast_queue=broadcast_queue,
|
||||
)
|
||||
ThreadRegistry.set_instance(thread_registry)
|
||||
app.state.thread_registry = thread_registry
|
||||
|
||||
# 6. Recover processes that survived a restart
|
||||
process_manager.recover_on_startup(engine.connect())
|
||||
|
||||
# 7. Reattach threads for running servers
|
||||
from core.dal.server_repository import ServerRepository
|
||||
with engine.connect() as db:
|
||||
server_repo = ServerRepository(db)
|
||||
running_servers = server_repo.get_running()
|
||||
for server in running_servers:
|
||||
try:
|
||||
thread_registry.reattach_server_threads(server["id"], db)
|
||||
logger.info("Reattached threads for server %d", server["id"])
|
||||
except Exception as exc:
|
||||
logger.error("Failed to reattach threads for server %d: %s", server["id"], exc)
|
||||
|
||||
# 8. Seed default admin if no users exist
|
||||
from core.auth.service import AuthService
|
||||
with engine.connect() as db:
|
||||
svc = AuthService(db)
|
||||
generated_password = svc.seed_admin_if_empty()
|
||||
db.commit()
|
||||
if generated_password:
|
||||
logger.warning("=" * 60)
|
||||
logger.warning(" FIRST RUN — default admin created")
|
||||
logger.warning(" Username: admin")
|
||||
logger.warning(" Password: %s", generated_password)
|
||||
logger.warning(" Change this password immediately!")
|
||||
logger.warning("=" * 60)
|
||||
|
||||
# 9. Register and start APScheduler cleanup jobs
|
||||
from core.jobs.scheduler import start_scheduler, stop_scheduler
|
||||
from core.jobs.cleanup_jobs import register_cleanup_jobs
|
||||
register_cleanup_jobs()
|
||||
start_scheduler()
|
||||
|
||||
yield
|
||||
|
||||
# ── Shutdown ──
|
||||
logger.info("Shutting down Languard...")
|
||||
try:
|
||||
ThreadRegistry.stop_all()
|
||||
except Exception as e:
|
||||
logger.error("Thread shutdown error: %s", e)
|
||||
broadcast_thread.stop()
|
||||
broadcast_thread.join(timeout=5.0)
|
||||
|
||||
from core.jobs.scheduler import stop_scheduler
|
||||
stop_scheduler()
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title="Languard Server Manager",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# ── Middleware ──
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ── Global exception handler ──
|
||||
@app.exception_handler(Exception)
|
||||
async def generic_exception_handler(request: Request, exc: Exception):
|
||||
logger.error("Unhandled exception: %s", exc, exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"success": False,
|
||||
"data": None,
|
||||
"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"},
|
||||
},
|
||||
)
|
||||
|
||||
# ── Routers ──
|
||||
from core.auth.router import router as auth_router
|
||||
from core.games.router import router as games_router
|
||||
from core.system.router import router as system_router
|
||||
from core.servers.router import router as servers_router
|
||||
from core.servers.players_router import router as players_router
|
||||
from core.servers.bans_router import router as bans_router
|
||||
from core.servers.missions_router import router as missions_router
|
||||
from core.servers.mods_router import router as mods_router
|
||||
from core.websocket.router import router as ws_router
|
||||
|
||||
app.include_router(auth_router, prefix="/api")
|
||||
app.include_router(games_router, prefix="/api")
|
||||
app.include_router(system_router, prefix="/api")
|
||||
app.include_router(servers_router, prefix="/api")
|
||||
app.include_router(players_router, prefix="/api")
|
||||
app.include_router(bans_router, prefix="/api")
|
||||
app.include_router(missions_router, prefix="/api")
|
||||
app.include_router(mods_router, prefix="/api")
|
||||
app.include_router(ws_router)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
Reference in New Issue
Block a user