mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-22 04:50:46 +02:00
247 lines
8.7 KiB
Python
247 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Request, Form, UploadFile, File
|
|
from fastapi.responses import HTMLResponse
|
|
from pathlib import Path
|
|
import os
|
|
import json
|
|
from ..app import templates
|
|
from ..services.build_utils import owned_set as owned_set_helper, owned_names as owned_names_helper
|
|
from ..services.summary_utils import summary_ctx
|
|
from ..services import orchestrator as orch
|
|
|
|
|
|
router = APIRouter(prefix="/configs")
|
|
|
|
|
|
def _config_dir() -> Path:
|
|
# Prefer explicit env var if provided, else default to ./config
|
|
p = os.getenv("DECK_CONFIG")
|
|
if p:
|
|
# If env points to a file, use its parent dir; else treat as dir
|
|
pp = Path(p)
|
|
return (pp.parent if pp.suffix else pp).resolve()
|
|
return (Path.cwd() / "config").resolve()
|
|
|
|
|
|
def _list_configs() -> list[dict]:
|
|
d = _config_dir()
|
|
try:
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
except Exception:
|
|
pass
|
|
items: list[dict] = []
|
|
for p in sorted(d.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True):
|
|
meta = {"name": p.name, "path": str(p), "mtime": p.stat().st_mtime}
|
|
try:
|
|
with p.open("r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
meta["commander"] = data.get("commander")
|
|
tags = [t for t in [data.get("primary_tag"), data.get("secondary_tag"), data.get("tertiary_tag")] if t]
|
|
meta["tags"] = tags
|
|
meta["bracket_level"] = data.get("bracket_level")
|
|
except Exception:
|
|
pass
|
|
items.append(meta)
|
|
return items
|
|
|
|
|
|
@router.get("/", response_class=HTMLResponse)
|
|
async def configs_index(request: Request) -> HTMLResponse:
|
|
items = _list_configs()
|
|
# Load example deck.json from the config directory, if present
|
|
example_json = None
|
|
example_name = "deck.json"
|
|
try:
|
|
example_path = _config_dir() / example_name
|
|
if example_path.exists() and example_path.is_file():
|
|
example_json = example_path.read_text(encoding="utf-8")
|
|
except Exception:
|
|
example_json = None
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": items, "example_json": example_json, "example_name": example_name},
|
|
)
|
|
|
|
|
|
@router.get("/view", response_class=HTMLResponse)
|
|
async def configs_view(request: Request, name: str) -> HTMLResponse:
|
|
base = _config_dir()
|
|
p = (base / name).resolve()
|
|
# Safety: ensure the resolved path is within config dir
|
|
try:
|
|
if base not in p.parents and p != base:
|
|
raise ValueError("Access denied")
|
|
except Exception:
|
|
pass
|
|
if not (p.exists() and p.is_file() and p.suffix.lower() == ".json"):
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": _list_configs(), "error": "Config not found."},
|
|
)
|
|
try:
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception as e:
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": _list_configs(), "error": f"Failed to read JSON: {e}"},
|
|
)
|
|
return templates.TemplateResponse(
|
|
"configs/view.html",
|
|
{"request": request, "path": str(p), "name": p.name, "data": data},
|
|
)
|
|
|
|
|
|
@router.post("/run", response_class=HTMLResponse)
|
|
async def configs_run(request: Request, name: str = Form(...), use_owned_only: str | None = Form(None)) -> HTMLResponse:
|
|
base = _config_dir()
|
|
p = (base / name).resolve()
|
|
try:
|
|
if base not in p.parents and p != base:
|
|
raise ValueError("Access denied")
|
|
except Exception:
|
|
pass
|
|
if not (p.exists() and p.is_file() and p.suffix.lower() == ".json"):
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": _list_configs(), "error": "Config not found."},
|
|
)
|
|
try:
|
|
cfg = json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception as e:
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": _list_configs(), "error": f"Failed to read JSON: {e}"},
|
|
)
|
|
|
|
commander = cfg.get("commander", "")
|
|
tags = [t for t in [cfg.get("primary_tag"), cfg.get("secondary_tag"), cfg.get("tertiary_tag")] if t]
|
|
bracket = int(cfg.get("bracket_level") or 0)
|
|
ideals = cfg.get("ideal_counts", {}) or {}
|
|
# Optional combine mode for tags (AND/OR); support a few aliases
|
|
try:
|
|
tag_mode = (str(cfg.get("tag_mode") or cfg.get("combine_mode") or cfg.get("mode") or "AND").upper())
|
|
if tag_mode not in ("AND", "OR"):
|
|
tag_mode = "AND"
|
|
except Exception:
|
|
tag_mode = "AND"
|
|
|
|
# Optional owned-only for headless runs via JSON flag or form override
|
|
owned_flag = False
|
|
try:
|
|
uo = cfg.get("use_owned_only")
|
|
if isinstance(uo, bool):
|
|
owned_flag = uo
|
|
elif isinstance(uo, str):
|
|
owned_flag = uo.strip().lower() in ("1","true","yes","on")
|
|
except Exception:
|
|
owned_flag = False
|
|
|
|
# Form override takes precedence if provided
|
|
if use_owned_only is not None:
|
|
owned_flag = str(use_owned_only).strip().lower() in ("1","true","yes","on")
|
|
|
|
owned_names = owned_names_helper() if owned_flag else None
|
|
|
|
# Optional combos preferences
|
|
prefer_combos = False
|
|
try:
|
|
pc = cfg.get("prefer_combos")
|
|
if isinstance(pc, bool):
|
|
prefer_combos = pc
|
|
elif isinstance(pc, str):
|
|
prefer_combos = pc.strip().lower() in ("1","true","yes","on")
|
|
except Exception:
|
|
prefer_combos = False
|
|
combo_target_count = None
|
|
try:
|
|
ctc = cfg.get("combo_target_count")
|
|
if isinstance(ctc, int):
|
|
combo_target_count = ctc
|
|
elif isinstance(ctc, str) and ctc.strip().isdigit():
|
|
combo_target_count = int(ctc.strip())
|
|
except Exception:
|
|
combo_target_count = None
|
|
combo_balance = None
|
|
try:
|
|
cb = cfg.get("combo_balance")
|
|
if isinstance(cb, str) and cb.strip().lower() in ("early","late","mix"):
|
|
combo_balance = cb.strip().lower()
|
|
except Exception:
|
|
combo_balance = None
|
|
|
|
# Run build headlessly with orchestrator
|
|
res = orch.run_build(
|
|
commander=commander,
|
|
tags=tags,
|
|
bracket=bracket,
|
|
ideals=ideals,
|
|
tag_mode=tag_mode,
|
|
use_owned_only=owned_flag,
|
|
owned_names=owned_names,
|
|
# Thread combo prefs through staged headless run
|
|
prefer_combos=prefer_combos,
|
|
combo_target_count=combo_target_count,
|
|
combo_balance=combo_balance,
|
|
)
|
|
if not res.get("ok"):
|
|
return templates.TemplateResponse(
|
|
"configs/run_result.html",
|
|
{
|
|
"request": request,
|
|
"ok": False,
|
|
"error": res.get("error") or "Build failed",
|
|
"log": res.get("log", ""),
|
|
"cfg_name": p.name,
|
|
"commander": commander,
|
|
"tag_mode": tag_mode,
|
|
"use_owned_only": owned_flag,
|
|
"owned_set": owned_set_helper(),
|
|
},
|
|
)
|
|
ctx = {
|
|
"request": request,
|
|
"ok": True,
|
|
"log": res.get("log", ""),
|
|
"csv_path": res.get("csv_path"),
|
|
"txt_path": res.get("txt_path"),
|
|
"summary": res.get("summary"),
|
|
"cfg_name": p.name,
|
|
"commander": commander,
|
|
"tag_mode": tag_mode,
|
|
"use_owned_only": owned_flag,
|
|
}
|
|
ctx.update(summary_ctx(summary=res.get("summary"), commander=commander, tags=tags))
|
|
return templates.TemplateResponse("configs/run_result.html", ctx)
|
|
|
|
|
|
|
|
@router.post("/upload", response_class=HTMLResponse)
|
|
async def configs_upload(request: Request, file: UploadFile = File(...)) -> HTMLResponse:
|
|
# Optional helper: allow uploading a JSON config
|
|
try:
|
|
content = await file.read()
|
|
data = json.loads(content.decode("utf-8"))
|
|
# Minimal validation
|
|
if not data.get("commander"):
|
|
raise ValueError("Missing 'commander'")
|
|
except Exception as e:
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": _list_configs(), "error": f"Invalid JSON: {e}"},
|
|
)
|
|
# Save to config dir with original filename (or unique)
|
|
d = _config_dir()
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
fname = file.filename or "config.json"
|
|
out = d / fname
|
|
i = 1
|
|
while out.exists():
|
|
stem = out.stem
|
|
out = d / f"{stem}_{i}.json"
|
|
i += 1
|
|
out.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
return templates.TemplateResponse(
|
|
"configs/index.html",
|
|
{"request": request, "items": _list_configs(), "notice": f"Uploaded {out.name}"},
|
|
)
|