'
+ return HTMLResponse(render_page("Login Required", content, None, active_tab="runs"), status_code=401)
+
+ run = await asyncio.to_thread(load_run, run_id)
+ if not run:
+ content = f'
Run not found: {run_id}
'
+ return HTMLResponse(render_page("Not Found", content, ctx.actor_id, active_tab="runs"), status_code=404)
+
+ # Check user owns this run
+ if run.username not in (ctx.username, ctx.actor_id):
+ content = '
Access denied.
'
+ return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="runs"), status_code=403)
+
+ # Try to load existing plan from cache
+ plan_data = None
+ PLAN_CACHE_DIR.mkdir(parents=True, exist_ok=True)
+
+ # Look for plan file matching this run
+ for plan_file in PLAN_CACHE_DIR.glob("*.json"):
+ try:
+ with open(plan_file) as f:
+ data = json.load(f)
+ # Check if this plan matches our run inputs
+ plan_inputs = data.get("input_hashes", {})
+ if set(plan_inputs.values()) == set(run.inputs):
+ plan_data = data
+ break
+ except (json.JSONDecodeError, IOError):
+ continue
+
+ # Build sub-navigation tabs
+ tabs_html = render_run_sub_tabs(run_id, active="plan")
+
+ if not plan_data:
+ content = f'''
+
+
+ Back to runs
+
+
+ {tabs_html}
+
+
+
Execution Plan
+
No execution plan available for this run.
+
Plans are generated when using recipe-based runs with the v2 API.
+
+ '''
+ return HTMLResponse(render_page_with_cytoscape(f"Plan: {run_id[:16]}...", content, ctx.actor_id, active_tab="runs"))
+
+ # Build Cytoscape nodes and edges from plan
+ nodes = []
+ edges = []
+ steps = plan_data.get("steps", [])
+
+ for step in steps:
+ node_type = step.get("node_type", "EFFECT")
+ color = NODE_COLORS.get(node_type, NODE_COLORS["default"])
+ cached = step.get("cached", False)
+ status = "cached" if cached else "pending"
+
+ # Shorter label for display
+ step_id = step.get("step_id", "")
+ label = step_id[:12] + "..." if len(step_id) > 12 else step_id
+
+ nodes.append({
+ "data": {
+ "id": step_id,
+ "label": label,
+ "nodeType": node_type,
+ "level": step.get("level", 0),
+ "cacheId": step.get("cache_id", ""),
+ "status": status,
+ "color": color,
+ "config": step.get("config")
+ }
+ })
+
+ # Build edges from the full plan JSON if available
+ if "plan_json" in plan_data:
+ try:
+ full_plan = json.loads(plan_data["plan_json"])
+ for step in full_plan.get("steps", []):
+ step_id = step.get("step_id", "")
+ for input_step in step.get("input_steps", []):
+ edges.append({
+ "data": {
+ "source": input_step,
+ "target": step_id
+ }
+ })
+ except json.JSONDecodeError:
+ pass
+
+ nodes_json = json.dumps(nodes)
+ edges_json = json.dumps(edges)
+
+ dag_html = render_dag_cytoscape(nodes_json, edges_json)
+
+ # Stats summary
+ total = plan_data.get("total_steps", len(steps))
+ cached = plan_data.get("cached_steps", sum(1 for s in steps if s.get("cached")))
+ pending = plan_data.get("pending_steps", total - cached)
+
+ content = f'''
+
+
+ Back to runs
+
+
+ {tabs_html}
+
+
+ '''
+
+ return HTMLResponse(render_page_with_cytoscape(f"Plan: {run_id[:16]}...", content, ctx.actor_id, active_tab="runs"))
+
+
+@app.get("/run/{run_id}/analysis", response_class=HTMLResponse)
+async def run_analysis_page(run_id: str, request: Request):
+ """Show analysis results for run inputs."""
+ ctx = await get_user_context_from_cookie(request)
+ if not ctx:
+ content = '
Not logged in.
'
+ return HTMLResponse(render_page("Login Required", content, None, active_tab="runs"), status_code=401)
+
+ run = await asyncio.to_thread(load_run, run_id)
+ if not run:
+ content = f'
Run not found: {run_id}
'
+ return HTMLResponse(render_page("Not Found", content, ctx.actor_id, active_tab="runs"), status_code=404)
+
+ # Check user owns this run
+ if run.username not in (ctx.username, ctx.actor_id):
+ content = '
Access denied.
'
+ return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="runs"), status_code=403)
+
+ tabs_html = render_run_sub_tabs(run_id, active="analysis")
+
+ # Load analysis results for each input
+ analysis_html = ""
+ ANALYSIS_CACHE_DIR.mkdir(parents=True, exist_ok=True)
+
+ for i, input_hash in enumerate(run.inputs):
+ analysis_path = ANALYSIS_CACHE_DIR / f"{input_hash}.json"
+ analysis_data = None
+
+ if analysis_path.exists():
+ try:
+ with open(analysis_path) as f:
+ analysis_data = json.load(f)
+ except (json.JSONDecodeError, IOError):
+ pass
+
+ input_name = f"Input {i + 1}"
+
+ if analysis_data:
+ tempo = analysis_data.get("tempo", "N/A")
+ if isinstance(tempo, float):
+ tempo = f"{tempo:.1f}"
+ beat_times = analysis_data.get("beat_times", [])
+ beat_count = len(beat_times)
+ energy = analysis_data.get("energy")
+
+ # Beat visualization (simple bar chart showing beat positions)
+ beat_bars = ""
+ if beat_times and len(beat_times) > 0:
+ # Show first 50 beats as vertical bars
+ display_beats = beat_times[:50]
+ max_time = max(display_beats) if display_beats else 1
+ for bt in display_beats:
+ # Normalize to percentage
+ pos = (bt / max_time) * 100 if max_time > 0 else 0
+ beat_bars += f''
+
+ energy_bar = ""
+ if energy is not None:
+ try:
+ energy_pct = min(float(energy) * 100, 100)
+ energy_bar = f'''
+
+ {analysis_html}
+ '''
+
+ return HTMLResponse(render_page(f"Analysis: {run_id[:16]}...", content, ctx.actor_id, active_tab="runs"))
+
+
+@app.get("/run/{run_id}/artifacts", response_class=HTMLResponse)
+async def run_artifacts_page(run_id: str, request: Request):
+ """Show all cached artifacts produced by this run."""
+ ctx = await get_user_context_from_cookie(request)
+ if not ctx:
+ content = '
Not logged in.
'
+ return HTMLResponse(render_page("Login Required", content, None, active_tab="runs"), status_code=401)
+
+ run = await asyncio.to_thread(load_run, run_id)
+ if not run:
+ content = f'
Run not found: {run_id}
'
+ return HTMLResponse(render_page("Not Found", content, ctx.actor_id, active_tab="runs"), status_code=404)
+
+ # Check user owns this run
+ if run.username not in (ctx.username, ctx.actor_id):
+ content = '
+
+
+"""
+
def render_page(title: str, content: str, actor_id: Optional[str] = None, active_tab: str = None) -> str:
"""Render a page with nav bar and content. Used for clean URL pages.