make static ui

This commit is contained in:
9qeklajc
2025-10-24 14:27:47 +02:00
parent 90f56357d9
commit 2121b36e79
6 changed files with 152 additions and 5 deletions
+13
View File
@@ -38,6 +38,10 @@ help:
@echo " make check-deps - Check system dependencies"
@echo " make setup - First-time project setup"
@echo ""
@echo "UI targets:"
@echo " make ui-build - Build UI for production (static export)"
@echo " make ui-dev - Start UI development server"
@echo ""
@echo "Database migration shortcuts:"
@echo " make create-migration - Auto-generate new migration"
@echo " make db-upgrade - Apply all pending migrations"
@@ -261,3 +265,12 @@ docs-deploy:
docs-install:
@echo "📚 Installing documentation dependencies..."
pip install -r docs/requirements.txt
# UI build
ui-build:
@echo "🎨 Building UI for static deployment..."
./scripts/build-ui.sh
ui-dev:
@echo "🎨 Starting UI development server..."
cd ui && (command -v pnpm >/dev/null 2>&1 && pnpm run dev || npm run dev)
+32 -1
View File
@@ -143,9 +143,40 @@ make db-migrate
make db-upgrade
```
## Admin UI
Routstr includes a modern Next.js admin dashboard that's served directly from the Python backend as static files - no separate Node.js server required.
### Building the UI
```bash
make ui-build
```
This compiles the Next.js application into static HTML, CSS, and JavaScript files in `ui/out/`.
### Accessing the Dashboard
Once built, the UI is automatically served by the FastAPI backend:
- **Dashboard**: `http://localhost:8000/`
- **Login**: `http://localhost:8000/login`
- **Models Management**: `http://localhost:8000/models`
- **Providers Management**: `http://localhost:8000/providers`
- **Settings**: `http://localhost:8000/settings`
The dashboard provides:
- Real-time wallet balance monitoring
- Model pricing configuration
- Upstream provider management
- Transaction history
- System settings
**Authentication**: Use the `ADMIN_PASSWORD` environment variable to access the dashboard.
## Withdrawing Balance
Go to `https://<your.routstr.proxy>/admin/` (NOTE: be sure to add the '/' at the end), enter the `ADMIN_PASSWORD` you set above and withdraw your balance as a Cashu token.
Go to the admin dashboard at `http://localhost:8000/` and login with your `ADMIN_PASSWORD` to withdraw your balance as a Cashu token.
## Example Client
+56 -2
View File
@@ -1,10 +1,12 @@
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
from typing import AsyncGenerator
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException
from ..balance import balance_router, deprecated_wallet_router
@@ -159,7 +161,6 @@ app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore
app.add_exception_handler(Exception, general_exception_handler)
@app.get("/", include_in_schema=False)
@app.get("/v1/info")
async def info() -> dict:
return {
@@ -176,6 +177,9 @@ async def info() -> dict:
@app.get("/admin")
async def admin_redirect() -> RedirectResponse:
ui_dist_path = Path(__file__).parent.parent.parent / "ui" / "out"
if ui_dist_path.exists():
return RedirectResponse("/")
return RedirectResponse("/admin/")
@@ -184,6 +188,56 @@ async def providers() -> RedirectResponse:
return RedirectResponse("/v1/providers/")
UI_DIST_PATH = Path(__file__).parent.parent.parent / "ui" / "out"
if UI_DIST_PATH.exists() and UI_DIST_PATH.is_dir():
logger.info(f"Serving static UI from {UI_DIST_PATH}")
app.mount("/_next", StaticFiles(directory=UI_DIST_PATH / "_next", check_dir=True), name="next-static")
@app.get("/", include_in_schema=False)
async def serve_root_ui() -> FileResponse:
return FileResponse(UI_DIST_PATH / "index.html")
@app.get("/dashboard", include_in_schema=False)
@app.get("/dashboard/{path:path}", include_in_schema=False)
async def serve_dashboard_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "index.html")
@app.get("/login", include_in_schema=False)
@app.get("/login/{path:path}", include_in_schema=False)
async def serve_login_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "login" / "index.html")
@app.get("/models", include_in_schema=False)
@app.get("/models/{path:path}", include_in_schema=False)
async def serve_models_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "models" / "index.html")
@app.get("/providers", include_in_schema=False)
@app.get("/providers/{path:path}", include_in_schema=False)
async def serve_providers_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "providers" / "index.html")
@app.get("/settings", include_in_schema=False)
@app.get("/settings/{path:path}", include_in_schema=False)
async def serve_settings_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "settings" / "index.html")
@app.get("/transactions", include_in_schema=False)
@app.get("/transactions/{path:path}", include_in_schema=False)
async def serve_transactions_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "transactions" / "index.html")
@app.get("/unauthorized", include_in_schema=False)
@app.get("/unauthorized/{path:path}", include_in_schema=False)
async def serve_unauthorized_ui(path: str = "") -> FileResponse:
return FileResponse(UI_DIST_PATH / "unauthorized" / "index.html")
app.mount("/static", StaticFiles(directory=UI_DIST_PATH, check_dir=True), name="ui-static")
else:
logger.warning(f"UI dist directory not found at {UI_DIST_PATH}, skipping static file serving")
app.include_router(models_router)
app.include_router(admin_router)
app.include_router(balance_router)
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
UI_DIR="$PROJECT_ROOT/ui"
echo "Building Routstr UI for static deployment..."
echo "UI directory: $UI_DIR"
if [ ! -d "$UI_DIR" ]; then
echo "Error: UI directory not found at $UI_DIR"
exit 1
fi
cd "$UI_DIR"
echo "Installing dependencies..."
if command -v pnpm &> /dev/null; then
pnpm install
elif command -v npm &> /dev/null; then
npm install
else
echo "Error: Neither pnpm nor npm found. Please install Node.js and npm."
exit 1
fi
echo "Building static export..."
if command -v pnpm &> /dev/null; then
pnpm run build
else
npm run build
fi
echo ""
echo "✓ UI build complete!"
echo "Static files generated at: $UI_DIR/out"
echo ""
echo "To serve the UI from the Python backend:"
echo " 1. Make sure NEXT_PUBLIC_API_URL is not set (or set to empty) to use relative API paths"
echo " 2. Start the backend: uvicorn routstr.core.main:app --host 0.0.0.0 --port 8000"
echo " 3. Access the UI at: http://localhost:8000"
echo ""
+5 -2
View File
@@ -1,8 +1,11 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
/* config options here */
output: 'export',
trailingSlash: true,
images: {
unoptimized: true,
},
};
export default nextConfig;
+1
View File
@@ -5,6 +5,7 @@
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"export": "next build",
"start": "next start",
"lint": "next lint",
"format-check": "prettier --check --ignore-path .gitignore .",