Add asset detail and user detail UI pages

- /ui/asset/{name}: Shows full asset info (owner, hash, origin, tags, description, ActivityPub URL)
- /ui/user/{username}: Shows user profile with their published assets and activity stats
- Updated users list to link to user detail pages

🤖 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:18:52 +00:00
parent 231cd1653f
commit a0cdf31c36

223
server.py
View File

@@ -513,10 +513,18 @@ async def ui_registry_page(request: Request):
rows = ""
for name, asset in sorted(assets.items(), key=lambda x: x[1].get("created_at", ""), reverse=True):
hash_short = asset.get("content_hash", "")[:16] + "..."
owner = asset.get("owner", "unknown")
asset_type = asset.get("asset_type", "")
type_color = "bg-blue-600" if asset_type == "image" else "bg-purple-600" if asset_type == "video" else "bg-gray-600"
rows += f'''
<tr class="border-b border-dark-500">
<td class="py-3 px-4"><strong class="text-white">{name}</strong></td>
<td class="py-3 px-4 text-gray-400">{asset.get("asset_type", "")}</td>
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
<td class="py-3 px-4">
<a href="/ui/asset/{name}" class="text-blue-400 hover:text-blue-300 font-medium">{name}</a>
</td>
<td class="py-3 px-4"><span class="px-2 py-1 {type_color} text-white text-xs rounded">{asset_type}</span></td>
<td class="py-3 px-4">
<a href="/ui/user/{owner}" class="text-gray-400 hover:text-blue-300">{owner}</a>
</td>
<td class="py-3 px-4"><code class="text-xs bg-dark-600 px-2 py-1 rounded text-green-300">{hash_short}</code></td>
<td class="py-3 px-4 text-gray-400">{", ".join(asset.get("tags", []))}</td>
</tr>
@@ -529,6 +537,7 @@ async def ui_registry_page(request: Request):
<tr class="bg-dark-600 text-left">
<th class="py-3 px-4 font-medium text-gray-300">Name</th>
<th class="py-3 px-4 font-medium text-gray-300">Type</th>
<th class="py-3 px-4 font-medium text-gray-300">Owner</th>
<th class="py-3 px-4 font-medium text-gray-300">Content Hash</th>
<th class="py-3 px-4 font-medium text-gray-300">Tags</th>
</tr>
@@ -601,12 +610,11 @@ async def ui_users_page(request: Request):
'''
else:
rows = ""
for username, user_data in sorted(users.items()):
actor_url = f"https://{DOMAIN}/users/{username}"
webfinger = f"@{username}@{DOMAIN}"
for uname, user_data in sorted(users.items()):
webfinger = f"@{uname}@{DOMAIN}"
rows += f'''
<tr class="border-b border-dark-500">
<td class="py-3 px-4"><a href="{actor_url}" target="_blank" class="text-blue-400 hover:text-blue-300 font-medium">{username}</a></td>
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
<td class="py-3 px-4"><a href="/ui/user/{uname}" class="text-blue-400 hover:text-blue-300 font-medium">{uname}</a></td>
<td class="py-3 px-4"><code class="text-xs bg-dark-600 px-2 py-1 rounded text-gray-300">{webfinger}</code></td>
<td class="py-3 px-4 text-gray-400">{user_data.get("created_at", "")[:10]}</td>
</tr>
@@ -632,6 +640,205 @@ async def ui_users_page(request: Request):
return HTMLResponse(base_html("Users", content, current_user))
@app.get("/ui/asset/{name}", response_class=HTMLResponse)
async def ui_asset_detail(name: str, request: Request):
"""Asset detail page."""
username = get_user_from_cookie(request)
registry = load_registry()
assets = registry.get("assets", {})
if name not in assets:
content = f'''
<h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2>
<p class="text-gray-400">No asset named "{name}" exists.</p>
<p class="mt-4"><a href="/ui/registry" class="text-blue-400 hover:text-blue-300">&larr; Back to Registry</a></p>
'''
return HTMLResponse(base_html("Asset Not Found", content, username))
asset = assets[name]
owner = asset.get("owner", "unknown")
content_hash = asset.get("content_hash", "")
asset_type = asset.get("asset_type", "")
tags = asset.get("tags", [])
description = asset.get("description", "")
origin = asset.get("origin", {})
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"
# Origin display
origin_html = ""
if origin:
origin_type = origin.get("type", "unknown")
if origin_type == "self":
origin_html = '<span class="text-green-400">Original content by author</span>'
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>
'''
if origin_note:
origin_html += f'<p class="text-gray-500 text-sm mt-1">{origin_note}</p>'
# Tags display
tags_html = ""
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>'
content = f'''
<p class="mb-4"><a href="/ui/registry" class="text-blue-400 hover:text-blue-300">&larr; Back to Registry</a></p>
<div class="flex flex-wrap items-center gap-4 mb-6">
<h2 class="text-2xl font-bold text-white">{name}</h2>
<span class="px-3 py-1 {type_color} text-white text-sm rounded">{asset_type}</span>
</div>
<div class="grid gap-6 md:grid-cols-2">
<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">Owner</h3>
<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">Content Hash</h3>
<code class="text-green-300 text-sm break-all">{content_hash}</code>
</div>
<div class="bg-dark-600 rounded-lg p-4">
<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>
<div class="flex flex-wrap gap-2">{tags_html}</div>
</div>
</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>
</div>
'''
return HTMLResponse(base_html(f"Asset: {name}", content, username))
@app.get("/ui/user/{username}", response_class=HTMLResponse)
async def ui_user_detail(username: str, request: Request):
"""User detail page showing their published assets."""
current_user = get_user_from_cookie(request)
if not user_exists(username):
content = f'''
<h2 class="text-xl font-semibold text-white mb-4">User Not Found</h2>
<p class="text-gray-400">No user named "{username}" exists.</p>
<p class="mt-4"><a href="/ui/users" class="text-blue-400 hover:text-blue-300">&larr; Back to Users</a></p>
'''
return HTMLResponse(base_html("User Not Found", content, current_user))
# Get user's assets
registry = load_registry()
all_assets = registry.get("assets", {})
user_assets = {name: asset for name, asset in all_assets.items() if asset.get("owner") == username}
# Get user's activities
all_activities = load_activities()
actor_id = f"https://{DOMAIN}/users/{username}"
user_activities = [a for a in all_activities if a.get("actor_id") == actor_id]
webfinger = f"@{username}@{DOMAIN}"
# Assets table
if user_assets:
rows = ""
for name, asset in sorted(user_assets.items(), key=lambda x: x[1].get("created_at", ""), reverse=True):
hash_short = asset.get("content_hash", "")[:16] + "..."
asset_type = asset.get("asset_type", "")
type_color = "bg-blue-600" if asset_type == "image" else "bg-purple-600" if asset_type == "video" else "bg-gray-600"
rows += f'''
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
<td class="py-3 px-4">
<a href="/ui/asset/{name}" class="text-blue-400 hover:text-blue-300 font-medium">{name}</a>
</td>
<td class="py-3 px-4"><span class="px-2 py-1 {type_color} text-white text-xs rounded">{asset_type}</span></td>
<td class="py-3 px-4"><code class="text-xs bg-dark-600 px-2 py-1 rounded text-green-300">{hash_short}</code></td>
<td class="py-3 px-4 text-gray-400">{", ".join(asset.get("tags", []))}</td>
</tr>
'''
assets_html = f'''
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="bg-dark-600 text-left">
<th class="py-3 px-4 font-medium text-gray-300">Name</th>
<th class="py-3 px-4 font-medium text-gray-300">Type</th>
<th class="py-3 px-4 font-medium text-gray-300">Content Hash</th>
<th class="py-3 px-4 font-medium text-gray-300">Tags</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
'''
else:
assets_html = '<p class="text-gray-400">No published assets yet.</p>'
content = f'''
<p class="mb-4"><a href="/ui/users" class="text-blue-400 hover:text-blue-300">&larr; Back to Users</a></p>
<div class="flex flex-wrap items-center gap-4 mb-6">
<h2 class="text-2xl font-bold text-white">{username}</h2>
<code class="px-3 py-1 bg-dark-600 text-gray-300 text-sm rounded">{webfinger}</code>
</div>
<div class="grid gap-4 sm:grid-cols-2 mb-8">
<div class="bg-dark-600 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-400">{len(user_assets)}</div>
<div class="text-sm text-gray-400 mt-1">Published Assets</div>
</div>
<div class="bg-dark-600 rounded-lg p-4 text-center">
<div class="text-2xl font-bold text-blue-400">{len(user_activities)}</div>
<div class="text-sm text-gray-400 mt-1">Activities</div>
</div>
</div>
<div class="mb-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 mb-1">
Actor URL: <a href="https://{DOMAIN}/users/{username}" target="_blank" class="text-blue-400 hover:text-blue-300">https://{DOMAIN}/users/{username}</a>
</p>
<p class="text-sm text-gray-300">
Outbox: <a href="https://{DOMAIN}/users/{username}/outbox" target="_blank" class="text-blue-400 hover:text-blue-300">https://{DOMAIN}/users/{username}/outbox</a>
</p>
</div>
<h3 class="text-xl font-semibold text-white mb-4">Published Assets ({len(user_assets)})</h3>
{assets_html}
'''
return HTMLResponse(base_html(f"User: {username}", content, current_user))
# ============ API Endpoints ============
@app.get("/")