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:
223
server.py
223
server.py
@@ -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">← 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">← 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">← 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">← 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("/")
|
||||
|
||||
Reference in New Issue
Block a user