Add cache pinning and discard functionality
- Pin items when published (published, input_to_published)
- Discard endpoint (DELETE /cache/{hash}) refuses pinned items
- UI shows pin status and discard button on cache detail
- Run publish also pins output and all inputs for provenance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
119
server.py
119
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):
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Publish to L2 (ActivityPub)</h3>
|
||||
{publish_html}
|
||||
</div>
|
||||
|
||||
<!-- Status & Actions Section -->
|
||||
<div class="border-t border-dark-500 pt-6 mt-6">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Status</h3>
|
||||
<div class="bg-dark-600 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm text-gray-400">Pinned:</span>
|
||||
{'<span class="text-green-400">Yes</span>' if pinned else '<span class="text-gray-500">No</span>'}
|
||||
{f'<span class="text-xs text-gray-500 ml-2">({pin_reason})</span>' if pinned and pin_reason else ''}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Pinned items cannot be discarded. Items are pinned when published or used as inputs to published content.</p>
|
||||
</div>
|
||||
|
||||
<div id="discard-result"></div>
|
||||
{'<p class="text-gray-500 text-sm">Cannot discard pinned items.</p>' if pinned else f"""
|
||||
<button hx-delete="/ui/cache/{content_hash}/discard" hx-target="#discard-result" hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you want to discard this item? This cannot be undone."
|
||||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors">
|
||||
Discard Item
|
||||
</button>
|
||||
"""}
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
@@ -826,7 +850,7 @@ async def ui_publish_cache(content_hash: str, request: Request):
|
||||
"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 f'''
|
||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
|
||||
@@ -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 '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Login required</div>'
|
||||
|
||||
# Check ownership
|
||||
user_hashes = get_user_cache_hashes(current_user)
|
||||
if content_hash not in user_hashes:
|
||||
return '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Access denied</div>'
|
||||
|
||||
cache_path = CACHE_DIR / content_hash
|
||||
if not cache_path.exists():
|
||||
return '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Content not found</div>'
|
||||
|
||||
# Check if pinned
|
||||
meta = load_cache_meta(content_hash)
|
||||
if meta.get("pinned"):
|
||||
pin_reason = meta.get("pin_reason", "unknown")
|
||||
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Cannot discard: item is pinned ({pin_reason})</div>'
|
||||
|
||||
# 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 '''
|
||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
|
||||
Item discarded. <a href="/ui?tab=cache" class="underline">Back to cache</a>
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
# 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('<div class="error">Not logged in</div>')
|
||||
|
||||
# Get the run to pin its output and inputs
|
||||
run = load_run(run_id)
|
||||
if not run:
|
||||
return HTMLResponse('<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg">Run not found</div>')
|
||||
|
||||
# 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'<div class="error">Error: {error}</div>')
|
||||
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'''
|
||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
|
||||
Published to L2 as <strong>{result["asset"]["name"]}</strong>
|
||||
|
||||
Reference in New Issue
Block a user