Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
from __future__ import annotations
2026-03-17 16:34:50 -07:00
from fastapi import APIRouter , Request , Query
from fastapi . responses import HTMLResponse
from typing import Any
2025-10-07 15:56:57 -07:00
import json
2026-03-17 16:34:50 -07:00
from urllib . parse import urlparse
from html import escape as _esc
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
from . . app import templates
from . . services . tasks import get_session , new_sid
2026-03-17 16:34:50 -07:00
from . . services . telemetry import log_commander_create_deck
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
2025-10-06 09:17:59 -07:00
2025-10-08 11:38:30 -07:00
def _merge_hx_trigger ( response : Any , payload : dict [ str , Any ] ) - > None :
if not payload or response is None :
return
try :
existing = response . headers . get ( " HX-Trigger " ) if hasattr ( response , " headers " ) else None
except Exception :
existing = None
try :
if existing :
try :
data = json . loads ( existing )
except Exception :
data = { }
if isinstance ( data , dict ) :
data . update ( payload )
response . headers [ " HX-Trigger " ] = json . dumps ( data )
return
response . headers [ " HX-Trigger " ] = json . dumps ( payload )
except Exception :
try :
response . headers [ " HX-Trigger " ] = json . dumps ( payload )
except Exception :
pass
def _step5_summary_placeholder_html ( token : int , * , message : str | None = None ) - > str :
text = message or " Deck summary will appear after the build completes. "
return (
f ' <div id= " deck-summary " data-summary '
f ' hx-get= " /build/step5/summary?token= { token } " '
2025-10-08 20:59:51 -07:00
' hx-trigger= " step5:refresh from:body " hx-swap= " outerHTML " > '
2025-10-08 11:38:30 -07:00
f ' <div class= " muted " style= " margin-top:1rem; " > { _esc ( text ) } </div> '
' </div> '
)
def _current_builder_summary ( sess : dict ) - > Any | None :
try :
ctx = sess . get ( " build_ctx " ) or { }
builder = ctx . get ( " builder " ) if isinstance ( ctx , dict ) else None
if builder is None :
return None
summary_fn = getattr ( builder , " build_deck_summary " , None )
if callable ( summary_fn ) :
2026-03-17 16:34:50 -07:00
summary_data = summary_fn ( )
# Also save to session for consistency
if summary_data :
sess [ " summary " ] = summary_data
return summary_data
2025-10-08 11:38:30 -07:00
except Exception :
return None
return None
2025-10-06 09:17:59 -07:00
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
router = APIRouter ( prefix = " /build " )
2025-08-29 09:19:03 -07:00
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
@router.get ( " / " , response_class = HTMLResponse )
async def build_index ( request : Request ) - > HTMLResponse :
sid = request . cookies . get ( " sid " ) or new_sid ( )
sess = get_session ( sid )
2025-09-30 15:49:08 -07:00
# Seed commander from query string when arriving from commander browser
q_commander = None
try :
q_commander = request . query_params . get ( " commander " )
if q_commander :
# Persist a human-friendly commander name into session for the wizard
sess [ " commander " ] = str ( q_commander )
2025-10-28 16:17:55 -07:00
# Set flag to indicate this is a quick-build scenario
sess [ " quick_build " ] = True
2025-09-30 15:49:08 -07:00
except Exception :
pass
return_url = None
try :
raw_return = request . query_params . get ( " return " )
if raw_return :
parsed = urlparse ( raw_return )
if not parsed . scheme and not parsed . netloc and parsed . path :
safe_path = parsed . path if parsed . path . startswith ( " / " ) else f " / { parsed . path } "
safe_return = safe_path
if parsed . query :
safe_return + = f " ? { parsed . query } "
if parsed . fragment :
safe_return + = f " # { parsed . fragment } "
return_url = safe_return
except Exception :
return_url = None
if q_commander :
try :
log_commander_create_deck (
request ,
commander = str ( q_commander ) ,
return_url = return_url ,
)
except Exception :
pass
2025-08-26 20:00:07 -07:00
# Determine last step (fallback heuristics if not set)
last_step = sess . get ( " last_step " )
if not last_step :
if sess . get ( " build_ctx " ) :
last_step = 5
elif sess . get ( " ideals " ) :
last_step = 4
elif sess . get ( " bracket " ) :
last_step = 3
elif sess . get ( " commander " ) :
last_step = 2
else :
last_step = 1
2025-10-28 16:17:55 -07:00
# Only pass commander to template if coming from commander browser (?commander= query param)
# This prevents stale commander from being pre-filled on subsequent builds
# The query param only exists on initial navigation from commander browser
should_auto_fill = q_commander is not None
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
resp = templates . TemplateResponse (
2025-09-12 10:50:57 -07:00
request ,
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
" build/index.html " ,
2025-08-26 20:00:07 -07:00
{
" sid " : sid ,
2025-10-28 16:17:55 -07:00
" commander " : sess . get ( " commander " ) if should_auto_fill else None ,
2025-08-26 20:00:07 -07:00
" tags " : sess . get ( " tags " , [ ] ) ,
2025-08-28 14:57:22 -07:00
" name " : sess . get ( " custom_export_base " ) ,
2025-08-26 20:00:07 -07:00
" last_step " : last_step ,
2025-09-30 15:49:08 -07:00
" return_url " : return_url ,
2025-08-26 20:00:07 -07:00
} ,
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
)
resp . set_cookie ( " sid " , sid , httponly = True , samesite = " lax " )
return resp
2025-09-03 18:00:06 -07:00
# Support /build without trailing slash
@router.get ( " " , response_class = HTMLResponse )
async def build_index_alias ( request : Request ) - > HTMLResponse :
return await build_index ( request )
2025-08-29 09:19:03 -07:00
2026-03-17 16:34:50 -07:00
@router.get ( " /batch-progress " )
def batch_build_progress ( request : Request , batch_id : str = Query ( . . . ) ) :
""" Poll endpoint for Batch Build progress. Returns either progress indicator or redirect to comparison. """
2025-10-14 16:09:58 -07:00
import logging
logger = logging . getLogger ( __name__ )
2026-03-17 16:34:50 -07:00
sid = request . cookies . get ( " sid " ) or new_sid ( )
2025-10-14 16:09:58 -07:00
sess = get_session ( sid )
2026-03-17 16:34:50 -07:00
from . . services . build_cache import BuildCache
2025-10-14 16:09:58 -07:00
2026-03-17 16:34:50 -07:00
batch_status = BuildCache . get_batch_status ( sess , batch_id )
logger . info ( f " [Batch Progress Poll] batch_id= { batch_id } , status= { batch_status } " )
2025-10-14 16:09:58 -07:00
2026-03-17 16:34:50 -07:00
if not batch_status :
return HTMLResponse ( ' <div class= " error " >Batch not found. Please refresh.</div> ' )
2025-10-14 16:09:58 -07:00
2026-03-17 16:34:50 -07:00
if batch_status [ " status " ] == " completed " :
# All builds complete - redirect to comparison page
response = HTMLResponse ( f ' <script>window.location.href = " /compare/ { batch_id } " ;</script> ' )
response . set_cookie ( " sid " , sid , httponly = True , samesite = " lax " )
return response
2025-10-14 16:09:58 -07:00
2026-03-17 16:34:50 -07:00
# Get config to determine color count for time estimate
config = BuildCache . get_batch_config ( sess , batch_id )
commander_name = config . get ( " commander " , " " ) if config else " "
2025-09-09 09:36:17 -07:00
2026-03-17 16:34:50 -07:00
# Estimate time based on color count (from testing data)
time_estimate = " 1-3 minutes "
if commander_name and config :
# Try to get commander's color identity
2025-09-02 16:03:12 -07:00
try :
2026-03-17 16:34:50 -07:00
from . . services import orchestrator as orch
cmd_data = orch . load_commander ( commander_name )
if cmd_data and " colorIdentity " in cmd_data :
color_count = len ( cmd_data . get ( " colorIdentity " , [ ] ) )
if color_count < = 2 :
time_estimate = " 1-3 minutes "
elif color_count == 3 :
time_estimate = " 2-4 minutes "
else : # 4-5 colors
time_estimate = " 3-5 minutes "
2025-09-02 16:03:12 -07:00
except Exception :
2026-03-17 16:34:50 -07:00
pass # Default to 1-3 if we can't determine
2025-10-14 16:09:58 -07:00
2026-03-17 16:34:50 -07:00
# Build still running - return progress content partial only
ctx = {
2025-10-06 09:17:59 -07:00
" request " : request ,
2026-03-17 16:34:50 -07:00
" batch_id " : batch_id ,
" build_count " : batch_status [ " count " ] ,
" completed " : batch_status [ " completed " ] ,
" progress_pct " : batch_status [ " progress_pct " ] ,
" status " : f " Building deck { batch_status [ ' completed ' ] + 1 } of { batch_status [ ' count ' ] } ... " if batch_status [ ' completed ' ] < batch_status [ ' count ' ] else " Finalizing... " ,
" has_errors " : batch_status [ " has_errors " ] ,
" error_count " : batch_status [ " error_count " ] ,
" time_estimate " : time_estimate
2025-10-20 18:29:53 -07:00
}
response = templates . TemplateResponse ( " build/_batch_progress_content.html " , ctx )
response . set_cookie ( " sid " , sid , httponly = True , samesite = " lax " )
return response
2025-09-09 09:36:17 -07:00
2026-03-17 16:34:50 -07:00
# ==============================================================================
# Phase 5 Routes Moved to Focused Modules (Roadmap 9 M1)
# ==============================================================================
# Permalinks and Lock Management → build_permalinks.py:
# - POST /build/lock - Card lock toggle
# - GET /build/permalink - State serialization
# - GET /build/from - State restoration
#
# Alternatives → build_alternatives.py:
# - GET /build/alternatives - Role-based card suggestions
#
# Compliance and Replacement → build_compliance.py:
# - POST /build/replace - Inline card replacement
# - POST /build/replace/undo - Undo replacement
# - GET /build/compare - Batch comparison stub
# - GET /build/compliance - Compliance panel
# - POST /build/enforce/apply - Apply enforcement
# - GET /build/enforcement - Full-page enforcement
# ==============================================================================