diff --git a/server.py b/server.py index e27c27e..ef15f0b 100644 --- a/server.py +++ b/server.py @@ -377,13 +377,7 @@ def detect_media_type(cache_path: Path) -> str: return "unknown" -UI_HTML = """ - - - - Art DAG L1 Server - - + .provenance { background: #1a1a1a; border-radius: 8px; padding: 16px; margin-top: 16px; } + .prov-item { margin: 8px 0; } + .prov-label { color: #888; font-size: 12px; } + .prov-value { font-family: monospace; font-size: 13px; } + code { background: #222; padding: 2px 6px; border-radius: 4px; } +""" + +UI_HTML = """ + + + + Art DAG L1 Server + + -

Art DAG L1 Server

+

Art DAG L1 Server

@@ -458,8 +473,10 @@ async def ui_runs(): for run in runs[:20]: # Limit to 20 most recent status_class = run.status + effect_url = f"https://git.rose-ash.com/art-dag/effects/src/branch/main/{run.recipe}" html_parts.append(f''' +
@@ -516,14 +533,162 @@ async def ui_runs(): if run.status == "failed" and run.error: html_parts.append(f'
Error: {run.error}
') - html_parts.append('
') + html_parts.append('
') html_parts.append('
') return '\n'.join(html_parts) +@app.get("/ui/detail/{run_id}", response_class=HTMLResponse) +async def ui_detail_page(run_id: str): + """Full detail page for a run.""" + run = load_run(run_id) + if not run: + return HTMLResponse('

Run not found

', status_code=404) + + # Check Celery task status if running + if run.status == "running" and run.celery_task_id: + task = celery_app.AsyncResult(run.celery_task_id) + if task.ready(): + if task.successful(): + result = task.result + run.status = "completed" + run.completed_at = datetime.now(timezone.utc).isoformat() + run.output_hash = result.get("output", {}).get("content_hash") + output_path = Path(result.get("output", {}).get("local_path", "")) + if output_path.exists(): + cache_file(output_path) + else: + run.status = "failed" + run.error = str(task.result) + save_run(run) + + effect_url = f"https://git.rose-ash.com/art-dag/effects/src/branch/main/{run.recipe}" + status_class = run.status + + html = f""" + + + + {run.recipe} - {run.run_id[:8]} | Art DAG L1 + + + +

Art DAG L1 Server

+ ← Back to runs + +
+
+
+ {run.recipe} + {run.run_id} +
+ {run.status} +
+""" + + # Media row + has_input = run.inputs and (CACHE_DIR / run.inputs[0]).exists() + has_output = run.status == "completed" and run.output_hash and (CACHE_DIR / run.output_hash).exists() + + if has_input or has_output: + html += '
' + + if has_input: + input_hash = run.inputs[0] + input_media_type = detect_media_type(CACHE_DIR / input_hash) + html += f''' +
+ +
+ ''' + if input_media_type == "video": + html += f'' + elif input_media_type == "image": + html += f'input' + html += '
' + + if has_output: + output_hash = run.output_hash + output_media_type = detect_media_type(CACHE_DIR / output_hash) + html += f''' +
+ +
+ ''' + if output_media_type == "video": + html += f'' + elif output_media_type == "image": + html += f'output' + html += '
' + + html += '
' + + # Provenance section + html += f''' +
+

Provenance

+
+
Effect
+ +
+
+
Input(s)
+
+ ''' + for inp in run.inputs: + html += f'{inp}
' + html += f''' +
+
+ ''' + + if run.output_hash: + html += f''' +
+
Output
+ +
+ ''' + + html += f''' +
+
Run ID
+
{run.run_id}
+
+
+
Created
+
{run.created_at}
+
+ ''' + + if run.completed_at: + html += f''' +
+
Completed
+
{run.completed_at}
+
+ ''' + + if run.error: + html += f''' +
+
Error
+
{run.error}
+
+ ''' + + html += ''' +
+
+ + +''' + return html + + @app.get("/ui/run/{run_id}", response_class=HTMLResponse) -async def ui_run_detail(run_id: str): +async def ui_run_partial(run_id: str): """HTMX partial: single run (for polling updates).""" run = load_run(run_id) if not run: