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
|
All routes (API + static files) are mounted at /turnstone so the app works
|
||||||
handles both API routes and the frontend. Caddy strips the /turnstone prefix
|
identically whether accessed directly (http://host:8534/turnstone/) or through
|
||||||
before forwarding, so FastAPI always sees paths starting with /api/, /health, etc.
|
Caddy (menagerie.circuitforge.tech/turnstone) without prefix stripping.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -11,9 +11,9 @@ import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import FastAPI, Query
|
from fastapi import APIRouter, FastAPI, Query
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
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
|
||||||
|
|
@ -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"))
|
DB_PATH = Path(os.environ.get("TURNSTONE_DB", Path(__file__).parent.parent / "data" / "turnstone.db"))
|
||||||
DIST_DIR = Path(__file__).parent.parent / "web" / "dist"
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|
@ -30,18 +30,20 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Serve built Vue assets if the dist directory exists.
|
# Serve built Vue assets at the path Vite embeds in index.html.
|
||||||
# Must be mounted before the SPA catch-all route below.
|
if (DIST_DIR / "assets").exists():
|
||||||
if DIST_DIR.exists():
|
app.mount("/turnstone/assets", StaticFiles(directory=str(DIST_DIR / "assets")), name="assets")
|
||||||
app.mount("/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:
|
def health() -> dict:
|
||||||
return {"status": "ok", "db": str(DB_PATH)}
|
return {"status": "ok", "db": str(DB_PATH)}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/search")
|
@router.get("/api/search")
|
||||||
def search_logs(
|
def search_logs(
|
||||||
q: Annotated[str, Query(description="Search query")] = "",
|
q: Annotated[str, Query(description="Search query")] = "",
|
||||||
source: Annotated[str | None, Query(description="Filter by log source ID (partial match)")] = None,
|
source: Annotated[str | None, Query(description="Filter by log source ID (partial match)")] = None,
|
||||||
|
|
@ -61,13 +63,10 @@ def search_logs(
|
||||||
until=until,
|
until=until,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
return {
|
return {"count": len(results), "results": [dataclasses.asdict(r) for r in results]}
|
||||||
"count": len(results),
|
|
||||||
"results": [dataclasses.asdict(r) for r in results],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/diagnose")
|
@router.get("/api/diagnose")
|
||||||
def diagnose(
|
def diagnose(
|
||||||
q: Annotated[str, Query(description="Service name or problem description")] = "",
|
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,
|
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:
|
def list_sources() -> dict:
|
||||||
sources = _list_sources(DB_PATH)
|
return {"sources": _list_sources(DB_PATH)}
|
||||||
return {"sources": sources}
|
|
||||||
|
|
||||||
|
|
||||||
# SPA catch-all — must be last. Serves index.html for any path that doesn't
|
app.include_router(router)
|
||||||
# match an API route, enabling Vue Router's client-side navigation.
|
|
||||||
@app.get("/{path:path}")
|
|
||||||
|
# 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:
|
def spa_fallback(path: str) -> FileResponse:
|
||||||
if DIST_DIR.exists():
|
if DIST_DIR.exists():
|
||||||
candidate = DIST_DIR / path
|
candidate = DIST_DIR / path
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue