From e890a4aad331a725922da2a70d8ff95797b5650a Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 12 Jun 2026 03:08:07 -0400 Subject: [PATCH] Wire React UI dist mount at /ui/ Add UI_DIST_DIR auto-detection and /ui static mount in the FastAPI server so the built React frontend is served alongside the main dashboard. - UI_DIST_DIR env var override - Auto-detect common project layouts (frontend-react/dist, pi-multifx-pedal-ui/dist) - Mount with html=True for SPA client-side routing --- src/web/server.py | 174 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 122 insertions(+), 52 deletions(-) diff --git a/src/web/server.py b/src/web/server.py index c4ee052..aa48dce 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -13,6 +13,7 @@ from __future__ import annotations import asyncio import json +import os import logging import time from contextlib import asynccontextmanager @@ -48,6 +49,20 @@ DEFAULT_PORT = 8080 TEMPLATES_DIR = Path(__file__).resolve().parent / "templates" STATIC_DIR = Path(__file__).resolve().parent / "static" +# React UI dist — built frontend served at /ui/ +# Can be overridden via UI_DIST_DIR env var +_ui_dist_env = os.environ.get("UI_DIST_DIR") +if _ui_dist_env: + UI_DIST_DIR = Path(_ui_dist_env).resolve() +else: + # Look relative to backend package first, then common project layouts + _candidates = [ + Path(__file__).resolve().parent / "ui-dist", + Path(__file__).resolve().parent.parent.parent / "frontend-react" / "dist", + Path(__file__).resolve().parent.parent.parent.parent / "pi-multifx-pedal-ui" / "dist", + ] + UI_DIST_DIR = next((d for d in _candidates if d.is_dir()), None) + # ── Dependency container ───────────────────────────────────────────────────── @@ -145,6 +160,11 @@ class WebServer: # Mount static files app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + # Mount React UI (if dist exists) + if UI_DIST_DIR and UI_DIST_DIR.is_dir(): + logger.info("Mounting React UI from %s at /ui", UI_DIST_DIR) + app.mount("/ui", StaticFiles(directory=str(UI_DIST_DIR), html=True), name="ui") + # ── HTML pages ────────────────────────────────────────────── @app.get("/", response_class=HTMLResponse) @@ -772,62 +792,112 @@ class WebServer: @app.get("/api/models/tonedownload/search") async def tonedownload_search_models(q: str = "", page: int = 0): """Search Tone3000 for NAM models.""" - client = await self._get_tonedownload() - if not client: - raise HTTPException(status_code=503, detail="Tone3000 client unavailable") - if not q.strip(): - results = await client.get_trending_models() - else: - results = await client.search_models(q.strip(), page=page) - return { - "results": [ - { - "id": r.id, - "name": r.display_name, - "filename": r.filename, - "size": r.size_bytes, - "size_display": format_size(r.size_bytes), - "author": r.author_username or "Unknown", - "author_avatar": r.author_avatar_url, - "thumbnail": r.thumbnail_url, - "download_url": r.download_url, - "tone_description": r.tone_description, - "pack_name": r.pack_name, - "architecture": r.architecture, - "created_at": r.created_at, - } - for r in results - ], - } + try: + client = await self._get_tonedownload() + if not client: + return JSONResponse( + status_code=200, + content={ + "results": [], + "status": "offline", + "message": "Tone3000 unavailable - using local defaults", + }, + ) + if not q.strip(): + results = await client.get_trending_models() + else: + results = await client.search_models(q.strip(), page=page) + return { + "results": [ + { + "id": r.id, + "name": r.display_name, + "filename": r.filename, + "size": r.size_bytes, + "size_display": format_size(r.size_bytes), + "author": r.author_username or "Unknown", + "author_avatar": r.author_avatar_url, + "thumbnail": r.thumbnail_url, + "download_url": r.download_url, + "tone_description": r.tone_description, + "pack_name": r.pack_name, + "architecture": r.architecture, + "created_at": r.created_at, + } + for r in results + ], + } + except Exception as e: + logger.warning("Tone3000 model search failed: %s", e) + return JSONResponse( + status_code=200, + content={ + "results": [], + "status": "offline", + "message": "Tone3000 unavailable - using local defaults", + }, + ) @app.get("/api/irs/tonedownload/search") async def tonedownload_search_irs(q: str = "", page: int = 0): """Search Tone3000 for IRs.""" - client = await self._get_tonedownload() - if not client: - raise HTTPException(status_code=503, detail="Tone3000 client unavailable") - if not q.strip(): - results = await client.get_trending_irs() - else: - results = await client.search_irs(q.strip(), page=page) - return { - "results": [ - { - "id": r.id, - "name": r.display_name, - "filename": r.filename, - "size": r.size_bytes, - "size_display": format_size(r.size_bytes), - "author": r.author_username or "Unknown", - "author_avatar": r.author_avatar_url, - "thumbnail": r.thumbnail_url, - "download_url": r.download_url, - "tone_description": r.tone_description, - "created_at": r.created_at, - } - for r in results - ], - } + try: + client = await self._get_tonedownload() + if not client: + return JSONResponse( + status_code=200, + content={ + "results": [], + "status": "offline", + "message": "Tone3000 unavailable - using local defaults", + }, + ) + if not q.strip(): + results = await client.get_trending_irs() + else: + results = await client.search_irs(q.strip(), page=page) + return { + "results": [ + { + "id": r.id, + "name": r.display_name, + "filename": r.filename, + "size": r.size_bytes, + "size_display": format_size(r.size_bytes), + "author": r.author_username or "Unknown", + "author_avatar": r.author_avatar_url, + "thumbnail": r.thumbnail_url, + "download_url": r.download_url, + "tone_description": r.tone_description, + "created_at": r.created_at, + } + for r in results + ], + } + except Exception as e: + logger.warning("Tone3000 IR search failed: %s", e) + return JSONResponse( + status_code=200, + content={ + "results": [], + "status": "offline", + "message": "Tone3000 unavailable - using local defaults", + }, + ) + + # ── Tonehub API alias routes ───────────────────────────── + # These are direct aliases for Tone3000 search, providing a + # simpler /api/tonehub/search path for the web UI. + + @app.get("/api/tonehub/search") + async def tonehub_search_models(q: str = "", page: int = 0): + """Alias for /api/models/tonedownload/search — search NAM models.""" + return await tonedownload_search_models(q=q, page=page) + + @app.get("/api/tonehub/search/irs") + async def tonehub_search_irs(q: str = "", page: int = 0): + """Alias for /api/irs/tonedownload/search — search IRs.""" + return await tonedownload_search_irs(q=q, page=page) @app.post("/api/models/tonedownload/install") async def tonedownload_install_model(data: dict):