From 2121b36e79a3765fb3b84eeacad589c70403a7f6 Mon Sep 17 00:00:00 2001 From: 9qeklajc <9qeklajc> Date: Fri, 24 Oct 2025 14:21:11 +0200 Subject: [PATCH] make static ui --- Makefile | 13 ++++++++++ README.md | 33 ++++++++++++++++++++++++- routstr/core/main.py | 58 ++++++++++++++++++++++++++++++++++++++++++-- scripts/build-ui.sh | 45 ++++++++++++++++++++++++++++++++++ ui/next.config.ts | 7 ++++-- ui/package.json | 1 + 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100755 scripts/build-ui.sh diff --git a/Makefile b/Makefile index df9fb4a2..082a9d78 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 07d25fe9..90d4e50e 100644 --- a/README.md +++ b/README.md @@ -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:///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 diff --git a/routstr/core/main.py b/routstr/core/main.py index 644e19d6..f900e541 100644 --- a/routstr/core/main.py +++ b/routstr/core/main.py @@ -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) diff --git a/scripts/build-ui.sh b/scripts/build-ui.sh new file mode 100755 index 00000000..aafbd617 --- /dev/null +++ b/scripts/build-ui.sh @@ -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 "" + diff --git a/ui/next.config.ts b/ui/next.config.ts index ab7e82de..4597eb87 100644 --- a/ui/next.config.ts +++ b/ui/next.config.ts @@ -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; diff --git a/ui/package.json b/ui/package.json index 8500b595..9a054902 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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 .",