fix(ui): sub-path routing and API proxy for /magpie/ base URL

- spa_server.py: strip /magpie prefix before API check and file lookup;
  all HTTP methods call _normalise_path() first so /magpie/api/v1/* proxies
  correctly and /magpie/assets/* resolve against the dist root
- manage.sh: pass --base /magpie to spa_server.py so the handler knows
  the deployment prefix
- main.ts: pass import.meta.env.BASE_URL to createWebHistory so Vue Router
  strips the /magpie prefix before matching routes

Without these fixes, assets returned index.html (MIME type error), API
calls returned HTML instead of JSON (stats.posts undefined), and router
routes never matched (matched: []).

Closes: #12
This commit is contained in:
Alan Weinstock 2026-06-15 11:50:12 -07:00
parent 35c6e5f7bc
commit 8ea4baa915
3 changed files with 26 additions and 3 deletions

View file

@ -12,7 +12,7 @@ import OpportunitiesView from './components/OpportunitiesView.vue'
import SignalsView from './components/SignalsView.vue'
const router = createRouter({
history: createWebHistory(),
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/', redirect: '/signals' },
{ path: '/signals', component: SignalsView },

View file

@ -85,7 +85,7 @@ _start_web() {
if [[ -f "frontend/dist/index.html" ]]; then
info "Starting web on :${WEB_PORT} (static dist — production mode)..."
conda run --no-capture-output -n "$CONDA_ENV" \
python scripts/spa_server.py --port "$WEB_PORT" --directory frontend/dist >> "$LOG_WEB" 2>&1 &
python scripts/spa_server.py --port "$WEB_PORT" --directory frontend/dist --base /magpie >> "$LOG_WEB" 2>&1 &
else
info "Starting web on :${WEB_PORT} (Vite dev server — no dist found)..."
cd frontend
@ -266,7 +266,7 @@ case "$cmd" in
# In production, Caddy proxies menagerie.circuitforge.tech/magpie* → this port.
info "Serving pre-built frontend on :${WEB_PORT} (SPA fallback enabled)..."
conda run --no-capture-output -n "$CONDA_ENV" \
python scripts/spa_server.py --port "$WEB_PORT" --directory frontend/dist >> "$LOG_WEB" 2>&1 &
python scripts/spa_server.py --port "$WEB_PORT" --directory frontend/dist --base /magpie >> "$LOG_WEB" 2>&1 &
echo $! > "$PID_WEB"
ok "Static server up → http://localhost:${WEB_PORT}"
;;

View file

@ -20,6 +20,18 @@ from http.server import HTTPServer, SimpleHTTPRequestHandler
class SPAHandler(SimpleHTTPRequestHandler):
api_port: int = 8532
base_prefix: str = "" # e.g. "/magpie" — stripped before file lookup
# ------------------------------------------------------------------ #
# Path helpers
# ------------------------------------------------------------------ #
def _strip_base(self, path: str) -> str:
"""Remove the base prefix so file lookup works against the dist root."""
p = path.split("?", 1)[0].split("#", 1)[0]
if self.base_prefix and p.startswith(self.base_prefix):
path = path[len(self.base_prefix):]
return path or "/"
# ------------------------------------------------------------------ #
# API proxy
@ -51,11 +63,16 @@ class SPAHandler(SimpleHTTPRequestHandler):
# Request dispatch
# ------------------------------------------------------------------ #
def _normalise_path(self) -> None:
"""Strip base prefix so all subsequent checks work on bare /api/ or /asset paths."""
self.path = self._strip_base(self.path)
def _is_api(self) -> bool:
path = self.path.split("?", 1)[0]
return path.startswith("/api/")
def do_GET(self) -> None:
self._normalise_path()
if self._is_api():
self._proxy_api()
return
@ -65,24 +82,28 @@ class SPAHandler(SimpleHTTPRequestHandler):
super().do_GET()
def do_POST(self) -> None:
self._normalise_path()
if self._is_api():
self._proxy_api()
return
self.send_error(405, "Method Not Allowed")
def do_PUT(self) -> None:
self._normalise_path()
if self._is_api():
self._proxy_api()
return
self.send_error(405, "Method Not Allowed")
def do_PATCH(self) -> None:
self._normalise_path()
if self._is_api():
self._proxy_api()
return
self.send_error(405, "Method Not Allowed")
def do_DELETE(self) -> None:
self._normalise_path()
if self._is_api():
self._proxy_api()
return
@ -98,9 +119,11 @@ def main() -> None:
parser.add_argument("--port", type=int, default=8531)
parser.add_argument("--directory", default="frontend/dist")
parser.add_argument("--api-port", type=int, default=8532)
parser.add_argument("--base", default="", help="URL base prefix to strip (e.g. /magpie)")
args = parser.parse_args()
SPAHandler.api_port = args.api_port
SPAHandler.base_prefix = args.base.rstrip("/")
os.chdir(args.directory)
server = HTTPServer(("", args.port), SPAHandler)
print(