Skip to content

Commit 80f5e51

Browse files
committed
fix(spa): serve /app assets and align app base path
1 parent f20f8f3 commit 80f5e51

7 files changed

Lines changed: 53 additions & 3 deletions

File tree

.dockerignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@ state/
88
data/
99
stat_modules/__pycache__/
1010
.venv/
11+
chronicle-ui/node_modules/
12+
chronicle-ui/dist/
13+
chronicle-ui/*.tsbuildinfo
14+
chronicle-ui/vite.config.d.ts
15+
.agents/
16+
_bmad/
17+
_bmad-output/
18+
docs/

chronicle-ui/src/main.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { AppRoutes } from "./app-routes";
88
import { appTheme } from "./theme/app-theme";
99
import { useAppReady } from "./hooks/use-app-ready";
1010

11+
const routerBasePath = (import.meta.env.BASE_URL || "/").replace(/\/+$/, "") || "/";
12+
1113
function AppRoot() {
1214
const appReady = useAppReady();
1315
if (!appReady) {
@@ -17,7 +19,7 @@ function AppRoot() {
1719
return (
1820
<ThemeProvider theme={appTheme}>
1921
<CssBaseline />
20-
<BrowserRouter>
22+
<BrowserRouter basename={routerBasePath}>
2123
<AppShell>
2224
<AppRoutes />
2325
</AppShell>

chronicle-ui/src/vite-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />

chronicle-ui/vite.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { defineConfig } from "vitest/config";
22
import react from "@vitejs/plugin-react";
33
var proxyTarget = process.env.VITE_API_PROXY_TARGET || "http://localhost:1609";
4+
var rawBasePath = process.env.VITE_BASE_PATH || "/app/";
5+
var normalizedBasePath = "/".concat(rawBasePath.replace(/^\/+/, "").replace(/\/+$/, ""), "/");
46
export default defineConfig({
7+
base: normalizedBasePath,
58
plugins: [react()],
69
server: {
710
proxy: {

chronicle-ui/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { defineConfig } from "vitest/config";
22
import react from "@vitejs/plugin-react";
33

44
const proxyTarget = process.env.VITE_API_PROXY_TARGET || "http://localhost:1609";
5+
const rawBasePath = process.env.VITE_BASE_PATH || "/app/";
6+
const normalizedBasePath = `/${rawBasePath.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
57

68
export default defineConfig({
9+
base: normalizedBasePath,
710
plugins: [react()],
811
server: {
912
proxy: {

chronicle/api_server.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from urllib.parse import urlencode
99

1010
import requests
11-
from flask import Flask, redirect, render_template, request, send_from_directory
11+
from flask import Flask, abort, redirect, render_template, request, send_from_directory
1212
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
1313

1414
from .activity_pipeline import preview_profile_match, run_once
@@ -926,10 +926,18 @@ def legacy_core_flow_page(flow_name: str):
926926
def spa_app_entry(spa_path: str = ""):
927927
dist_dir = PROJECT_ROOT / "chronicle-ui" / "dist"
928928
index_file = dist_dir / "index.html"
929+
requested_path = str(spa_path or "").strip("/")
929930
if index_file.exists():
931+
if requested_path:
932+
requested_file = dist_dir / requested_path
933+
if requested_file.is_file():
934+
return send_from_directory(str(dist_dir), requested_path)
935+
if "." in Path(requested_path).name:
936+
# Missing concrete asset (JS/CSS/map/etc.) should not return HTML fallback.
937+
return abort(404)
930938
return send_from_directory(str(dist_dir), "index.html")
931939

932-
first_segment = str(spa_path or "").strip("/").split("/", 1)[0]
940+
first_segment = requested_path.split("/", 1)[0]
933941
normalized_flow = _normalize_ui_flow(first_segment)
934942
if normalized_flow is None:
935943
return redirect(_legacy_flow_path("view"), code=302)

tests/test_api_server.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,31 @@ def test_root_redirects_to_view(self) -> None:
154154
self.assertEqual(response.status_code, 302)
155155
self.assertEqual(response.headers.get("Location"), "/view")
156156

157+
def test_spa_entry_serves_dist_asset_when_present(self) -> None:
158+
with tempfile.TemporaryDirectory() as temp_dir:
159+
root = Path(temp_dir)
160+
dist_dir = root / "chronicle-ui" / "dist"
161+
assets_dir = dist_dir / "assets"
162+
assets_dir.mkdir(parents=True, exist_ok=True)
163+
(dist_dir / "index.html").write_text("<html><body><div id='root'></div></body></html>", encoding="utf-8")
164+
(assets_dir / "index.js").write_text("console.log('spa boot');", encoding="utf-8")
165+
166+
with patch.object(api_server, "PROJECT_ROOT", root):
167+
response = self.client.get("/app/assets/index.js")
168+
self.assertEqual(response.status_code, 200)
169+
self.assertIn("spa boot", response.get_data(as_text=True))
170+
171+
def test_spa_entry_returns_404_for_missing_dist_asset(self) -> None:
172+
with tempfile.TemporaryDirectory() as temp_dir:
173+
root = Path(temp_dir)
174+
dist_dir = root / "chronicle-ui" / "dist"
175+
dist_dir.mkdir(parents=True, exist_ok=True)
176+
(dist_dir / "index.html").write_text("<html><body><div id='root'></div></body></html>", encoding="utf-8")
177+
178+
with patch.object(api_server, "PROJECT_ROOT", root):
179+
response = self.client.get("/app/assets/missing.js")
180+
self.assertEqual(response.status_code, 404)
181+
157182
def test_dashboard_page_endpoint(self) -> None:
158183
response = self.client.get("/dashboard")
159184
self.assertEqual(response.status_code, 200)

0 commit comments

Comments
 (0)