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):

Publish to L2 (ActivityPub)

{publish_html} + + +
+

Status

+
+
+ Pinned: + {'Yes' if pinned else 'No'} + {f'({pin_reason})' if pinned and pin_reason else ''} +
+

Pinned items cannot be discarded. Items are pinned when published or used as inputs to published content.

+
+ +
+ {'

Cannot discard pinned items.

' if pinned else f""" + + """} +
''' @@ -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'''
@@ -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"]}