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
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
|
@ -8,10 +13,13 @@ from typing import Annotated
|
||||||
|
|
||||||
from fastapi import FastAPI, Query
|
from fastapi import FastAPI, Query
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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
|
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"))
|
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")
|
app = FastAPI(title="Turnstone API", version="0.1.0")
|
||||||
|
|
||||||
|
|
@ -22,6 +30,11 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
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")
|
@app.get("/health")
|
||||||
def health() -> dict:
|
def health() -> dict:
|
||||||
|
|
@ -88,3 +101,15 @@ def diagnose(
|
||||||
def list_sources() -> dict:
|
def list_sources() -> dict:
|
||||||
sources = _list_sources(DB_PATH)
|
sources = _list_sources(DB_PATH)
|
||||||
return {"sources": sources}
|
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)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
API_PORT=8534
|
API_PORT=8534 # FastAPI: serves REST API + built Vue SPA
|
||||||
UI_PORT=8535
|
VITE_PORT=5174 # Vite HMR port in dev mode (proxies /api → 8534)
|
||||||
VITE_PORT=5174 # local HMR port in dev mode (proxies /api → 8534)
|
|
||||||
|
|
||||||
LOG_DIR="log"
|
LOG_DIR="log"
|
||||||
API_PID_FILE=".turnstone-api.pid"
|
API_PID_FILE=".turnstone-api.pid"
|
||||||
UI_PID_FILE=".turnstone-ui.pid"
|
|
||||||
|
|
||||||
DB="${TURNSTONE_DB:-${SCRIPT_DIR}/data/turnstone.db}"
|
DB="${TURNSTONE_DB:-${SCRIPT_DIR}/data/turnstone.db}"
|
||||||
|
|
||||||
|
|
@ -71,11 +69,11 @@ usage() {
|
||||||
echo " Usage: ./manage.sh <command> [args]"
|
echo " Usage: ./manage.sh <command> [args]"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Production-like (built SPA + uvicorn):"
|
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}start${NC} Build Vue SPA, start FastAPI + SPA on :${API_PORT}"
|
||||||
echo -e " ${GREEN}stop${NC} Stop API and UI servers"
|
echo -e " ${GREEN}stop${NC} Stop the server"
|
||||||
echo -e " ${GREEN}restart${NC} Stop then start"
|
echo -e " ${GREEN}restart${NC} Stop then start"
|
||||||
echo -e " ${GREEN}status${NC} Show running processes"
|
echo -e " ${GREEN}status${NC} Show running process"
|
||||||
echo -e " ${GREEN}logs [api|ui]${NC} Tail log files (default: api)"
|
echo -e " ${GREEN}logs${NC} Tail server log"
|
||||||
echo -e " ${GREEN}open${NC} Open UI in browser"
|
echo -e " ${GREEN}open${NC} Open UI in browser"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Development (hot-reload):"
|
echo " Development (hot-reload):"
|
||||||
|
|
@ -108,7 +106,7 @@ case "$CMD" in
|
||||||
|
|
||||||
start)
|
start)
|
||||||
if _is_alive "$API_PID_FILE"; then
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
mkdir -p "$LOG_DIR" data
|
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
|
(cd web && npm run build) 2>&1 | tee "${LOG_DIR}/build.log" | grep -E "built in|error" || true
|
||||||
success "SPA built → web/dist/"
|
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 \
|
TURNSTONE_DB="$DB" nohup "$PYTHON" -m uvicorn app.rest:app \
|
||||||
--host 0.0.0.0 --port "$API_PORT" \
|
--host 0.0.0.0 --port "$API_PORT" \
|
||||||
>> "${LOG_DIR}/api.log" 2>&1 &
|
>> "${LOG_DIR}/api.log" 2>&1 &
|
||||||
echo $! > "$API_PID_FILE"
|
echo $! > "$API_PID_FILE"
|
||||||
_wait_for_port "$API_PORT" "FastAPI" "$API_PID_FILE"
|
_wait_for_port "$API_PORT" "Turnstone" "$API_PID_FILE"
|
||||||
success "API → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))"
|
success "Running → 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"))"
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
stop)
|
stop)
|
||||||
_kill_pid_file "$API_PID_FILE" "FastAPI"
|
_kill_pid_file "$API_PID_FILE" "Turnstone"
|
||||||
_kill_pid_file "$UI_PID_FILE" "UI server"
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
restart)
|
restart)
|
||||||
|
|
@ -146,29 +136,19 @@ case "$CMD" in
|
||||||
status)
|
status)
|
||||||
echo ""
|
echo ""
|
||||||
if _is_alive "$API_PID_FILE"; then
|
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
|
else
|
||||||
echo -e " FastAPI ${RED}STOPPED${NC}"
|
echo -e " Turnstone ${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}"
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
;;
|
;;
|
||||||
|
|
||||||
logs)
|
logs)
|
||||||
target="${1:-api}"
|
tail -f "${LOG_DIR}/api.log"
|
||||||
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
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
open)
|
open)
|
||||||
URL="http://localhost:${UI_PORT}"
|
URL="http://localhost:${API_PORT}"
|
||||||
info "Opening ${URL}"
|
info "Opening ${URL}"
|
||||||
if command -v xdg-open &>/dev/null; then xdg-open "$URL"
|
if command -v xdg-open &>/dev/null; then xdg-open "$URL"
|
||||||
elif command -v open &>/dev/null; then open "$URL"
|
elif command -v open &>/dev/null; then open "$URL"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue