From d6c575760bc1227fe479ae4023e186e0fe3b2f62 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 4 Feb 2026 17:18:16 +0000 Subject: [PATCH] Add Delete and Run Again buttons to run detail page - Add "Run Again" button that reruns the recipe with same parameters - Add "Delete" button with confirmation to delete run and artifacts - Consolidate result display into single #action-result span - Implement POST /runs/rerun/{recipe_id} endpoint Co-Authored-By: Claude Opus 4.5 --- app/routers/runs.py | 91 ++++++++++++++++++++++++++++++++++ app/templates/runs/detail.html | 18 ++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/app/routers/runs.py b/app/routers/runs.py index 3ff6cbf..865023f 100644 --- a/app/routers/runs.py +++ b/app/routers/runs.py @@ -944,6 +944,97 @@ async def publish_run( return {"ipfs_cid": ipfs_cid, "output_cid": output_cid, "published": True} +@router.post("/rerun/{recipe_id}", response_class=HTMLResponse) +async def rerun_recipe( + recipe_id: str, + request: Request, +): + """HTMX handler: run a recipe again. + + Fetches the recipe by CID and starts a new streaming run. + """ + import uuid + import database + from tasks.streaming import run_stream + + ctx = await get_current_user(request) + if not ctx: + return HTMLResponse( + '
Login required
', + status_code=401 + ) + + # Fetch the recipe + try: + from ..services.recipe_service import RecipeService + recipe_service = RecipeService(get_redis_client(), get_cache_manager()) + recipe = await recipe_service.get_recipe(recipe_id) + if not recipe: + return HTMLResponse(f'
Recipe not found: {recipe_id[:16]}...
') + + # Get the S-expression content + recipe_sexp = recipe.get("sexp") + if not recipe_sexp: + return HTMLResponse('
Recipe has no S-expression content
') + + # Extract recipe name for output + import re + name_match = re.search(r'\(stream\s+"([^"]+)"', recipe_sexp) + recipe_name = name_match.group(1) if name_match else f"stream" + + # Generate new run ID + run_id = str(uuid.uuid4()) + + # Extract duration from recipe if present (look for :duration pattern) + duration = None + duration_match = re.search(r':duration\s+(\d+(?:\.\d+)?)', recipe_sexp) + if duration_match: + duration = float(duration_match.group(1)) + + # Extract fps from recipe if present + fps = None + fps_match = re.search(r':fps\s+(\d+(?:\.\d+)?)', recipe_sexp) + if fps_match: + fps = float(fps_match.group(1)) + + # Submit Celery task to GPU queue + task = run_stream.apply_async( + kwargs=dict( + run_id=run_id, + recipe_sexp=recipe_sexp, + output_name=f"{recipe_name}.mp4", + duration=duration, + fps=fps, + actor_id=ctx.actor_id, + sources_sexp=None, + audio_sexp=None, + ), + queue='gpu', + ) + + # Store in database + await database.create_pending_run( + run_id=run_id, + celery_task_id=task.id, + recipe=recipe_id, + inputs=[], + actor_id=ctx.actor_id, + dag_json=recipe_sexp, + output_name=f"{recipe_name}.mp4", + ) + + logger.info(f"Started rerun {run_id} for recipe {recipe_id[:16]}...") + + return HTMLResponse( + f'
Started new run
' + f'' + ) + + except Exception as e: + logger.error(f"Failed to rerun recipe {recipe_id}: {e}") + return HTMLResponse(f'
Error: {str(e)}
') + + @router.delete("/admin/purge-failed") async def purge_failed_runs( request: Request, diff --git a/app/templates/runs/detail.html b/app/templates/runs/detail.html index 22b9bd8..4c5e003 100644 --- a/app/templates/runs/detail.html +++ b/app/templates/runs/detail.html @@ -29,12 +29,26 @@ {{ run.error }} {% endif %}
+ {% if run.recipe %} + + {% endif %} - + +