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,

View File

@@ -29,12 +29,26 @@
<span class="text-red-400 text-sm ml-2">{{ run.error }}</span>
{% endif %}
<div class="flex-grow"></div>
{% if run.recipe %}
<button hx-post="/runs/rerun/{{ run.recipe }}"
hx-target="#action-result"
hx-swap="innerHTML"
class="bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded text-sm font-medium">
Run Again
</button>
{% endif %}
<button hx-post="/runs/{{ run.run_id }}/publish"
hx-target="#share-result"
hx-target="#action-result"
class="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-sm font-medium">
Share to L2
</button>
<span id="share-result"></span>
<button hx-delete="/runs/{{ run.run_id }}/ui"
hx-target="#action-result"
hx-confirm="Delete this run and all its artifacts? This cannot be undone."
class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-sm font-medium">
Delete
</button>
<span id="action-result"></span>
</div>
<!-- Info Grid -->