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 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-04 17:18:16 +00:00
parent 9a8a701492
commit d6c575760b
2 changed files with 107 additions and 2 deletions

View File

@@ -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(
'<div class="text-red-400">Login required</div>',
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'<div class="text-red-400">Recipe not found: {recipe_id[:16]}...</div>')
# Get the S-expression content
recipe_sexp = recipe.get("sexp")
if not recipe_sexp:
return HTMLResponse('<div class="text-red-400">Recipe has no S-expression content</div>')
# 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'<div class="text-green-400">Started new run</div>'
f'<script>setTimeout(() => window.location.href = "/runs/{run_id}", 1000);</script>'
)
except Exception as e:
logger.error(f"Failed to rerun recipe {recipe_id}: {e}")
return HTMLResponse(f'<div class="text-red-400">Error: {str(e)}</div>')
@router.delete("/admin/purge-failed")
async def purge_failed_runs(
request: Request,