""" 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'Synced {synced}, {len(errors)} errors') return HTMLResponse(f'Synced {synced} assets') return {"synced": synced, "errors": errors}