Clean URLs, rename registry to assets, home page with counts

- Home page shows asset/activity/user counts with links (not redirect)
- Rename /registry to /assets everywhere
- Clean auth routes: /login, /logout, /register
- Update all navigation links to clean URLs
- Remove /ui prefix from main links

🤖 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 23:21:22 +00:00
parent 11fa01a864
commit cb848aacbe

119
server.py
View File

@@ -19,7 +19,7 @@ from typing import Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi import FastAPI, HTTPException, Request, Response, Depends, Cookie from fastapi import FastAPI, HTTPException, Request, Response, Depends, Cookie
from fastapi.responses import JSONResponse, HTMLResponse from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel from pydantic import BaseModel
import requests import requests
@@ -243,16 +243,15 @@ def base_html(title: str, content: str, username: str = None) -> str:
user_section = f''' user_section = f'''
<div class="flex items-center gap-4 text-sm text-gray-400"> <div class="flex items-center gap-4 text-sm text-gray-400">
Logged in as <strong class="text-white">{username}</strong> Logged in as <strong class="text-white">{username}</strong>
<button hx-post="/ui/logout" hx-swap="innerHTML" hx-target="body" <a href="/logout" class="px-3 py-1 bg-dark-500 hover:bg-dark-600 rounded text-blue-400 hover:text-blue-300 transition-colors">
class="px-3 py-1 bg-dark-500 hover:bg-dark-600 rounded text-blue-400 hover:text-blue-300 transition-colors">
Logout Logout
</button> </a>
</div> </div>
''' if username else ''' ''' if username else '''
<div class="flex items-center gap-4 text-sm"> <div class="flex items-center gap-4 text-sm">
<a href="/ui/login" class="text-blue-400 hover:text-blue-300">Login</a> <a href="/login" class="text-blue-400 hover:text-blue-300">Login</a>
<span class="text-gray-500">|</span> <span class="text-gray-500">|</span>
<a href="/ui/register" class="text-blue-400 hover:text-blue-300">Register</a> <a href="/register" class="text-blue-400 hover:text-blue-300">Register</a>
</div> </div>
''' '''
@@ -268,14 +267,13 @@ def base_html(title: str, content: str, username: str = None) -> str:
<div class="max-w-5xl mx-auto px-4 py-6 sm:px-6 lg:px-8"> <div class="max-w-5xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<header class="flex flex-wrap items-center justify-between gap-4 mb-6 pb-4 border-b border-dark-500"> <header class="flex flex-wrap items-center justify-between gap-4 mb-6 pb-4 border-b border-dark-500">
<h1 class="text-2xl font-bold"> <h1 class="text-2xl font-bold">
<a href="/ui" class="text-white hover:text-gray-200">Art DAG L2</a> <a href="/" class="text-white hover:text-gray-200">Art DAG L2</a>
</h1> </h1>
{user_section} {user_section}
</header> </header>
<nav class="flex flex-wrap gap-6 mb-6 pb-4 border-b border-dark-500"> <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="/assets" class="text-gray-400 hover:text-white transition-colors">Assets</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="/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> <a href="/users" class="text-gray-400 hover:text-white transition-colors">Users</a>
</nav> </nav>
@@ -345,7 +343,7 @@ async def ui_home(request: Request):
return HTMLResponse(base_html("Home", content, username)) return HTMLResponse(base_html("Home", content, username))
@app.get("/ui/login", response_class=HTMLResponse) @app.get("/login", response_class=HTMLResponse)
async def ui_login_page(request: Request): async def ui_login_page(request: Request):
"""Login page.""" """Login page."""
username = get_user_from_cookie(request) username = get_user_from_cookie(request)
@@ -354,13 +352,13 @@ async def ui_login_page(request: Request):
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4"> <div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
You are already logged in as <strong>{username}</strong> You are already logged in as <strong>{username}</strong>
</div> </div>
<p><a href="/ui" class="text-blue-400 hover:text-blue-300">Go to home page</a></p> <p><a href="/" class="text-blue-400 hover:text-blue-300">Go to home page</a></p>
''', username)) ''', username))
content = ''' content = '''
<h2 class="text-xl font-semibold text-white mb-6">Login</h2> <h2 class="text-xl font-semibold text-white mb-6">Login</h2>
<div id="login-result"></div> <div id="login-result"></div>
<form hx-post="/ui/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-4 max-w-md"> <form hx-post="/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-4 max-w-md">
<div> <div>
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label> <label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label>
<input type="text" id="username" name="username" required <input type="text" id="username" name="username" required
@@ -375,12 +373,12 @@ async def ui_login_page(request: Request):
Login Login
</button> </button>
</form> </form>
<p class="mt-6 text-gray-400">Don't have an account? <a href="/ui/register" class="text-blue-400 hover:text-blue-300">Register</a></p> <p class="mt-6 text-gray-400">Don't have an account? <a href="/register" class="text-blue-400 hover:text-blue-300">Register</a></p>
''' '''
return HTMLResponse(base_html("Login", content)) return HTMLResponse(base_html("Login", content))
@app.post("/ui/login", response_class=HTMLResponse) @app.post("/login", response_class=HTMLResponse)
async def ui_login_submit(request: Request): async def ui_login_submit(request: Request):
"""Handle login form submission.""" """Handle login form submission."""
form = await request.form() form = await request.form()
@@ -398,7 +396,7 @@ async def ui_login_submit(request: Request):
response = HTMLResponse(f''' response = HTMLResponse(f'''
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">Login successful! Redirecting...</div> <div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">Login successful! Redirecting...</div>
<script>window.location.href = "/ui";</script> <script>window.location.href = "/";</script>
''') ''')
response.set_cookie( response.set_cookie(
key="auth_token", key="auth_token",
@@ -410,7 +408,7 @@ async def ui_login_submit(request: Request):
return response return response
@app.get("/ui/register", response_class=HTMLResponse) @app.get("/register", response_class=HTMLResponse)
async def ui_register_page(request: Request): async def ui_register_page(request: Request):
"""Register page.""" """Register page."""
username = get_user_from_cookie(request) username = get_user_from_cookie(request)
@@ -419,13 +417,13 @@ async def ui_register_page(request: Request):
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4"> <div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
You are already logged in as <strong>{username}</strong> You are already logged in as <strong>{username}</strong>
</div> </div>
<p><a href="/ui" class="text-blue-400 hover:text-blue-300">Go to home page</a></p> <p><a href="/" class="text-blue-400 hover:text-blue-300">Go to home page</a></p>
''', username)) ''', username))
content = ''' content = '''
<h2 class="text-xl font-semibold text-white mb-6">Register</h2> <h2 class="text-xl font-semibold text-white mb-6">Register</h2>
<div id="register-result"></div> <div id="register-result"></div>
<form hx-post="/ui/register" hx-target="#register-result" hx-swap="innerHTML" class="space-y-4 max-w-md"> <form hx-post="/register" hx-target="#register-result" hx-swap="innerHTML" class="space-y-4 max-w-md">
<div> <div>
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label> <label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label>
<input type="text" id="username" name="username" required pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, underscore, dash only" <input type="text" id="username" name="username" required pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, underscore, dash only"
@@ -450,12 +448,12 @@ async def ui_register_page(request: Request):
Register Register
</button> </button>
</form> </form>
<p class="mt-6 text-gray-400">Already have an account? <a href="/ui/login" class="text-blue-400 hover:text-blue-300">Login</a></p> <p class="mt-6 text-gray-400">Already have an account? <a href="/login" class="text-blue-400 hover:text-blue-300">Login</a></p>
''' '''
return HTMLResponse(base_html("Register", content)) return HTMLResponse(base_html("Register", content))
@app.post("/ui/register", response_class=HTMLResponse) @app.post("/register", response_class=HTMLResponse)
async def ui_register_submit(request: Request): async def ui_register_submit(request: Request):
"""Handle register form submission.""" """Handle register form submission."""
form = await request.form() form = await request.form()
@@ -482,7 +480,7 @@ async def ui_register_submit(request: Request):
response = HTMLResponse(f''' response = HTMLResponse(f'''
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">Registration successful! Redirecting...</div> <div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">Registration successful! Redirecting...</div>
<script>window.location.href = "/ui";</script> <script>window.location.href = "/";</script>
''') ''')
response.set_cookie( response.set_cookie(
key="auth_token", key="auth_token",
@@ -494,17 +492,15 @@ async def ui_register_submit(request: Request):
return response return response
@app.post("/ui/logout", response_class=HTMLResponse) @app.get("/logout")
async def ui_logout(): async def logout():
"""Handle logout.""" """Handle logout - clear cookie and redirect to home."""
response = HTMLResponse(''' response = RedirectResponse(url="/", status_code=302)
<script>window.location.href = "/ui";</script>
''')
response.delete_cookie("auth_token") response.delete_cookie("auth_token")
return response return response
@app.get("/ui/registry", response_class=HTMLResponse) @app.get("/ui/assets", response_class=HTMLResponse)
async def ui_registry_page(request: Request): async def ui_registry_page(request: Request):
"""Registry page showing all assets.""" """Registry page showing all assets."""
username = get_user_from_cookie(request) username = get_user_from_cookie(request)
@@ -927,7 +923,7 @@ async def ui_asset_detail(name: str, request: Request):
content = f''' content = f'''
<h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2> <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="text-gray-400">No asset named "{name}" exists.</p>
<p class="mt-4"><a href="/registry" class="text-blue-400 hover:text-blue-300">&larr; Back to Registry</a></p> <p class="mt-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">&larr; Back to Assets</a></p>
''' '''
return HTMLResponse(base_html("Asset Not Found", content, username)) return HTMLResponse(base_html("Asset Not Found", content, username))
@@ -1099,7 +1095,7 @@ async def ui_asset_detail(name: str, request: Request):
''' '''
content = f''' content = f'''
<p class="mb-4"><a href="/registry" class="text-blue-400 hover:text-blue-300">&larr; Back to Registry</a></p> <p class="mb-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">&larr; Back to Assets</a></p>
<div class="flex flex-wrap items-center gap-4 mb-6"> <div class="flex flex-wrap items-center gap-4 mb-6">
<h2 class="text-2xl font-bold text-white">{name}</h2> <h2 class="text-2xl font-bold text-white">{name}</h2>
@@ -1264,22 +1260,42 @@ async def ui_user_detail(username: str, request: Request):
@app.get("/") @app.get("/")
async def root(request: Request): async def root(request: Request):
"""Server info or redirect to UI.""" """Server info. HTML shows home page with counts, JSON returns stats."""
# If browser, redirect to UI
accept = request.headers.get("accept", "")
if "text/html" in accept and "application/json" not in accept:
return HTMLResponse(status_code=302, headers={"Location": "/ui"})
registry = load_registry() registry = load_registry()
activities = load_activities() activities = load_activities()
users = load_users(DATA_DIR) users = load_users(DATA_DIR)
assets_count = len(registry.get("assets", {}))
activities_count = len(activities)
users_count = len(users)
if wants_html(request):
username = get_user_from_cookie(request)
content = f'''
<div class="grid gap-6 md:grid-cols-3">
<a href="/assets" class="block bg-dark-600 rounded-lg p-6 hover:bg-dark-500 transition-colors">
<div class="text-4xl font-bold text-white mb-2">{assets_count}</div>
<div class="text-gray-400">Assets</div>
</a>
<a href="/activities" class="block bg-dark-600 rounded-lg p-6 hover:bg-dark-500 transition-colors">
<div class="text-4xl font-bold text-white mb-2">{activities_count}</div>
<div class="text-gray-400">Activities</div>
</a>
<a href="/users" class="block bg-dark-600 rounded-lg p-6 hover:bg-dark-500 transition-colors">
<div class="text-4xl font-bold text-white mb-2">{users_count}</div>
<div class="text-gray-400">Users</div>
</a>
</div>
'''
return HTMLResponse(base_html("Home", content, username))
return { return {
"name": "Art DAG L2 Server", "name": "Art DAG L2 Server",
"version": "0.1.0", "version": "0.1.0",
"domain": DOMAIN, "domain": DOMAIN,
"assets_count": len(registry.get("assets", {})), "assets_count": assets_count,
"activities_count": len(activities), "activities_count": activities_count,
"users_count": len(users) "users_count": users_count
} }
@@ -1593,9 +1609,9 @@ async def get_followers(username: str):
) )
# ============ Registry Endpoints ============ # ============ Assets Endpoints ============
@app.get("/registry") @app.get("/assets")
async def get_registry(request: Request, page: int = 1, limit: int = 20): 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).""" """Get registry. HTML for browsers (with infinite scroll), JSON for APIs (with pagination)."""
registry = load_registry() registry = load_registry()
@@ -1647,7 +1663,7 @@ async def get_registry(request: Request, page: int = 1, limit: int = 20):
if page > 1: if page > 1:
if has_more: if has_more:
rows += f''' rows += f'''
<tr hx-get="/registry?page={page + 1}" hx-trigger="revealed" hx-swap="afterend"> <tr hx-get="/assets?page={page + 1}" hx-trigger="revealed" hx-swap="afterend">
<td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td> <td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td>
</tr> </tr>
''' '''
@@ -1657,7 +1673,7 @@ async def get_registry(request: Request, page: int = 1, limit: int = 20):
infinite_scroll_trigger = "" infinite_scroll_trigger = ""
if has_more: if has_more:
infinite_scroll_trigger = f''' infinite_scroll_trigger = f'''
<tr hx-get="/registry?page=2" hx-trigger="revealed" hx-swap="afterend"> <tr hx-get="/assets?page=2" hx-trigger="revealed" hx-swap="afterend">
<td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td> <td colspan="5" class="py-4 text-center text-gray-400">Loading more...</td>
</tr> </tr>
''' '''
@@ -1706,7 +1722,7 @@ async def get_asset_by_name(name: str, request: Request):
content = f''' content = f'''
<h2 class="text-xl font-semibold text-white mb-4">Asset Not Found</h2> <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="text-gray-400">No asset named "{name}" exists.</p>
<p class="mt-4"><a href="/registry" class="text-blue-400 hover:text-blue-300">&larr; Back to Registry</a></p> <p class="mt-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">&larr; Back to Assets</a></p>
''' '''
return HTMLResponse(base_html("Asset Not Found", content, get_user_from_cookie(request))) return HTMLResponse(base_html("Asset Not Found", content, get_user_from_cookie(request)))
raise HTTPException(404, f"Asset not found: {name}") raise HTTPException(404, f"Asset not found: {name}")
@@ -1717,7 +1733,7 @@ async def get_asset_by_name(name: str, request: Request):
return registry["assets"][name] return registry["assets"][name]
@app.get("/registry/{name}") @app.get("/assets/{name}")
async def get_asset(name: str): async def get_asset(name: str):
"""Get a specific asset (API only, use /asset/{name} for content negotiation).""" """Get a specific asset (API only, use /asset/{name} for content negotiation)."""
registry = load_registry() registry = load_registry()
@@ -1726,7 +1742,7 @@ async def get_asset(name: str):
return registry["assets"][name] return registry["assets"][name]
@app.patch("/registry/{name}") @app.patch("/assets/{name}")
async def update_asset(name: str, req: UpdateAssetRequest, user: User = Depends(get_required_user)): async def update_asset(name: str, req: UpdateAssetRequest, user: User = Depends(get_required_user)):
"""Update an existing asset's metadata. Creates an Update activity.""" """Update an existing asset's metadata. Creates an Update activity."""
registry = load_registry() registry = load_registry()
@@ -1849,13 +1865,13 @@ def _register_asset_impl(req: RegisterRequest, owner: str):
return {"asset": asset, "activity": activity} return {"asset": asset, "activity": activity}
@app.post("/registry") @app.post("/assets")
async def register_asset(req: RegisterRequest, user: User = Depends(get_required_user)): async def register_asset(req: RegisterRequest, user: User = Depends(get_required_user)):
"""Register a new asset and create ownership activity. Requires authentication.""" """Register a new asset and create ownership activity. Requires authentication."""
return _register_asset_impl(req, user.username) return _register_asset_impl(req, user.username)
@app.post("/registry/record-run") @app.post("/assets/record-run")
async def record_run(req: RecordRunRequest, user: User = Depends(get_required_user)): async def record_run(req: RecordRunRequest, user: User = Depends(get_required_user)):
"""Record an L1 run and register the output. Requires authentication.""" """Record an L1 run and register the output. Requires authentication."""
# Fetch run from the specified L1 server # Fetch run from the specified L1 server
@@ -1897,7 +1913,7 @@ async def record_run(req: RecordRunRequest, user: User = Depends(get_required_us
), user.username) ), user.username)
@app.post("/registry/publish-cache") @app.post("/assets/publish-cache")
async def publish_cache(req: PublishCacheRequest, user: User = Depends(get_required_user)): async def publish_cache(req: PublishCacheRequest, user: User = Depends(get_required_user)):
""" """
Publish a cache item from L1 with metadata. Publish a cache item from L1 with metadata.
@@ -2132,9 +2148,8 @@ async def get_object(content_hash: str, request: Request):
wants_html = "text/html" in accept and "application/json" not in accept and "application/activity+json" not in accept wants_html = "text/html" in accept and "application/json" not in accept and "application/activity+json" not in accept
if wants_html: if wants_html:
# Redirect to UI page for browsers # Redirect to detail page for browsers
from fastapi.responses import RedirectResponse return RedirectResponse(url=f"/asset/{name}", status_code=303)
return RedirectResponse(url=f"/ui/asset/{name}", status_code=303)
owner = asset.get("owner", "unknown") owner = asset.get("owner", "unknown")
return JSONResponse( return JSONResponse(