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 = meta.get("tags", [])
|
||||||
tags_str = ", ".join(tags) if tags else ""
|
tags_str = ", ".join(tags) if tags else ""
|
||||||
published = meta.get("published", {})
|
published = meta.get("published", {})
|
||||||
|
pinned = meta.get("pinned", False)
|
||||||
|
pin_reason = meta.get("pin_reason", "")
|
||||||
|
|
||||||
# Detect media type for publish
|
# Detect media type for publish
|
||||||
cache_path = CACHE_DIR / content_hash
|
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>
|
<h3 class="text-lg font-semibold text-white mb-4">Publish to L2 (ActivityPub)</h3>
|
||||||
{publish_html}
|
{publish_html}
|
||||||
</div>
|
</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(),
|
"published_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"last_synced_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'''
|
return f'''
|
||||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
|
<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()]
|
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 (bootstrap data)
|
||||||
KNOWN_ASSETS = {
|
KNOWN_ASSETS = {
|
||||||
"cat": "33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b",
|
"cat": "33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b",
|
||||||
@@ -1181,14 +1280,14 @@ async def publish_cache_to_l2(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, f"L2 publish failed: {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 = {
|
publish_info = {
|
||||||
"to_l2": True,
|
"to_l2": True,
|
||||||
"asset_name": req.asset_name,
|
"asset_name": req.asset_name,
|
||||||
"published_at": datetime.now(timezone.utc).isoformat(),
|
"published_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"last_synced_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 {
|
return {
|
||||||
"published": True,
|
"published": True,
|
||||||
@@ -1665,6 +1764,11 @@ async def ui_publish_run(run_id: str, request: Request, output_name: str = Form(
|
|||||||
if not token:
|
if not token:
|
||||||
return HTMLResponse('<div class="error">Not logged in</div>')
|
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
|
# Call L2 to publish the run, including this L1's public URL
|
||||||
# Longer timeout because L2 calls back to L1 to fetch run details
|
# Longer timeout because L2 calls back to L1 to fetch run details
|
||||||
try:
|
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>')
|
return HTMLResponse(f'<div class="error">Error: {error}</div>')
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
result = resp.json()
|
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'''
|
return HTMLResponse(f'''
|
||||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
|
<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>
|
Published to L2 as <strong>{result["asset"]["name"]}</strong>
|
||||||
|
|||||||
Reference in New Issue
Block a user