From eaa9291a5c2f85abe0ca7b7dd979c0c73b445580 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Fri, 8 May 2026 17:27:46 -0700 Subject: [PATCH] fix: serve Vue SPA from FastAPI, drop separate port 8535 Python http.server can't do SPA routing and Caddy was forwarding /turnstone/* paths that the static server couldn't resolve. - app/rest.py: mount web/dist/assets as StaticFiles; add SPA catch-all route that serves index.html for any unmatched path - manage.sh: start/stop/status simplified to single process on :8534; remove UI_PORT / UI_PID_FILE; drop http.server invocation - Caddyfile: replace split API/:8534 + SPA/:8535 block with a single strip_prefix + reverse_proxy to :8534 --- app/rest.py | 27 ++++++++++++++++++++++++++- manage.sh | 50 +++++++++++++++----------------------------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/app/rest.py b/app/rest.py index 6560dd2..874b2bf 100644 --- a/app/rest.py +++ b/app/rest.py @@ -1,4 +1,9 @@ -"""Turnstone REST API — thin HTTP wrapper around the search and ingest services.""" +"""Turnstone REST API — thin HTTP wrapper around the search and ingest services. + +Serves the Vue SPA from web/dist/ as static files so a single uvicorn process +handles both API routes and the frontend. Caddy strips the /turnstone prefix +before forwarding, so FastAPI always sees paths starting with /api/, /health, etc. +""" from __future__ import annotations import dataclasses @@ -8,10 +13,13 @@ from typing import Annotated from fastapi import FastAPI, Query from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles from app.services.search import search as _search, list_sources as _list_sources, format_results DB_PATH = Path(os.environ.get("TURNSTONE_DB", Path(__file__).parent.parent / "data" / "turnstone.db")) +DIST_DIR = Path(__file__).parent.parent / "web" / "dist" app = FastAPI(title="Turnstone API", version="0.1.0") @@ -22,6 +30,11 @@ app.add_middleware( allow_headers=["*"], ) +# Serve built Vue assets if the dist directory exists. +# Must be mounted before the SPA catch-all route below. +if DIST_DIR.exists(): + app.mount("/assets", StaticFiles(directory=str(DIST_DIR / "assets")), name="assets") + @app.get("/health") def health() -> dict: @@ -88,3 +101,15 @@ def diagnose( def list_sources() -> dict: sources = _list_sources(DB_PATH) return {"sources": sources} + + +# SPA catch-all — must be last. Serves index.html for any path that doesn't +# match an API route, enabling Vue Router's client-side navigation. +@app.get("/{path:path}") +def spa_fallback(path: str) -> FileResponse: + if DIST_DIR.exists(): + candidate = DIST_DIR / path + if candidate.is_file(): + return FileResponse(str(candidate)) + return FileResponse(str(DIST_DIR / "index.html")) + return FileResponse("/dev/null", status_code=503) diff --git a/manage.sh b/manage.sh index 578dadc..4035b9b 100755 --- a/manage.sh +++ b/manage.sh @@ -12,13 +12,11 @@ error() { echo -e "${RED}[turnstone]${NC} $*" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -API_PORT=8534 -UI_PORT=8535 -VITE_PORT=5174 # local HMR port in dev mode (proxies /api → 8534) +API_PORT=8534 # FastAPI: serves REST API + built Vue SPA +VITE_PORT=5174 # Vite HMR port in dev mode (proxies /api → 8534) LOG_DIR="log" API_PID_FILE=".turnstone-api.pid" -UI_PID_FILE=".turnstone-ui.pid" DB="${TURNSTONE_DB:-${SCRIPT_DIR}/data/turnstone.db}" @@ -71,11 +69,11 @@ usage() { echo " Usage: ./manage.sh [args]" echo "" echo " Production-like (built SPA + uvicorn):" - echo -e " ${GREEN}start${NC} Build Vue SPA, start API (:${API_PORT}) + static UI (:${UI_PORT})" - echo -e " ${GREEN}stop${NC} Stop API and UI servers" + echo -e " ${GREEN}start${NC} Build Vue SPA, start FastAPI + SPA on :${API_PORT}" + echo -e " ${GREEN}stop${NC} Stop the server" echo -e " ${GREEN}restart${NC} Stop then start" - echo -e " ${GREEN}status${NC} Show running processes" - echo -e " ${GREEN}logs [api|ui]${NC} Tail log files (default: api)" + echo -e " ${GREEN}status${NC} Show running process" + echo -e " ${GREEN}logs${NC} Tail server log" echo -e " ${GREEN}open${NC} Open UI in browser" echo "" echo " Development (hot-reload):" @@ -108,7 +106,7 @@ case "$CMD" in start) if _is_alive "$API_PID_FILE"; then - warn "API already running (PID $(<"$API_PID_FILE")) — use 'restart' to rebuild." + warn "Already running (PID $(<"$API_PID_FILE")) — use 'restart' to rebuild." exit 0 fi mkdir -p "$LOG_DIR" data @@ -117,25 +115,17 @@ case "$CMD" in (cd web && npm run build) 2>&1 | tee "${LOG_DIR}/build.log" | grep -E "built in|error" || true success "SPA built → web/dist/" - info "Starting FastAPI on port ${API_PORT}…" + info "Starting on port ${API_PORT}…" TURNSTONE_DB="$DB" nohup "$PYTHON" -m uvicorn app.rest:app \ --host 0.0.0.0 --port "$API_PORT" \ >> "${LOG_DIR}/api.log" 2>&1 & echo $! > "$API_PID_FILE" - _wait_for_port "$API_PORT" "FastAPI" "$API_PID_FILE" - success "API → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))" - - info "Starting static UI server on port ${UI_PORT}…" - nohup "$PYTHON" -m http.server "$UI_PORT" --directory web/dist \ - >> "${LOG_DIR}/ui.log" 2>&1 & - echo $! > "$UI_PID_FILE" - _wait_for_port "$UI_PORT" "UI server" "$UI_PID_FILE" - success "UI → http://localhost:${UI_PORT} (PID $(<"$UI_PID_FILE"))" + _wait_for_port "$API_PORT" "Turnstone" "$API_PID_FILE" + success "Running → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))" ;; stop) - _kill_pid_file "$API_PID_FILE" "FastAPI" - _kill_pid_file "$UI_PID_FILE" "UI server" + _kill_pid_file "$API_PID_FILE" "Turnstone" ;; restart) @@ -146,29 +136,19 @@ case "$CMD" in status) echo "" if _is_alive "$API_PID_FILE"; then - success "FastAPI RUNNING PID $(<"$API_PID_FILE") → http://localhost:${API_PORT}" + success "Turnstone RUNNING PID $(<"$API_PID_FILE") → http://localhost:${API_PORT}" else - echo -e " FastAPI ${RED}STOPPED${NC}" - fi - if _is_alive "$UI_PID_FILE"; then - success "UI server RUNNING PID $(<"$UI_PID_FILE") → http://localhost:${UI_PORT}" - else - echo -e " UI server ${RED}STOPPED${NC}" + echo -e " Turnstone ${RED}STOPPED${NC}" fi echo "" ;; logs) - target="${1:-api}" - case "$target" in - api) tail -f "${LOG_DIR}/api.log" ;; - ui) tail -f "${LOG_DIR}/ui.log" ;; - *) error "Unknown log target: $target. Use 'api' or 'ui'." ;; - esac + tail -f "${LOG_DIR}/api.log" ;; open) - URL="http://localhost:${UI_PORT}" + URL="http://localhost:${API_PORT}" info "Opening ${URL}" if command -v xdg-open &>/dev/null; then xdg-open "$URL" elif command -v open &>/dev/null; then open "$URL"