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:
@@ -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,
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user