From 8c4a30d18f689697b8eb56471c05bbeecf1f6836 Mon Sep 17 00:00:00 2001 From: gilesb Date: Sun, 11 Jan 2026 15:36:38 +0000 Subject: [PATCH] Simplify renderers to use env-configured L1 servers - L1 servers now come from L1_SERVERS env var instead of per-user attachment - Added renderers/list.html template showing available servers - Health check shows if servers are online - Elegant error handling for invalid requests (no more raw JSON errors) - Connect button passes auth token to L1 for seamless login Co-Authored-By: Claude Opus 4.5 --- app/routers/renderers.py | 171 ++++++++++-------------------- app/templates/renderers/list.html | 52 +++++++++ 2 files changed, 108 insertions(+), 115 deletions(-) create mode 100644 app/templates/renderers/list.html diff --git a/app/routers/renderers.py b/app/routers/renderers.py index 3861f11..4b9edf6 100644 --- a/app/routers/renderers.py +++ b/app/routers/renderers.py @@ -1,15 +1,16 @@ """ Renderer (L1) management routes for L2 server. -Handles L1 server attachments and delegation. +L1 servers are configured via environment variable L1_SERVERS. +Users connect to renderers to create and run recipes. """ import logging from typing import Optional +import requests from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.responses import HTMLResponse -from pydantic import BaseModel +from fastapi.responses import HTMLResponse, RedirectResponse from artdag_common import render from artdag_common.middleware import wants_html, wants_json @@ -21,132 +22,72 @@ router = APIRouter() logger = logging.getLogger(__name__) -class AttachRendererRequest(BaseModel): - l1_url: str +def check_renderer_health(url: str, timeout: float = 5.0) -> bool: + """Check if a renderer is healthy.""" + try: + resp = requests.get(f"{url}/", timeout=timeout) + return resp.status_code == 200 + except Exception: + return False @router.get("") -async def list_renderers( - request: Request, - user: dict = Depends(require_auth), -): - """List attached L1 renderers.""" - import db +async def list_renderers(request: Request): + """List configured L1 renderers.""" + # Get user if logged in + username = get_user_from_cookie(request) + user = None + if username: + # Get token for connection links + token = request.cookies.get("auth_token", "") + user = {"username": username, "token": token} - 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 - ] + # Build server list with health status + servers = [] + for url in settings.l1_servers: + servers.append({ + "url": url, + "healthy": check_renderer_health(url), + }) if wants_json(request): - return {"renderers": renderers, "known_servers": settings.l1_servers} + return {"servers": servers} templates = get_templates(request) return render(templates, "renderers/list.html", request, - renderers=renderers, - known_servers=settings.l1_servers, + servers=servers, user=user, active_tab="renderers", ) +@router.get("/{path:path}") +async def renderer_catchall(path: str, request: Request): + """Catch-all for invalid renderer URLs - redirect to list.""" + if wants_json(request): + raise HTTPException(404, "Not found") + return RedirectResponse(url="/renderers", status_code=302) + + @router.post("") -async def attach_renderer( - req: AttachRendererRequest, - user: dict = Depends(require_auth), -): - """Attach an L1 renderer.""" - import db +@router.post("/{path:path}") +async def renderer_post_catchall(request: Request, path: str = ""): + """ + Catch-all for POST requests. - l1_url = req.l1_url.rstrip("/") + The old API expected JSON POST to attach renderers. + Now renderers are env-configured, so redirect to the list. + """ + if wants_json(request): + return { + "error": "Renderers are now configured via environment. See /renderers for available servers.", + "servers": settings.l1_servers, + } - # 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'Synced {synced}, {len(errors)} errors') - return HTMLResponse(f'Synced {synced} assets') - - return {"synced": synced, "errors": errors} + templates = get_templates(request) + return render(templates, "renderers/list.html", request, + servers=[{"url": url, "healthy": check_renderer_health(url)} for url in settings.l1_servers], + user=get_user_from_cookie(request), + error="Renderers are configured by the system administrator. Use the Connect button to access a renderer.", + active_tab="renderers", + ) diff --git a/app/templates/renderers/list.html b/app/templates/renderers/list.html new file mode 100644 index 0000000..66f93b8 --- /dev/null +++ b/app/templates/renderers/list.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block content %} +
+

Renderers

+ +

+ Renderers are L1 servers that process your media. Connect to a renderer to create and run recipes. +

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if success %} +
+ {{ success }} +
+ {% endif %} + +
+ {% for server in servers %} +
+
+ + {{ server.url }} + + {% if server.healthy %} + Online + {% else %} + Offline + {% endif %} +
+ +
+ {% else %} +

No renderers configured.

+ {% endfor %} +
+ +
+

Renderers are configured by the system administrator.

+
+
+{% endblock %}