mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
make static ui
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Executable
+45
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 .",
|
||||
|
||||
Reference in New Issue
Block a user