Files
languard-servers-manager/backend/core/servers/missions_router.py
Tran G. (Revernomad) Khoa 6511353b55 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
2026-04-17 11:58:34 +07:00

115 lines
3.8 KiB
Python

"""Mission management endpoints — list, upload, delete mission files."""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
from sqlalchemy.engine import Connection
from adapters.exceptions import AdapterError
from adapters.registry import GameAdapterRegistry
from core.servers.service import ServerService
from database import get_db
from dependencies import get_current_user, require_admin
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/servers/{server_id}/missions", tags=["missions"])
_MAX_UPLOAD_SIZE = 500 * 1024 * 1024 # 500 MB
def _ok(data):
return {"success": True, "data": data, "error": None}
def _get_mission_manager(server_id: int, game_type: str):
"""Get MissionManager for the server's game type."""
adapter = GameAdapterRegistry.get(game_type)
if not adapter.has_capability("mission_manager"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"code": "NOT_SUPPORTED", "message": f"Game type '{game_type}' does not support mission management"},
)
return adapter.get_mission_manager(server_id)
@router.get("")
def list_missions(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
"""List all available mission files on disk."""
server = ServerService(db).get_server(server_id) # raises 404 if not found
mgr = _get_mission_manager(server_id, server["game_type"])
try:
missions = mgr.list_missions()
except AdapterError as exc:
raise HTTPException(status_code=500, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
return _ok({
"server_id": server_id,
"missions": missions,
"total": len(missions),
})
@router.post("", status_code=status.HTTP_201_CREATED)
async def upload_mission(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
file: UploadFile = File(...),
) -> dict:
"""
Upload a mission .pbo file.
Max size: 500 MB.
"""
server = ServerService(db).get_server(server_id) # raises 404 if not found
if not file.filename:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"code": "NO_FILENAME", "message": "No filename provided"},
)
content = await file.read()
if len(content) > _MAX_UPLOAD_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail={"code": "FILE_TOO_LARGE", "message": f"File too large. Max size is {_MAX_UPLOAD_SIZE // (1024*1024)} MB"},
)
mgr = _get_mission_manager(server_id, server["game_type"])
try:
mission = mgr.upload_mission(file.filename, content)
except AdapterError as exc:
raise HTTPException(status_code=400, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
return _ok(mission)
@router.delete("/{filename}")
def delete_mission(
server_id: int,
filename: str,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
) -> dict:
"""Delete a mission file by filename."""
server = ServerService(db).get_server(server_id) # raises 404 if not found
mgr = _get_mission_manager(server_id, server["game_type"])
try:
deleted = mgr.delete_mission(filename)
except AdapterError as exc:
raise HTTPException(status_code=400, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "NOT_FOUND", "message": f"Mission '{filename}' not found"},
)
return _ok({"message": f"Mission '{filename}' deleted"})