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 asyncio
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
@@ -48,6 +49,20 @@ DEFAULT_PORT = 8080
|
|||||||
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
||||||
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
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 ─────────────────────────────────────────────────────
|
# ── Dependency container ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -145,6 +160,11 @@ class WebServer:
|
|||||||
# Mount static files
|
# Mount static files
|
||||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
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 ──────────────────────────────────────────────
|
# ── HTML pages ──────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
@@ -772,62 +792,112 @@ class WebServer:
|
|||||||
@app.get("/api/models/tonedownload/search")
|
@app.get("/api/models/tonedownload/search")
|
||||||
async def tonedownload_search_models(q: str = "", page: int = 0):
|
async def tonedownload_search_models(q: str = "", page: int = 0):
|
||||||
"""Search Tone3000 for NAM models."""
|
"""Search Tone3000 for NAM models."""
|
||||||
client = await self._get_tonedownload()
|
try:
|
||||||
if not client:
|
client = await self._get_tonedownload()
|
||||||
raise HTTPException(status_code=503, detail="Tone3000 client unavailable")
|
if not client:
|
||||||
if not q.strip():
|
return JSONResponse(
|
||||||
results = await client.get_trending_models()
|
status_code=200,
|
||||||
else:
|
content={
|
||||||
results = await client.search_models(q.strip(), page=page)
|
"results": [],
|
||||||
return {
|
"status": "offline",
|
||||||
"results": [
|
"message": "Tone3000 unavailable - using local defaults",
|
||||||
{
|
},
|
||||||
"id": r.id,
|
)
|
||||||
"name": r.display_name,
|
if not q.strip():
|
||||||
"filename": r.filename,
|
results = await client.get_trending_models()
|
||||||
"size": r.size_bytes,
|
else:
|
||||||
"size_display": format_size(r.size_bytes),
|
results = await client.search_models(q.strip(), page=page)
|
||||||
"author": r.author_username or "Unknown",
|
return {
|
||||||
"author_avatar": r.author_avatar_url,
|
"results": [
|
||||||
"thumbnail": r.thumbnail_url,
|
{
|
||||||
"download_url": r.download_url,
|
"id": r.id,
|
||||||
"tone_description": r.tone_description,
|
"name": r.display_name,
|
||||||
"pack_name": r.pack_name,
|
"filename": r.filename,
|
||||||
"architecture": r.architecture,
|
"size": r.size_bytes,
|
||||||
"created_at": r.created_at,
|
"size_display": format_size(r.size_bytes),
|
||||||
}
|
"author": r.author_username or "Unknown",
|
||||||
for r in results
|
"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")
|
@app.get("/api/irs/tonedownload/search")
|
||||||
async def tonedownload_search_irs(q: str = "", page: int = 0):
|
async def tonedownload_search_irs(q: str = "", page: int = 0):
|
||||||
"""Search Tone3000 for IRs."""
|
"""Search Tone3000 for IRs."""
|
||||||
client = await self._get_tonedownload()
|
try:
|
||||||
if not client:
|
client = await self._get_tonedownload()
|
||||||
raise HTTPException(status_code=503, detail="Tone3000 client unavailable")
|
if not client:
|
||||||
if not q.strip():
|
return JSONResponse(
|
||||||
results = await client.get_trending_irs()
|
status_code=200,
|
||||||
else:
|
content={
|
||||||
results = await client.search_irs(q.strip(), page=page)
|
"results": [],
|
||||||
return {
|
"status": "offline",
|
||||||
"results": [
|
"message": "Tone3000 unavailable - using local defaults",
|
||||||
{
|
},
|
||||||
"id": r.id,
|
)
|
||||||
"name": r.display_name,
|
if not q.strip():
|
||||||
"filename": r.filename,
|
results = await client.get_trending_irs()
|
||||||
"size": r.size_bytes,
|
else:
|
||||||
"size_display": format_size(r.size_bytes),
|
results = await client.search_irs(q.strip(), page=page)
|
||||||
"author": r.author_username or "Unknown",
|
return {
|
||||||
"author_avatar": r.author_avatar_url,
|
"results": [
|
||||||
"thumbnail": r.thumbnail_url,
|
{
|
||||||
"download_url": r.download_url,
|
"id": r.id,
|
||||||
"tone_description": r.tone_description,
|
"name": r.display_name,
|
||||||
"created_at": r.created_at,
|
"filename": r.filename,
|
||||||
}
|
"size": r.size_bytes,
|
||||||
for r in results
|
"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")
|
@app.post("/api/models/tonedownload/install")
|
||||||
async def tonedownload_install_model(data: dict):
|
async def tonedownload_install_model(data: dict):
|
||||||
|
|||||||
Reference in New Issue
Block a user