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}
|
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")
|
@router.delete("/admin/purge-failed")
|
||||||
async def purge_failed_runs(
|
async def purge_failed_runs(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -29,12 +29,26 @@
|
|||||||
<span class="text-red-400 text-sm ml-2">{{ run.error }}</span>
|
<span class="text-red-400 text-sm ml-2">{{ run.error }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-grow"></div>
|
<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"
|
<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">
|
class="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-sm font-medium">
|
||||||
Share to L2
|
Share to L2
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Info Grid -->
|
<!-- Info Grid -->
|
||||||
|
|||||||
Reference in New Issue
Block a user