feat: Phase 2 — Mission rotation management + multi-file upload
- Backend: add terrain field to Arma3MissionManager.list_missions() - Backend: add missions field to ServerConfig Pydantic model - Backend: add GET /missions/rotation and PUT /missions/rotation endpoints - Frontend: Mission type gains terrain field; new MissionRotationEntry type - Frontend: useServerMissionRotation and useUpdateMissionRotation hooks - Frontend: useUploadMission updated to accept File[] with sequential upload - Frontend: MissionList redesigned with Available Missions + Mission Rotation sections - Frontend: per-file upload progress tracking, terrain badges, difficulty select - Tests: 5 new tests; fixed existing useUploadMission test for File[] API; 141 pass
This commit is contained in:
@@ -57,6 +57,7 @@ class ServerConfig(BaseModel):
|
||||
headless_clients: list[str] = Field(default_factory=list)
|
||||
local_clients: list[str] = Field(default_factory=list)
|
||||
admin_uids: list[str] = Field(default_factory=list)
|
||||
missions: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BasicConfig(BaseModel):
|
||||
|
||||
@@ -52,10 +52,12 @@ class Arma3MissionManager:
|
||||
try:
|
||||
for entry in missions_dir.iterdir():
|
||||
if entry.is_file() and entry.suffix.lower() == _ALLOWED_EXTENSION:
|
||||
parsed = self.parse_mission_filename(entry.name)
|
||||
missions.append({
|
||||
"name": entry.stem,
|
||||
"filename": entry.name,
|
||||
"size_bytes": entry.stat().st_size,
|
||||
"terrain": parsed["terrain"],
|
||||
})
|
||||
except OSError as exc:
|
||||
raise AdapterError(f"Cannot list missions: {exc}") from exc
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.engine import Connection
|
||||
|
||||
from adapters.exceptions import AdapterError
|
||||
@@ -20,6 +21,16 @@ router = APIRouter(prefix="/servers/{server_id}/missions", tags=["missions"])
|
||||
_MAX_UPLOAD_SIZE = 500 * 1024 * 1024 # 500 MB
|
||||
|
||||
|
||||
class MissionRotationEntry(BaseModel):
|
||||
name: str
|
||||
difficulty: str = ""
|
||||
|
||||
|
||||
class MissionRotationUpdate(BaseModel):
|
||||
missions: list[MissionRotationEntry]
|
||||
config_version: int
|
||||
|
||||
|
||||
def _ok(data):
|
||||
return {"success": True, "data": data, "error": None}
|
||||
|
||||
@@ -35,6 +46,35 @@ def _get_mission_manager(server_id: int, game_type: str):
|
||||
return adapter.get_mission_manager(server_id)
|
||||
|
||||
|
||||
@router.get("/rotation")
|
||||
def get_mission_rotation(
|
||||
server_id: int,
|
||||
db: Annotated[Connection, Depends(get_db)],
|
||||
_user: Annotated[dict, Depends(get_current_user)],
|
||||
) -> dict:
|
||||
"""Get the current mission rotation from the server config."""
|
||||
config = ServerService(db).get_config_section(server_id, "server")
|
||||
missions = config.get("missions", [])
|
||||
return _ok({"missions": missions})
|
||||
|
||||
|
||||
@router.put("/rotation")
|
||||
def update_mission_rotation(
|
||||
server_id: int,
|
||||
body: MissionRotationUpdate,
|
||||
db: Annotated[Connection, Depends(get_db)],
|
||||
_admin: Annotated[dict, Depends(require_admin)],
|
||||
) -> dict:
|
||||
"""Replace the mission rotation in the server config."""
|
||||
updated = ServerService(db).update_config_section(
|
||||
server_id=server_id,
|
||||
section="server",
|
||||
data={"missions": [e.model_dump() for e in body.missions]},
|
||||
expected_version=body.config_version,
|
||||
)
|
||||
return _ok({"missions": updated.get("missions", [])})
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_missions(
|
||||
server_id: int,
|
||||
|
||||
Reference in New Issue
Block a user