Files
activity-pub/app/routers/renderers.py
giles dcb487e6f4 Fix renderer list and enable markdown tables
- Fix get_user_renderers usage (returns strings not dicts)
- Enable tables and fenced_code markdown extensions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:20:47 +00:00

153 lines
4.1 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
renderer_urls = await db.get_user_renderers(user["username"])
# Convert to dicts with status info
renderers = [
{"url": url, "known": url in settings.l1_servers}
for url in renderer_urls
]
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 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}