Enhance L2 asset detail with content display and provenance
- Add L1_PUBLIC_URL and EFFECTS_REPO_URL config - Display images/videos directly from L1 cache - Show provenance section for rendered outputs: - Effect name with link to source code - Input content hashes (linked) - L1 run ID (linked) - Render timestamp - Download button for content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
159
server.py
159
server.py
@@ -34,7 +34,8 @@ from auth import (
|
||||
# Configuration
|
||||
DOMAIN = os.environ.get("ARTDAG_DOMAIN", "artdag.rose-ash.com")
|
||||
DATA_DIR = Path(os.environ.get("ARTDAG_DATA", str(Path.home() / ".artdag" / "l2")))
|
||||
# Note: L1_SERVER is no longer needed - L1 URL comes with each request
|
||||
L1_PUBLIC_URL = os.environ.get("L1_PUBLIC_URL", "https://celery-artdag.rose-ash.com")
|
||||
EFFECTS_REPO_URL = os.environ.get("EFFECTS_REPO_URL", "https://git.rose-ash.com/art-dag/effects")
|
||||
|
||||
# Ensure data directory exists
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
@@ -642,7 +643,7 @@ async def ui_users_page(request: Request):
|
||||
|
||||
@app.get("/ui/asset/{name}", response_class=HTMLResponse)
|
||||
async def ui_asset_detail(name: str, request: Request):
|
||||
"""Asset detail page."""
|
||||
"""Asset detail page with content preview and provenance."""
|
||||
username = get_user_from_cookie(request)
|
||||
registry = load_registry()
|
||||
assets = registry.get("assets", {})
|
||||
@@ -662,12 +663,56 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
tags = asset.get("tags", [])
|
||||
description = asset.get("description", "")
|
||||
origin = asset.get("origin", {})
|
||||
provenance = asset.get("provenance", {})
|
||||
metadata = asset.get("metadata", {})
|
||||
created_at = asset.get("created_at", "")[:10]
|
||||
|
||||
type_color = "bg-blue-600" if asset_type == "image" else "bg-purple-600" if asset_type == "video" else "bg-gray-600"
|
||||
|
||||
# Determine L1 server URL for content
|
||||
l1_server = provenance.get("l1_server", L1_PUBLIC_URL).rstrip("/")
|
||||
|
||||
# Content display - image or video from L1
|
||||
if asset_type == "video":
|
||||
# Use iOS-compatible MP4 endpoint
|
||||
content_html = f'''
|
||||
<div class="bg-dark-600 rounded-lg p-4 mb-6">
|
||||
<video src="{l1_server}/cache/{content_hash}/mp4" controls autoplay muted loop playsinline
|
||||
class="max-w-full max-h-96 mx-auto rounded-lg"></video>
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{l1_server}/cache/{content_hash}" download
|
||||
class="inline-block px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
Download Original
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
elif asset_type == "image":
|
||||
content_html = f'''
|
||||
<div class="bg-dark-600 rounded-lg p-4 mb-6">
|
||||
<img src="{l1_server}/cache/{content_hash}" alt="{name}"
|
||||
class="max-w-full max-h-96 mx-auto rounded-lg">
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{l1_server}/cache/{content_hash}" download
|
||||
class="inline-block px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
else:
|
||||
content_html = f'''
|
||||
<div class="bg-dark-600 rounded-lg p-4 mb-6 text-center">
|
||||
<p class="text-gray-400 mb-3">Content type: {asset_type}</p>
|
||||
<a href="{l1_server}/cache/{content_hash}" download
|
||||
class="inline-block px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
'''
|
||||
|
||||
# Origin display
|
||||
origin_html = ""
|
||||
origin_html = '<span class="text-gray-500">Not specified</span>'
|
||||
if origin:
|
||||
origin_type = origin.get("type", "unknown")
|
||||
if origin_type == "self":
|
||||
@@ -675,18 +720,69 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
elif origin_type == "external":
|
||||
origin_url = origin.get("url", "")
|
||||
origin_note = origin.get("note", "")
|
||||
origin_html = f'''
|
||||
<a href="{origin_url}" target="_blank" class="text-blue-400 hover:text-blue-300 break-all">{origin_url}</a>
|
||||
'''
|
||||
origin_html = f'<a href="{origin_url}" target="_blank" class="text-blue-400 hover:text-blue-300 break-all">{origin_url}</a>'
|
||||
if origin_note:
|
||||
origin_html += f'<p class="text-gray-500 text-sm mt-1">{origin_note}</p>'
|
||||
|
||||
# Tags display
|
||||
tags_html = ""
|
||||
tags_html = '<span class="text-gray-500">No tags</span>'
|
||||
if tags:
|
||||
tags_html = " ".join([f'<span class="px-2 py-1 bg-dark-500 text-gray-300 text-xs rounded">{t}</span>' for t in tags])
|
||||
else:
|
||||
tags_html = '<span class="text-gray-500">No tags</span>'
|
||||
|
||||
# Provenance section - for rendered outputs
|
||||
provenance_html = ""
|
||||
if provenance:
|
||||
recipe = provenance.get("recipe", "")
|
||||
inputs = provenance.get("inputs", [])
|
||||
l1_run_id = provenance.get("l1_run_id", "")
|
||||
rendered_at = provenance.get("rendered_at", "")[:10] if provenance.get("rendered_at") else ""
|
||||
|
||||
# Build effect link
|
||||
effect_url = f"{EFFECTS_REPO_URL}/src/branch/main/{recipe}"
|
||||
|
||||
# Build inputs display
|
||||
inputs_html = ""
|
||||
for inp in inputs:
|
||||
inp_hash = inp.get("content_hash", "") if isinstance(inp, dict) else inp
|
||||
if inp_hash:
|
||||
inputs_html += f'''
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<a href="{l1_server}/cache/{inp_hash}" target="_blank"
|
||||
class="text-blue-400 hover:text-blue-300 font-mono text-xs">{inp_hash[:24]}...</a>
|
||||
</div>
|
||||
'''
|
||||
|
||||
provenance_html = f'''
|
||||
<div class="border-t border-dark-500 pt-6 mt-6">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Provenance</h3>
|
||||
<p class="text-sm text-gray-400 mb-4">This asset was created by applying an effect to input content.</p>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h4 class="text-sm font-medium text-gray-400 mb-2">Effect</h4>
|
||||
<a href="{effect_url}" target="_blank"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
{recipe}
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h4 class="text-sm font-medium text-gray-400 mb-2">Input(s)</h4>
|
||||
{inputs_html if inputs_html else '<span class="text-gray-500">No inputs recorded</span>'}
|
||||
</div>
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h4 class="text-sm font-medium text-gray-400 mb-2">L1 Run</h4>
|
||||
<a href="{l1_server}/ui/detail/{l1_run_id}" target="_blank"
|
||||
class="text-blue-400 hover:text-blue-300 font-mono text-xs">{l1_run_id[:16]}...</a>
|
||||
</div>
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h4 class="text-sm font-medium text-gray-400 mb-2">Rendered</h4>
|
||||
<span class="text-white">{rendered_at if rendered_at else 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<p class="mb-4"><a href="/ui/registry" class="text-blue-400 hover:text-blue-300">← Back to Registry</a></p>
|
||||
@@ -696,6 +792,8 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
<span class="px-3 py-1 {type_color} text-white text-sm rounded">{asset_type}</span>
|
||||
</div>
|
||||
|
||||
{content_html}
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="space-y-4">
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
@@ -703,6 +801,18 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
<a href="/ui/user/{owner}" class="text-blue-400 hover:text-blue-300 text-lg">{owner}</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Description</h3>
|
||||
<p class="text-white">{description if description else '<span class="text-gray-500">No description</span>'}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Origin</h3>
|
||||
{origin_html}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Content Hash</h3>
|
||||
<code class="text-green-300 text-sm break-all">{content_hash}</code>
|
||||
@@ -712,18 +822,6 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Created</h3>
|
||||
<span class="text-white">{created_at}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Description</h3>
|
||||
<p class="text-white">{description if description else '<span class="text-gray-500">No description</span>'}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Origin</h3>
|
||||
{origin_html if origin_html else '<span class="text-gray-500">Not specified</span>'}
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">Tags</h3>
|
||||
@@ -732,11 +830,20 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-dark-600 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-2">ActivityPub</h3>
|
||||
<p class="text-sm text-gray-300">
|
||||
Object URL: <a href="https://{DOMAIN}/objects/{content_hash}" target="_blank" class="text-blue-400 hover:text-blue-300">https://{DOMAIN}/objects/{content_hash}</a>
|
||||
</p>
|
||||
{provenance_html}
|
||||
|
||||
<div class="border-t border-dark-500 pt-6 mt-6">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">ActivityPub</h3>
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-300 mb-2">
|
||||
<span class="text-gray-400">Object URL:</span>
|
||||
<a href="https://{DOMAIN}/objects/{content_hash}" target="_blank" class="text-blue-400 hover:text-blue-300 ml-2">https://{DOMAIN}/objects/{content_hash}</a>
|
||||
</p>
|
||||
<p class="text-sm text-gray-300">
|
||||
<span class="text-gray-400">Owner Actor:</span>
|
||||
<a href="https://{DOMAIN}/users/{owner}" target="_blank" class="text-blue-400 hover:text-blue-300 ml-2">https://{DOMAIN}/users/{owner}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
return HTMLResponse(base_html(f"Asset: {name}", content, username))
|
||||
|
||||
Reference in New Issue
Block a user