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
This commit is contained in:
2026-06-12 03:08:07 -04:00
parent da3097c2dc
commit e890a4aad3
+122 -52
View File
@@ -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):