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 <noreply@anthropic.com>
This commit is contained in:
152
server.py
152
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'''
|
||||
<div class="flex justify-between items-start py-2 border-b border-green-800 last:border-0">
|
||||
<div>
|
||||
<div class="text-white font-medium">{asset_name}</div>
|
||||
<a href="{asset_url}" target="_blank" class="text-white font-medium hover:text-blue-300">{asset_name}</a>
|
||||
<div class="text-xs text-gray-400">{l2_server}</div>
|
||||
</div>
|
||||
<div class="text-right text-xs text-gray-400">
|
||||
@@ -2497,7 +2498,16 @@ async def list_media(
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Media ({total} items)</h2>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">Media ({total} items)</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="sync-result"></div>
|
||||
<button hx-post="/ui/sync-l2" hx-target="#sync-result" hx-swap="innerHTML"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Sync with L2
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{''.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 '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg">Login required</div>'
|
||||
|
||||
try:
|
||||
result = await sync_with_l2(ctx)
|
||||
synced = result.get("synced", 0)
|
||||
total = result.get("total_activities", 0)
|
||||
|
||||
if synced > 0:
|
||||
return f'''
|
||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg">
|
||||
Synced {synced} asset(s) from L2 ({total} activities found)
|
||||
</div>
|
||||
'''
|
||||
else:
|
||||
return f'''
|
||||
<div class="bg-yellow-900/50 border border-yellow-700 text-yellow-300 px-4 py-3 rounded-lg">
|
||||
No new assets to sync ({total} activities found)
|
||||
</div>
|
||||
'''
|
||||
except Exception as e:
|
||||
logger.error(f"L2 sync failed: {e}")
|
||||
return f'''
|
||||
<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg">
|
||||
Sync failed: {str(e)}
|
||||
</div>
|
||||
'''
|
||||
|
||||
|
||||
# ============ 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 = '<span class="px-2 py-1 bg-purple-600 text-white text-xs font-medium rounded-full" title="On IPFS">IPFS</span>' 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'<span class="px-2 py-1 bg-green-600 text-white text-xs font-medium rounded-full" title="Published to L2">L2</span>'
|
||||
else:
|
||||
published_badge = ''
|
||||
|
||||
# Format size
|
||||
size = item["size"]
|
||||
if size > 1024*1024:
|
||||
@@ -3703,6 +3850,7 @@ async def ui_media_list(
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 bg-blue-600 text-white text-xs font-medium rounded-full">{media_type}</span>
|
||||
{ipfs_badge}
|
||||
{published_badge}
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">{size_str}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user