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 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 pydantic import BaseModel
import requests
@@ -243,16 +243,15 @@ def base_html(title: str, content: str, username: str = None) -> str:
user_section = f'''
<div class="flex items-center gap-4 text-sm text-gray-400">
Logged in as <strong class="text-white">{username}</strong>
<button hx-post="/ui/logout" hx-swap="innerHTML" hx-target="body"
class="px-3 py-1 bg-dark-500 hover:bg-dark-600 rounded text-blue-400 hover:text-blue-300 transition-colors">
<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">
Logout
</button>
</a>
</div>
''' if username else '''
<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>
<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>
'''
@@ -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">
<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">
<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>
{user_section}
</header>
<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="/registry" class="text-gray-400 hover:text-white transition-colors">Registry</a>
<a href="/assets" class="text-gray-400 hover:text-white transition-colors">Assets</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>
@@ -345,7 +343,7 @@ async def ui_home(request: Request):
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):
"""Login page."""
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">
You are already logged in as <strong>{username}</strong>
</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))
content = '''
<h2 class="text-xl font-semibold text-white mb-6">Login</h2>
<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>
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label>
<input type="text" id="username" name="username" required
@@ -375,12 +373,12 @@ async def ui_login_page(request: Request):
Login
</button>
</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))
@app.post("/ui/login", response_class=HTMLResponse)
@app.post("/login", response_class=HTMLResponse)
async def ui_login_submit(request: Request):
"""Handle login form submission."""
form = await request.form()
@@ -398,7 +396,7 @@ async def ui_login_submit(request: Request):
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>
<script>window.location.href = "/ui";</script>
<script>window.location.href = "/";</script>
''')
response.set_cookie(
key="auth_token",
@@ -410,7 +408,7 @@ async def ui_login_submit(request: Request):
return response
@app.get("/ui/register", response_class=HTMLResponse)
@app.get("/register", response_class=HTMLResponse)
async def ui_register_page(request: Request):
"""Register page."""
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">
You are already logged in as <strong>{username}</strong>
</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))
content = '''
<h2 class="text-xl font-semibold text-white mb-6">Register</h2>
<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>
<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"
@@ -450,12 +448,12 @@ async def ui_register_page(request: Request):
Register
</button>
</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))
@app.post("/ui/register", response_class=HTMLResponse)
@app.post("/register", response_class=HTMLResponse)
async def ui_register_submit(request: Request):
"""Handle register form submission."""
form = await request.form()
@@ -482,7 +480,7 @@ async def ui_register_submit(request: Request):
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>
<script>window.location.href = "/ui";</script>
<script>window.location.href = "/";</script>
''')
response.set_cookie(
key="auth_token",
@@ -494,17 +492,15 @@ async def ui_register_submit(request: Request):
return response
@app.post("/ui/logout", response_class=HTMLResponse)
async def ui_logout():
"""Handle logout."""
response = HTMLResponse('''
<script>window.location.href = "/ui";</script>
''')
@app.get("/logout")
async def logout():
"""Handle logout - clear cookie and redirect to home."""
response = RedirectResponse(url="/", status_code=302)
response.delete_cookie("auth_token")
return response
@app.get("/ui/registry", response_class=HTMLResponse)
@app.get("/ui/assets", response_class=HTMLResponse)
async def ui_registry_page(request: Request):
"""Registry page showing all assets."""
username = get_user_from_cookie(request)
@@ -927,7 +923,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="/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))
@@ -1099,7 +1095,7 @@ async def ui_asset_detail(name: str, request: Request):
'''
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">
<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("/")
async def root(request: Request):
"""Server info or redirect to UI."""
# 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"})
"""Server info. HTML shows home page with counts, JSON returns stats."""
registry = load_registry()
activities = load_activities()
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 {
"name": "Art DAG L2 Server",
"version": "0.1.0",
"domain": DOMAIN,
"assets_count": len(registry.get("assets", {})),
"activities_count": len(activities),
"users_count": len(users)
"assets_count": assets_count,
"activities_count": activities_count,
"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):
"""Get registry. HTML for browsers (with infinite scroll), JSON for APIs (with pagination)."""
registry = load_registry()
@@ -1647,7 +1663,7 @@ async def get_registry(request: Request, page: int = 1, limit: int = 20):
if page > 1:
if has_more:
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>
</tr>
'''
@@ -1657,7 +1673,7 @@ async def get_registry(request: Request, page: int = 1, limit: int = 20):
infinite_scroll_trigger = ""
if has_more:
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>
</tr>
'''
@@ -1706,7 +1722,7 @@ async def get_asset_by_name(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="/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)))
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]
@app.get("/registry/{name}")
@app.get("/assets/{name}")
async def get_asset(name: str):
"""Get a specific asset (API only, use /asset/{name} for content negotiation)."""
registry = load_registry()
@@ -1726,7 +1742,7 @@ async def get_asset(name: str):
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)):
"""Update an existing asset's metadata. Creates an Update activity."""
registry = load_registry()
@@ -1849,13 +1865,13 @@ def _register_asset_impl(req: RegisterRequest, owner: str):
return {"asset": asset, "activity": activity}
@app.post("/registry")
@app.post("/assets")
async def register_asset(req: RegisterRequest, user: User = Depends(get_required_user)):
"""Register a new asset and create ownership activity. Requires authentication."""
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)):
"""Record an L1 run and register the output. Requires authentication."""
# 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)
@app.post("/registry/publish-cache")
@app.post("/assets/publish-cache")
async def publish_cache(req: PublishCacheRequest, user: User = Depends(get_required_user)):
"""
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
if wants_html:
# Redirect to UI page for browsers
from fastapi.responses import RedirectResponse
return RedirectResponse(url=f"/ui/asset/{name}", status_code=303)
# Redirect to detail page for browsers
return RedirectResponse(url=f"/asset/{name}", status_code=303)
owner = asset.get("owner", "unknown")
return JSONResponse(