diff --git a/server.py b/server.py
index b1d7d1a..de5f4a5 100644
--- a/server.py
+++ b/server.py
@@ -596,6 +596,8 @@ async def ui_cache_meta_form(content_hash: str, request: Request):
tags = meta.get("tags", [])
tags_str = ", ".join(tags) if tags else ""
published = meta.get("published", {})
+ pinned = meta.get("pinned", False)
+ pin_reason = meta.get("pin_reason", "")
# Detect media type for publish
cache_path = CACHE_DIR / content_hash
@@ -706,6 +708,28 @@ async def ui_cache_meta_form(content_hash: str, request: Request):
@@ -904,6 +928,81 @@ async def list_cache():
return [f.name for f in CACHE_DIR.iterdir() if f.is_file()]
+@app.delete("/cache/{content_hash}")
+async def discard_cache(content_hash: str, username: str = Depends(get_required_user)):
+ """
+ Discard (delete) a cached item.
+
+ Refuses to delete pinned items. Pinned items include:
+ - Published items
+ - Inputs to published items
+ """
+ cache_path = CACHE_DIR / content_hash
+ if not cache_path.exists():
+ raise HTTPException(404, "Content not found")
+
+ # Check ownership
+ user_hashes = get_user_cache_hashes(username)
+ if content_hash not in user_hashes:
+ raise HTTPException(403, "Access denied")
+
+ # Check if pinned
+ meta = load_cache_meta(content_hash)
+ if meta.get("pinned"):
+ pin_reason = meta.get("pin_reason", "unknown")
+ raise HTTPException(400, f"Cannot discard pinned item (reason: {pin_reason})")
+
+ # Delete the file and metadata
+ cache_path.unlink()
+ meta_path = CACHE_DIR / f"{content_hash}.meta.json"
+ if meta_path.exists():
+ meta_path.unlink()
+ # Also delete transcoded mp4 if exists
+ mp4_path = CACHE_DIR / f"{content_hash}.mp4"
+ if mp4_path.exists():
+ mp4_path.unlink()
+
+ return {"discarded": True, "content_hash": content_hash}
+
+
+@app.delete("/ui/cache/{content_hash}/discard", response_class=HTMLResponse)
+async def ui_discard_cache(content_hash: str, request: Request):
+ """HTMX handler: discard a cached item."""
+ current_user = get_user_from_cookie(request)
+ if not current_user:
+ return '
Login required
'
+
+ # Check ownership
+ user_hashes = get_user_cache_hashes(current_user)
+ if content_hash not in user_hashes:
+ return '
Access denied
'
+
+ cache_path = CACHE_DIR / content_hash
+ if not cache_path.exists():
+ return '
Content not found
'
+
+ # Check if pinned
+ meta = load_cache_meta(content_hash)
+ if meta.get("pinned"):
+ pin_reason = meta.get("pin_reason", "unknown")
+ return f'
Cannot discard: item is pinned ({pin_reason})
'
+
+ # Delete the file and metadata
+ cache_path.unlink()
+ meta_path = CACHE_DIR / f"{content_hash}.meta.json"
+ if meta_path.exists():
+ meta_path.unlink()
+ mp4_path = CACHE_DIR / f"{content_hash}.mp4"
+ if mp4_path.exists():
+ mp4_path.unlink()
+
+ return '''
+
+ Item discarded.
Back to cache
+
+ '''
+
+
# Known assets (bootstrap data)
KNOWN_ASSETS = {
"cat": "33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b",
@@ -1181,14 +1280,14 @@ async def publish_cache_to_l2(
except Exception as e:
raise HTTPException(500, f"L2 publish failed: {e}")
- # Update local metadata with publish status
+ # Update local metadata with publish status and pin
publish_info = {
"to_l2": True,
"asset_name": req.asset_name,
"published_at": datetime.now(timezone.utc).isoformat(),
"last_synced_at": datetime.now(timezone.utc).isoformat()
}
- save_cache_meta(content_hash, published=publish_info)
+ save_cache_meta(content_hash, published=publish_info, pinned=True, pin_reason="published")
return {
"published": True,
@@ -1665,6 +1764,11 @@ async def ui_publish_run(run_id: str, request: Request, output_name: str = Form(
if not token:
return HTMLResponse('
Not logged in
')
+ # Get the run to pin its output and inputs
+ run = load_run(run_id)
+ if not run:
+ return HTMLResponse('
Run not found
')
+
# Call L2 to publish the run, including this L1's public URL
# Longer timeout because L2 calls back to L1 to fetch run details
try:
@@ -1679,6 +1783,15 @@ async def ui_publish_run(run_id: str, request: Request, output_name: str = Form(
return HTMLResponse(f'
Error: {error}
')
resp.raise_for_status()
result = resp.json()
+
+ # Pin the output
+ if run.output_hash:
+ save_cache_meta(run.output_hash, pinned=True, pin_reason="published")
+
+ # Pin the inputs (for provenance)
+ for input_hash in run.inputs:
+ save_cache_meta(input_hash, pinned=True, pin_reason="input_to_published")
+
return HTMLResponse(f'''
Published to L2 as {result["asset"]["name"]}