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:
+122
-52
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user