Task 1: Move BUILTIN_COMMANDERS + load_commander_meta() to shared.py
Fix import_routes.py to use _shared.COMMANDER_META (module ref)
Task 2: Rewrite precon.py to use _shared.PRECON_INDEX (module ref)
Task 3: Add @app.on_event('startup') safety net for uvicorn --reload
Adds an @app.on_event('startup') hook that imports routes._wire.
This means lab_api.py needs zero changes — the routers self-register
when the app boots. Drop this file into the repo root and it works.
Alternatively, add to lab_api.py after app creation:
from routes import _autoload # noqa
Called via `import routes._wire` at the bottom of lab_api.py (after all
symbols are defined). Registers every shared object into routes.shared
and mounts all five APIRouters onto the FastAPI app.
Batch 6/6 — final wiring step.
Two fixes:
1. _parse_text_decklist(): replace fragile startswith() section-header
detection with case-insensitive regex so [Commander], [Main], [Deck]
etc. are always recognised regardless of capitalisation.
2. add_deck_card endpoint: when a card is added with is_commander=1,
also update the deck-level commander_name + commander_scryfall_id
so the portrait and top-bar label stay in sync.
Two fixes:
1. _parse_text_decklist(): replace fragile startswith() section header
detection with proper case-insensitive regex so [Commander], [Main],
[Deck] etc. are always recognised regardless of capitalisation.
2. add_deck_card endpoint: when a card is added with is_commander=1,
also update the deck-level commander_name and commander_scryfall_id
fields so the portrait and top-bar label stay in sync without
requiring a full reload.
- run_n() accepts 2-4 decks; run() wraps for backward compat
- _select_attack_target() heuristic for multiplayer combat targeting
- N-player elimination order tracking with finish positions
- Removal scans all opponents for scariest creature
- ML snapshot captures all N players
- Log entries use per-seat boards dict with legacy boardA/boardB for 2-player
Two places lost the commander flag:
1. _load_deck_cards_by_name() .dck fallback — used a flat in_cards
boolean for [Main], [Deck], and [Commander] sections so it couldn't
tell which section a card came from. Now tracks current_section and
sets is_commander=1 for cards in [Commander].
2. parse_dck_file() — stored section='Commander' in the card dict but
the frontend filters on c.is_commander, not c.section. Now includes
is_commander: 1/0 alongside the existing section field.
Replace all print() calls with structured logging using the Python
logging module. This provides:
- Named loggers per subsystem: commander_ai_lab.api, .batch, .sim,
.coach, .deckgen, .collection, .scanner, .ml, .cache, .pplx
- Dual output: console (StreamHandler) + rotating file (RotatingFileHandler)
at logs/commander-ai-lab.log (25 MB x 5 backups = 125 MB max)
- Configurable via env vars: CAL_LOG_DIR, CAL_LOG_MAX_MB, CAL_LOG_BACKUP_COUNT
- --verbose / -v flag for DEBUG-level output
- overnight-run.py: separate rotating log at logs/overnight-run.log (10 MB x 5)
- Sub-module loggers in ml/, scanner/, deck_sources/, src/ all propagate
to the root commander_ai_lab logger
- Stripped redundant [TAG] prefixes from messages (logger name carries context)
- Format: '2026-03-13 14:00:00 INFO [commander_ai_lab.batch] message'
Files changed: lab_api.py, overnight-run.py, and 11 sub-module .py files
176 total print() calls converted (101 in lab_api.py, 76 in sub-modules)
GameResult.to_dict() now returns both legacy keys (winner, playerA, playerB)
that the UI expects AND new N-player keys (winningSeat, playerResults) for
the Java BatchResult schema.