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:
parent
1f5854e90b
commit
9e46cd4c7f
2 changed files with 41 additions and 36 deletions
27
app/rest.py
27
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)
|
||||
|
|
|
|||
50
manage.sh
50
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 <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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue