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:
Tran G. (Revernomad) Khoa
2026-04-17 20:33:04 +07:00
parent dedf082491
commit 4aae08420b
9 changed files with 459 additions and 82 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -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,