fix: mount all routes at /turnstone prefix for direct LAN access
Vite builds with base='/turnstone/' so asset paths in index.html are
/turnstone/assets/*. Serving FastAPI at root / meant direct hits to
port 8534 got index.html for asset requests (blank page).
- All routes now under /turnstone (APIRouter prefix + StaticFiles mount
at /turnstone/assets + SPA catch-all at /turnstone/{path})
- Root / redirects to /turnstone/
- Caddy block reverted to no-strip: both direct LAN and Caddy access
hit the same paths, no per-host routing differences
This commit is contained in:
parent
eaa9291a5c
commit
e579396eb8
1 changed files with 31 additions and 24 deletions
55
app/rest.py
55
app/rest.py
|
|
@ -1,8 +1,8 @@
|
|||
"""Turnstone REST API — thin HTTP wrapper around the search and ingest services.
|
||||
"""Turnstone REST API — serves REST API and Vue SPA under the /turnstone prefix.
|
||||
|
||||
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.
|
||||
All routes (API + static files) are mounted at /turnstone so the app works
|
||||
identically whether accessed directly (http://host:8534/turnstone/) or through
|
||||
Caddy (menagerie.circuitforge.tech/turnstone) without prefix stripping.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -11,9 +11,9 @@ import os
|
|||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi import APIRouter, FastAPI, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.services.search import search as _search, list_sources as _list_sources, format_results
|
||||
|
|
@ -21,7 +21,7 @@ from app.services.search import search as _search, list_sources as _list_sources
|
|||
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", docs_url="/turnstone/docs", redoc_url=None)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
@ -30,18 +30,20 @@ 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")
|
||||
# Serve built Vue assets at the path Vite embeds in index.html.
|
||||
if (DIST_DIR / "assets").exists():
|
||||
app.mount("/turnstone/assets", StaticFiles(directory=str(DIST_DIR / "assets")), name="assets")
|
||||
|
||||
# API router — all routes accessible at /turnstone/api/* and /turnstone/health.
|
||||
router = APIRouter(prefix="/turnstone")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
@router.get("/health")
|
||||
def health() -> dict:
|
||||
return {"status": "ok", "db": str(DB_PATH)}
|
||||
|
||||
|
||||
@app.get("/api/search")
|
||||
@router.get("/api/search")
|
||||
def search_logs(
|
||||
q: Annotated[str, Query(description="Search query")] = "",
|
||||
source: Annotated[str | None, Query(description="Filter by log source ID (partial match)")] = None,
|
||||
|
|
@ -61,13 +63,10 @@ def search_logs(
|
|||
until=until,
|
||||
limit=limit,
|
||||
)
|
||||
return {
|
||||
"count": len(results),
|
||||
"results": [dataclasses.asdict(r) for r in results],
|
||||
}
|
||||
return {"count": len(results), "results": [dataclasses.asdict(r) for r in results]}
|
||||
|
||||
|
||||
@app.get("/api/diagnose")
|
||||
@router.get("/api/diagnose")
|
||||
def diagnose(
|
||||
q: Annotated[str, Query(description="Service name or problem description")] = "",
|
||||
source: Annotated[str | None, Query(description="Limit to a specific source ID (partial match)")] = None,
|
||||
|
|
@ -97,15 +96,23 @@ def diagnose(
|
|||
}
|
||||
|
||||
|
||||
@app.get("/api/sources")
|
||||
@router.get("/api/sources")
|
||||
def list_sources() -> dict:
|
||||
sources = _list_sources(DB_PATH)
|
||||
return {"sources": sources}
|
||||
return {"sources": _list_sources(DB_PATH)}
|
||||
|
||||
|
||||
# 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}")
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
# Root redirect → /turnstone/
|
||||
@app.get("/")
|
||||
def root_redirect() -> RedirectResponse:
|
||||
return RedirectResponse(url="/turnstone/")
|
||||
|
||||
|
||||
# SPA catch-all — serves index.html for any /turnstone/* path that isn't a
|
||||
# static asset or API route. Must be registered after include_router.
|
||||
@app.get("/turnstone/{path:path}")
|
||||
def spa_fallback(path: str) -> FileResponse:
|
||||
if DIST_DIR.exists():
|
||||
candidate = DIST_DIR / path
|
||||
|
|
|
|||
Loading…
Reference in a new issue