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 = ""
|
rows = ""
|
||||||
for name, asset in sorted(assets.items(), key=lambda x: x[1].get("created_at", ""), reverse=True):
|
for name, asset in sorted(assets.items(), key=lambda x: x[1].get("created_at", ""), reverse=True):
|
||||||
hash_short = asset.get("content_hash", "")[:16] + "..."
|
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'''
|
rows += f'''
|
||||||
<tr class="border-b border-dark-500">
|
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
|
||||||
<td class="py-3 px-4"><strong class="text-white">{name}</strong></td>
|
<td class="py-3 px-4">
|
||||||
<td class="py-3 px-4 text-gray-400">{asset.get("asset_type", "")}</td>
|
<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"><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>
|
<td class="py-3 px-4 text-gray-400">{", ".join(asset.get("tags", []))}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -529,6 +537,7 @@ async def ui_registry_page(request: Request):
|
|||||||
<tr class="bg-dark-600 text-left">
|
<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">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">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">Content Hash</th>
|
||||||
<th class="py-3 px-4 font-medium text-gray-300">Tags</th>
|
<th class="py-3 px-4 font-medium text-gray-300">Tags</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -601,12 +610,11 @@ async def ui_users_page(request: Request):
|
|||||||
'''
|
'''
|
||||||
else:
|
else:
|
||||||
rows = ""
|
rows = ""
|
||||||
for username, user_data in sorted(users.items()):
|
for uname, user_data in sorted(users.items()):
|
||||||
actor_url = f"https://{DOMAIN}/users/{username}"
|
webfinger = f"@{uname}@{DOMAIN}"
|
||||||
webfinger = f"@{username}@{DOMAIN}"
|
|
||||||
rows += f'''
|
rows += f'''
|
||||||
<tr class="border-b border-dark-500">
|
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
|
||||||
<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>
|
<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"><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>
|
<td class="py-3 px-4 text-gray-400">{user_data.get("created_at", "")[:10]}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -632,6 +640,205 @@ async def ui_users_page(request: Request):
|
|||||||
return HTMLResponse(base_html("Users", content, current_user))
|
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 ============
|
# ============ API Endpoints ============
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
Reference in New Issue
Block a user