Files
activity-pub/app/routers/renderers.py
giles d1e9287829 Add modular app structure for L2 server
- Create app factory with routers and templates
- Auth, assets, activities, anchors, storage, users, renderers routers
- Federation router for WebFinger and nodeinfo
- Jinja2 templates for L2 pages
- Config and dependency injection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:46:26 +00:00

151 lines
4.0 KiB
Python

"""
Renderer (L1) management routes for L2 server.
Handles L1 server attachments and delegation.
"""
import logging
from typing import Optional
from fastapi import APIRouter, Request, Depends, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from artdag_common import render
from artdag_common.middleware import wants_html, wants_json
from ..config import settings
from ..dependencies import get_templates, require_auth, get_user_from_cookie
router = APIRouter()
logger = logging.getLogger(__name__)
class AttachRendererRequest(BaseModel):
l1_url: str
@router.get("")
async def list_renderers(
request: Request,
user: dict = Depends(require_auth),
):
"""List attached L1 renderers."""
import db
renderers = await db.get_user_renderers(user["username"])
# Add status info
for r in renderers:
r["known"] = r["url"] in settings.l1_servers
if wants_json(request):
return {"renderers": renderers, "known_servers": settings.l1_servers}
templates = get_templates(request)
return render(templates, "renderers/list.html", request,
renderers=renderers,
known_servers=settings.l1_servers,
user=user,
active_tab="renderers",
)
@router.post("")
async def attach_renderer(
req: AttachRendererRequest,
user: dict = Depends(require_auth),
):
"""Attach an L1 renderer."""
import db
l1_url = req.l1_url.rstrip("/")
# Validate URL
if not l1_url.startswith("http"):
raise HTTPException(400, "Invalid URL")
# Check if already attached
existing = await db.get_user_renderers(user["username"])
if l1_url in [r["url"] for r in existing]:
raise HTTPException(400, "Renderer already attached")
# Test connection
try:
import requests
resp = requests.get(f"{l1_url}/health", timeout=10)
if resp.status_code != 200:
raise HTTPException(400, f"L1 server not healthy: {resp.status_code}")
except Exception as e:
raise HTTPException(400, f"Failed to connect to L1: {e}")
# Attach
await db.attach_renderer(user["username"], l1_url)
return {"attached": True, "url": l1_url}
@router.delete("/{renderer_id}")
async def detach_renderer(
renderer_id: int,
user: dict = Depends(require_auth),
):
"""Detach an L1 renderer."""
import db
success = await db.detach_renderer(user["username"], renderer_id)
if not success:
raise HTTPException(404, "Renderer not found")
return {"detached": True}
@router.post("/{renderer_id}/sync")
async def sync_renderer(
renderer_id: int,
request: Request,
user: dict = Depends(require_auth),
):
"""Sync assets with an L1 renderer."""
import db
import requests
renderer = await db.get_renderer(renderer_id)
if not renderer:
raise HTTPException(404, "Renderer not found")
if renderer.get("username") != user["username"]:
raise HTTPException(403, "Not authorized")
l1_url = renderer["url"]
# Get user's assets
assets = await db.get_user_assets(user["username"])
synced = 0
errors = []
for asset in assets:
if asset.get("ipfs_cid"):
try:
# Tell L1 to import from IPFS
resp = requests.post(
f"{l1_url}/cache/import",
json={"ipfs_cid": asset["ipfs_cid"]},
headers={"Authorization": f"Bearer {user['token']}"},
timeout=30,
)
if resp.status_code == 200:
synced += 1
else:
errors.append(f"{asset['name']}: {resp.status_code}")
except Exception as e:
errors.append(f"{asset['name']}: {e}")
if wants_html(request):
if errors:
return HTMLResponse(f'<span class="text-yellow-400">Synced {synced}, {len(errors)} errors</span>')
return HTMLResponse(f'<span class="text-green-400">Synced {synced} assets</span>')
return {"synced": synced, "errors": errors}