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
This commit is contained in:
pyr0ball 2026-05-08 17:27:46 -07:00
parent db9e206971
commit 518fc926ce
2 changed files with 41 additions and 36 deletions

View file

@ -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)

View file

@ -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 <command> [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"