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:
119
server.py
119
server.py
@@ -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">← Back to Registry</a></p>
|
||||
<p class="mt-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">← 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">← Back to Registry</a></p>
|
||||
<p class="mb-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">← 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">← Back to Registry</a></p>
|
||||
<p class="mt-4"><a href="/assets" class="text-blue-400 hover:text-blue-300">← 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(
|
||||
|
||||
Reference in New Issue
Block a user