Remove /ui prefix - clean URLs with content negotiation
- /registry, /activities, /users → HTML (browsers) or JSON (APIs)
- /asset/{name}, /activity/{index}, /users/{username} → same
- Infinite scroll on all list pages via HTMX
- API pagination: ?page=1&limit=20 with has_more
- All internal links updated to use clean URLs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
401
server.py
401
server.py
@@ -275,9 +275,9 @@ def base_html(title: str, content: str, username: str = None) -> str:
|
||||
|
||||
<nav class="flex flex-wrap gap-6 mb-6 pb-4 border-b border-dark-500">
|
||||
<a href="/ui" class="text-gray-400 hover:text-white transition-colors">Home</a>
|
||||
<a href="/ui/registry" class="text-gray-400 hover:text-white transition-colors">Registry</a>
|
||||
<a href="/ui/activities" class="text-gray-400 hover:text-white transition-colors">Activities</a>
|
||||
<a href="/ui/users" class="text-gray-400 hover:text-white transition-colors">Users</a>
|
||||
<a href="/registry" class="text-gray-400 hover:text-white transition-colors">Registry</a>
|
||||
<a href="/activities" class="text-gray-400 hover:text-white transition-colors">Activities</a>
|
||||
<a href="/users" class="text-gray-400 hover:text-white transition-colors">Users</a>
|
||||
</nav>
|
||||
|
||||
<main class="bg-dark-700 rounded-lg p-6">
|
||||
@@ -296,6 +296,12 @@ def get_user_from_cookie(request: Request) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def wants_html(request: Request) -> bool:
|
||||
"""Check if request wants HTML (browser) vs JSON (API)."""
|
||||
accept = request.headers.get("accept", "")
|
||||
return "text/html" in accept and "application/json" not in accept and "application/activity+json" not in accept
|
||||
|
||||
|
||||
# ============ UI Endpoints ============
|
||||
|
||||
@app.get("/ui", response_class=HTMLResponse)
|
||||
@@ -520,11 +526,11 @@ async def ui_registry_page(request: Request):
|
||||
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>
|
||||
<a href="/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>
|
||||
<a href="/users/{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>
|
||||
@@ -578,11 +584,11 @@ async def ui_activities_page(request: Request):
|
||||
<td class="py-3 px-4"><span class="px-2 py-1 {type_color} text-white text-xs font-medium rounded">{activity_type}</span></td>
|
||||
<td class="py-3 px-4 text-white">{obj.get("name", "Untitled")}</td>
|
||||
<td class="py-3 px-4">
|
||||
<a href="/ui/user/{actor_name}" class="text-gray-400 hover:text-blue-300">{actor_name}</a>
|
||||
<a href="/users/{actor_name}" class="text-gray-400 hover:text-blue-300">{actor_name}</a>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-gray-400">{activity.get("published", "")[:10]}</td>
|
||||
<td class="py-3 px-4">
|
||||
<a href="/ui/activity/{activity_index}" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors">View</a>
|
||||
<a href="/activity/{activity_index}" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
@@ -618,7 +624,7 @@ async def ui_activity_detail(activity_index: int, request: Request):
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Activity Not Found</h2>
|
||||
<p class="text-gray-400">This activity does not exist.</p>
|
||||
<p class="mt-4"><a href="/ui/activities" class="text-blue-400 hover:text-blue-300">← Back to Activities</a></p>
|
||||
<p class="mt-4"><a href="/activities" class="text-blue-400 hover:text-blue-300">← Back to Activities</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("Activity Not Found", content, username))
|
||||
|
||||
@@ -802,7 +808,7 @@ async def ui_activity_detail(activity_index: int, request: Request):
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<p class="mb-4"><a href="/ui/activities" class="text-blue-400 hover:text-blue-300">← Back to Activities</a></p>
|
||||
<p class="mb-4"><a href="/activities" class="text-blue-400 hover:text-blue-300">← Back to Activities</a></p>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 mb-6">
|
||||
<span class="px-3 py-1 {type_color} text-white text-sm font-medium rounded">{activity_type}</span>
|
||||
@@ -816,7 +822,7 @@ async def ui_activity_detail(activity_index: int, request: Request):
|
||||
<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">Actor</h3>
|
||||
<a href="/ui/user/{actor_name}" class="text-blue-400 hover:text-blue-300 text-lg">{actor_name}</a>
|
||||
<a href="/users/{actor_name}" class="text-blue-400 hover:text-blue-300 text-lg">{actor_name}</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
@@ -884,7 +890,7 @@ async def ui_users_page(request: Request):
|
||||
webfinger = f"@{uname}@{DOMAIN}"
|
||||
rows += f'''
|
||||
<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"><a href="/users/{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>
|
||||
@@ -921,7 +927,7 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
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>
|
||||
<p class="mt-4"><a href="/registry" class="text-blue-400 hover:text-blue-300">← Back to Registry</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("Asset Not Found", content, username))
|
||||
|
||||
@@ -1093,7 +1099,7 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<p class="mb-4"><a href="/ui/registry" class="text-blue-400 hover:text-blue-300">← Back to Registry</a></p>
|
||||
<p class="mb-4"><a href="/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>
|
||||
@@ -1106,7 +1112,7 @@ async def ui_asset_detail(name: str, request: Request):
|
||||
<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>
|
||||
<a href="/users/{owner}" class="text-blue-400 hover:text-blue-300 text-lg">{owner}</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-600 rounded-lg p-4">
|
||||
@@ -1166,7 +1172,7 @@ async def ui_user_detail(username: str, request: Request):
|
||||
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>
|
||||
<p class="mt-4"><a href="/users" class="text-blue-400 hover:text-blue-300">← Back to Users</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("User Not Found", content, current_user))
|
||||
|
||||
@@ -1192,7 +1198,7 @@ async def ui_user_detail(username: str, request: Request):
|
||||
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>
|
||||
<a href="/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>
|
||||
@@ -1220,7 +1226,7 @@ async def ui_user_detail(username: str, request: Request):
|
||||
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>
|
||||
<p class="mb-4"><a href="/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>
|
||||
@@ -1381,20 +1387,114 @@ async def webfinger(resource: str):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/users")
|
||||
async def get_users_list(request: Request, page: int = 1, limit: int = 20):
|
||||
"""Get all users. HTML for browsers (with infinite scroll), JSON for APIs (with pagination)."""
|
||||
all_users = list(load_users(DATA_DIR).items())
|
||||
total = len(all_users)
|
||||
|
||||
# Sort by username
|
||||
all_users.sort(key=lambda x: x[0])
|
||||
|
||||
# Pagination
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
users_page = all_users[start:end]
|
||||
has_more = end < total
|
||||
|
||||
if wants_html(request):
|
||||
username = get_user_from_cookie(request)
|
||||
|
||||
if not users_page:
|
||||
if page == 1:
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Users</h2>
|
||||
<p class="text-gray-400">No users registered yet.</p>
|
||||
'''
|
||||
else:
|
||||
return HTMLResponse("") # Empty for infinite scroll
|
||||
else:
|
||||
rows = ""
|
||||
for uname, user_data in users_page:
|
||||
webfinger = f"@{uname}@{DOMAIN}"
|
||||
rows += f'''
|
||||
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<a href="/users/{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>
|
||||
'''
|
||||
|
||||
# For infinite scroll, just return rows if not first page
|
||||
if page > 1:
|
||||
if has_more:
|
||||
rows += f'''
|
||||
<tr hx-get="/users?page={page + 1}" hx-trigger="revealed" hx-swap="afterend">
|
||||
<td colspan="3" class="py-4 text-center text-gray-400">Loading more...</td>
|
||||
</tr>
|
||||
'''
|
||||
return HTMLResponse(rows)
|
||||
|
||||
# First page - full content
|
||||
infinite_scroll_trigger = ""
|
||||
if has_more:
|
||||
infinite_scroll_trigger = f'''
|
||||
<tr hx-get="/users?page=2" hx-trigger="revealed" hx-swap="afterend">
|
||||
<td colspan="3" class="py-4 text-center text-gray-400">Loading more...</td>
|
||||
</tr>
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Users ({total} total)</h2>
|
||||
<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">Username</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">WebFinger</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
{infinite_scroll_trigger}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
|
||||
return HTMLResponse(base_html("Users", content, username))
|
||||
|
||||
# JSON response for APIs
|
||||
return {
|
||||
"users": [{"username": uname, **data} for uname, data in users_page],
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"has_more": has_more
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/users/{username}")
|
||||
async def get_actor(username: str, request: Request):
|
||||
"""Get actor profile for any registered user. Content negotiation: HTML for browsers, JSON for APIs."""
|
||||
if not user_exists(username):
|
||||
if wants_html(request):
|
||||
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="/users" class="text-blue-400 hover:text-blue-300">← Back to Users</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("User Not Found", content, get_user_from_cookie(request)))
|
||||
raise HTTPException(404, f"Unknown user: {username}")
|
||||
|
||||
# Check Accept header for content negotiation
|
||||
accept = request.headers.get("accept", "")
|
||||
wants_html = "text/html" in accept and "application/json" not in accept and "application/activity+json" not in accept
|
||||
|
||||
if wants_html:
|
||||
# Redirect to UI page for browsers
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse(url=f"/ui/user/{username}", status_code=303)
|
||||
if wants_html(request):
|
||||
# Render user detail page
|
||||
return await ui_user_detail(username, request)
|
||||
|
||||
actor = load_actor(username)
|
||||
|
||||
@@ -1496,14 +1596,130 @@ async def get_followers(username: str):
|
||||
# ============ Registry Endpoints ============
|
||||
|
||||
@app.get("/registry")
|
||||
async def get_registry():
|
||||
"""Get full registry."""
|
||||
return load_registry()
|
||||
async def get_registry(request: Request, page: int = 1, limit: int = 20):
|
||||
"""Get registry. HTML for browsers (with infinite scroll), JSON for APIs (with pagination)."""
|
||||
registry = load_registry()
|
||||
all_assets = list(registry.get("assets", {}).items())
|
||||
total = len(all_assets)
|
||||
|
||||
# Sort by created_at descending
|
||||
all_assets.sort(key=lambda x: x[1].get("created_at", ""), reverse=True)
|
||||
|
||||
# Pagination
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
assets_page = all_assets[start:end]
|
||||
has_more = end < total
|
||||
|
||||
if wants_html(request):
|
||||
username = get_user_from_cookie(request)
|
||||
|
||||
if not assets_page:
|
||||
if page == 1:
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Registry</h2>
|
||||
<p class="text-gray-400">No assets registered yet.</p>
|
||||
'''
|
||||
else:
|
||||
return HTMLResponse("") # Empty for infinite scroll
|
||||
else:
|
||||
rows = ""
|
||||
for name, asset in assets_page:
|
||||
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"
|
||||
owner = asset.get("owner", "unknown")
|
||||
content_hash = asset.get("content_hash", "")[:16] + "..."
|
||||
rows += f'''
|
||||
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
|
||||
<td class="py-3 px-4 text-white font-medium">{name}</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="/users/{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">{content_hash}</code></td>
|
||||
<td class="py-3 px-4">
|
||||
<a href="/asset/{name}" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
|
||||
# For infinite scroll, just return rows if not first page
|
||||
if page > 1:
|
||||
if has_more:
|
||||
rows += f'''
|
||||
<tr hx-get="/registry?page={page + 1}" hx-trigger="revealed" hx-swap="afterend">
|
||||
<td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td>
|
||||
</tr>
|
||||
'''
|
||||
return HTMLResponse(rows)
|
||||
|
||||
# First page - full content
|
||||
infinite_scroll_trigger = ""
|
||||
if has_more:
|
||||
infinite_scroll_trigger = f'''
|
||||
<tr hx-get="/registry?page=2" hx-trigger="revealed" hx-swap="afterend">
|
||||
<td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td>
|
||||
</tr>
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Registry ({total} assets)</h2>
|
||||
<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">Owner</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Hash</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
{infinite_scroll_trigger}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
|
||||
return HTMLResponse(base_html("Registry", content, username))
|
||||
|
||||
# JSON response for APIs
|
||||
return {
|
||||
"assets": {name: asset for name, asset in assets_page},
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"has_more": has_more
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/asset/{name}")
|
||||
async def get_asset_by_name(name: str, request: Request):
|
||||
"""Get asset by name. HTML for browsers, JSON for APIs."""
|
||||
registry = load_registry()
|
||||
if name not in registry.get("assets", {}):
|
||||
if wants_html(request):
|
||||
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="/registry" class="text-blue-400 hover:text-blue-300">← Back to Registry</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("Asset Not Found", content, get_user_from_cookie(request)))
|
||||
raise HTTPException(404, f"Asset not found: {name}")
|
||||
|
||||
if wants_html(request):
|
||||
return await ui_asset_detail(name, request)
|
||||
|
||||
return registry["assets"][name]
|
||||
|
||||
|
||||
@app.get("/registry/{name}")
|
||||
async def get_asset(name: str):
|
||||
"""Get a specific asset."""
|
||||
"""Get a specific asset (API only, use /asset/{name} for content negotiation)."""
|
||||
registry = load_registry()
|
||||
if name not in registry.get("assets", {}):
|
||||
raise HTTPException(404, f"Asset not found: {name}")
|
||||
@@ -1777,9 +1993,130 @@ async def publish_cache(req: PublishCacheRequest, user: User = Depends(get_requi
|
||||
# ============ Activities Endpoints ============
|
||||
|
||||
@app.get("/activities")
|
||||
async def get_activities():
|
||||
"""Get all activities."""
|
||||
return {"activities": load_activities()}
|
||||
async def get_activities(request: Request, page: int = 1, limit: int = 20):
|
||||
"""Get activities. HTML for browsers (with infinite scroll), JSON for APIs (with pagination)."""
|
||||
all_activities = load_activities()
|
||||
total = len(all_activities)
|
||||
|
||||
# Reverse for newest first
|
||||
all_activities = list(reversed(all_activities))
|
||||
|
||||
# Pagination
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
activities_page = all_activities[start:end]
|
||||
has_more = end < total
|
||||
|
||||
if wants_html(request):
|
||||
username = get_user_from_cookie(request)
|
||||
|
||||
if not activities_page:
|
||||
if page == 1:
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Activities</h2>
|
||||
<p class="text-gray-400">No activities yet.</p>
|
||||
'''
|
||||
else:
|
||||
return HTMLResponse("") # Empty for infinite scroll
|
||||
else:
|
||||
rows = ""
|
||||
for i, activity in enumerate(activities_page):
|
||||
activity_index = total - 1 - (start + i) # Original index
|
||||
obj = activity.get("object_data", {})
|
||||
activity_type = activity.get("activity_type", "")
|
||||
type_color = "bg-green-600" if activity_type == "Create" else "bg-yellow-600" if activity_type == "Update" else "bg-gray-600"
|
||||
actor_id = activity.get("actor_id", "")
|
||||
actor_name = actor_id.split("/")[-1] if actor_id else "unknown"
|
||||
rows += f'''
|
||||
<tr class="border-b border-dark-500 hover:bg-dark-600 transition-colors">
|
||||
<td class="py-3 px-4"><span class="px-2 py-1 {type_color} text-white text-xs font-medium rounded">{activity_type}</span></td>
|
||||
<td class="py-3 px-4 text-white">{obj.get("name", "Untitled")}</td>
|
||||
<td class="py-3 px-4">
|
||||
<a href="/users/{actor_name}" class="text-gray-400 hover:text-blue-300">{actor_name}</a>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-gray-400">{activity.get("published", "")[:10]}</td>
|
||||
<td class="py-3 px-4">
|
||||
<a href="/activity/{activity_index}" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
|
||||
# For infinite scroll, just return rows if not first page
|
||||
if page > 1:
|
||||
if has_more:
|
||||
rows += f'''
|
||||
<tr hx-get="/activities?page={page + 1}" hx-trigger="revealed" hx-swap="afterend">
|
||||
<td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td>
|
||||
</tr>
|
||||
'''
|
||||
return HTMLResponse(rows)
|
||||
|
||||
# First page - full content with header
|
||||
infinite_scroll_trigger = ""
|
||||
if has_more:
|
||||
infinite_scroll_trigger = f'''
|
||||
<tr hx-get="/activities?page=2" hx-trigger="revealed" hx-swap="afterend">
|
||||
<td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td>
|
||||
</tr>
|
||||
'''
|
||||
|
||||
content = f'''
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Activities ({total} total)</h2>
|
||||
<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">Type</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Object</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Actor</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Published</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
{infinite_scroll_trigger}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
|
||||
return HTMLResponse(base_html("Activities", content, username))
|
||||
|
||||
# JSON response for APIs
|
||||
return {
|
||||
"activities": activities_page,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"has_more": has_more
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/activity/{activity_index}")
|
||||
async def get_activity(activity_index: int, request: Request):
|
||||
"""Get single activity. HTML for browsers, JSON for APIs."""
|
||||
activities = load_activities()
|
||||
|
||||
if activity_index < 0 or activity_index >= len(activities):
|
||||
if wants_html(request):
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Activity Not Found</h2>
|
||||
<p class="text-gray-400">This activity does not exist.</p>
|
||||
<p class="mt-4"><a href="/activities" class="text-blue-400 hover:text-blue-300">← Back to Activities</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("Activity Not Found", content, get_user_from_cookie(request)))
|
||||
raise HTTPException(404, "Activity not found")
|
||||
|
||||
activity = activities[activity_index]
|
||||
|
||||
if wants_html(request):
|
||||
# Reuse the UI activity detail logic
|
||||
return await ui_activity_detail(activity_index, request)
|
||||
|
||||
return activity
|
||||
|
||||
|
||||
@app.get("/objects/{content_hash}")
|
||||
|
||||
Reference in New Issue
Block a user