- GET /api/lab/result: Starlette's JSONResponse uses allow_nan=False,
so NaN/Infinity floats from simulation output cause a ValueError crash.
Add _sanitize_floats() to replace non-finite floats with null before
returning. Also wrap json.load in try/except for better error reporting.
- POST /api/lab/precons/install-batch: Frontend sends {"names": [...]}
but backend expected {"fileNames": [...]}. Accept both field names.
- POST /api/lab/precons/install: Same mismatch — frontend sends {"name"}
but backend expected {"fileName"}. Accept both field names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
97 lines
3.1 KiB
Python
97 lines
3.1 KiB
Python
"""
|
|
routes/precon.py
|
|
================
|
|
Precon deck management endpoints:
|
|
GET /api/lab/precons
|
|
POST /api/lab/precons/install
|
|
POST /api/lab/precons/install-batch
|
|
POST /api/lab/precons/refresh
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
import routes.shared as _shared # module reference, not value import
|
|
from routes.shared import CFG, download_precon_database, log
|
|
from services.precon_service import _get_precon_dir
|
|
|
|
router = APIRouter(tags=["precon"])
|
|
|
|
|
|
@router.get("/api/lab/precons")
|
|
async def list_precons():
|
|
"""List all available precon decks."""
|
|
return {"precons": _shared.PRECON_INDEX} # reads current value every call
|
|
|
|
|
|
@router.post("/api/lab/precons/install")
|
|
async def install_precon(req: dict):
|
|
"""Install a precon deck to the Forge decks directory.
|
|
Body: {"fileName": "Elven_Empire.dck"} or {"name": "Elven_Empire.dck"}
|
|
"""
|
|
file_name = req.get("fileName", "") or req.get("name", "")
|
|
if not file_name:
|
|
raise HTTPException(400, "fileName is required")
|
|
|
|
src = _get_precon_dir() / file_name
|
|
if not src.exists():
|
|
raise HTTPException(404, f"Precon not found: {file_name}")
|
|
|
|
decks_dir = CFG.forge_decks_dir
|
|
if not decks_dir or not os.path.isdir(decks_dir):
|
|
raise HTTPException(500, f"Forge decks directory not found: {decks_dir}")
|
|
|
|
dst = Path(decks_dir) / file_name
|
|
shutil.copy2(str(src), str(dst))
|
|
|
|
deck_name = file_name.replace(".dck", "")
|
|
return {
|
|
"installed": True,
|
|
"deckName": deck_name,
|
|
"destination": str(dst),
|
|
"message": f"Installed {deck_name} to Forge decks",
|
|
}
|
|
|
|
|
|
@router.post("/api/lab/precons/install-batch")
|
|
async def install_precons_batch(req: dict):
|
|
"""Install multiple precon decks at once.
|
|
Body: {"fileNames": ["Elven_Empire.dck", ...]} or {"names": ["Elven_Empire.dck", ...]}
|
|
"""
|
|
file_names = req.get("fileNames", []) or req.get("names", [])
|
|
if not file_names:
|
|
raise HTTPException(400, "fileNames list is required")
|
|
|
|
decks_dir = CFG.forge_decks_dir
|
|
if not decks_dir or not os.path.isdir(decks_dir):
|
|
raise HTTPException(500, f"Forge decks directory not found: {decks_dir}")
|
|
|
|
results = []
|
|
for file_name in file_names:
|
|
src = _get_precon_dir() / file_name
|
|
if not src.exists():
|
|
results.append({"fileName": file_name, "installed": False, "error": "not found"})
|
|
continue
|
|
dst = Path(decks_dir) / file_name
|
|
shutil.copy2(str(src), str(dst))
|
|
deck_name = file_name.replace(".dck", "")
|
|
results.append({"fileName": file_name, "installed": True, "deckName": deck_name})
|
|
|
|
return {"results": results}
|
|
|
|
|
|
@router.post("/api/lab/precons/refresh")
|
|
async def refresh_precons():
|
|
"""Force re-download of all Commander precon decks from GitHub."""
|
|
result = download_precon_database(force=True)
|
|
if result.get("error"):
|
|
raise HTTPException(502, result["error"])
|
|
return {
|
|
"message": f"Downloaded {result['downloaded']} Commander precon decks",
|
|
"total": result.get("total", 0),
|
|
}
|