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:
gilesb
2026-01-07 21:26:14 +00:00
parent a0cdf31c36
commit c4b861c553

159
server.py
View File

@@ -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">&larr; 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))