From d3468cd47cacd45205aad320074143332cc0dd40 Mon Sep 17 00:00:00 2001 From: gilesb Date: Fri, 9 Jan 2026 03:18:48 +0000 Subject: [PATCH] Add L2 sync button and improve publish status display - Add "Sync with L2" button on media page to fetch user's outbox - Link asset names to L2 asset pages in publish status - Add green "L2" badge to media list for published items - Create /user/sync-l2 and /ui/sync-l2 endpoints Co-Authored-By: Claude Opus 4.5 --- server.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 122c772..fa05444 100644 --- a/server.py +++ b/server.py @@ -2002,10 +2002,11 @@ async def ui_cache_meta_form(content_hash: str, request: Request): asset_name = share.get("asset_name", "") published_at = share.get("published_at", "")[:10] if share.get("published_at") else "" last_synced = share.get("last_synced_at", "")[:10] if share.get("last_synced_at") else "" + asset_url = f"{l2_server}/assets/{asset_name}" shares_html += f'''
-
{asset_name}
+ {asset_name}
{l2_server}
@@ -2497,7 +2498,16 @@ async def list_media( ''' content = f''' -

Media ({total} items)

+
+

Media ({total} items)

+
+
+ +
+
{''.join(html_parts)} {infinite_scroll_trigger} @@ -3068,6 +3078,132 @@ async def republish_cache_to_l2( } +# ============ L2 Sync ============ + +def _fetch_l2_outbox_sync(l2_server: str, username: str) -> list: + """Fetch user's outbox from L2 (sync version for asyncio.to_thread).""" + try: + # Fetch outbox page with activities + resp = http_requests.get( + f"{l2_server}/users/{username}/outbox?page=true", + headers={"Accept": "application/activity+json"}, + timeout=10 + ) + if resp.status_code != 200: + logger.warning(f"L2 outbox fetch failed: {resp.status_code}") + return [] + data = resp.json() + return data.get("orderedItems", []) + except Exception as e: + logger.error(f"Failed to fetch L2 outbox: {e}") + return [] + + +@app.post("/user/sync-l2") +async def sync_with_l2(ctx: UserContext = Depends(get_required_user_context)): + """ + Sync local L2 share records with user's L2 outbox. + Fetches user's published assets from their L2 server and updates local tracking. + """ + l2_server = ctx.l2_server + username = ctx.username + + # Fetch outbox activities + activities = await asyncio.to_thread(_fetch_l2_outbox_sync, l2_server, username) + + if not activities: + return {"synced": 0, "message": "No activities found or L2 unavailable"} + + # Process Create activities for assets + synced_count = 0 + for activity in activities: + if activity.get("type") != "Create": + continue + + obj = activity.get("object", {}) + if not isinstance(obj, dict): + continue + + # Get asset info - look for content_hash in attachment or directly + content_hash = None + asset_name = obj.get("name", "") + + # Check attachments for content hash + for attachment in obj.get("attachment", []): + if attachment.get("name") == "content_hash": + content_hash = attachment.get("value") + break + + # Also check if there's a hash in the object URL or ID + if not content_hash: + # Try to extract from object ID like /objects/{hash} + obj_id = obj.get("id", "") + if "/objects/" in obj_id: + content_hash = obj_id.split("/objects/")[-1].split("/")[0] + + if not content_hash or not asset_name: + continue + + # Check if we have this content locally + cache_path = get_cache_path(content_hash) + if not cache_path: + continue # We don't have this content, skip + + # Determine content type from object type + obj_type = obj.get("type", "") + if obj_type == "Video": + content_type = "video" + elif obj_type == "Image": + content_type = "image" + else: + content_type = "media" + + # Update local L2 share record + await database.save_l2_share( + content_hash=content_hash, + actor_id=ctx.actor_id, + l2_server=l2_server, + asset_name=asset_name, + content_type=content_type + ) + synced_count += 1 + + return {"synced": synced_count, "total_activities": len(activities)} + + +@app.post("/ui/sync-l2", response_class=HTMLResponse) +async def ui_sync_with_l2(request: Request): + """HTMX handler: sync with L2 server.""" + ctx = get_user_context_from_cookie(request) + if not ctx: + return '
Login required
' + + try: + result = await sync_with_l2(ctx) + synced = result.get("synced", 0) + total = result.get("total_activities", 0) + + if synced > 0: + return f''' +
+ Synced {synced} asset(s) from L2 ({total} activities found) +
+ ''' + else: + return f''' +
+ No new assets to sync ({total} activities found) +
+ ''' + except Exception as e: + logger.error(f"L2 sync failed: {e}") + return f''' +
+ Sync failed: {str(e)} +
+ ''' + + # ============ Folder & Collection Management ============ @app.get("/user/folders") @@ -3687,6 +3823,17 @@ async def ui_media_list( ipfs_cid = cache_item.get("ipfs_cid") if cache_item else None ipfs_badge = 'IPFS' if ipfs_cid else '' + # Check L2 publish status + l2_shares = item["meta"].get("l2_shares", []) + if l2_shares: + first_share = l2_shares[0] + l2_server = first_share.get("l2_server", "") + asset_name = first_share.get("asset_name", "") + asset_url = f"{l2_server}/assets/{asset_name}" + published_badge = f'L2' + else: + published_badge = '' + # Format size size = item["size"] if size > 1024*1024: @@ -3703,6 +3850,7 @@ async def ui_media_list(
{media_type} {ipfs_badge} + {published_badge}
{size_str}