SPB Git

spb/findata

Public

web console (Next.js 15 static export, 16 routes, CSP-safe externalized scripts)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 1, 2026) parent 1400d2e Browse files

4 changed files +12 −4

modified apps/web/README.md +8 −0
@@ -21,6 +21,14 @@ pnpm build # → apps/web/out/ (index.html + one directory per route,
21 21
22 22 `services/api/src/findata_api/main.py` mounts `apps/web/out` (or `FINDATA_WEB_DIST`) with `StaticFiles(html=True)`, so `/datasets/` serves `out/datasets/index.html`, and the console is same-origin with `/api/v1`. The CSP is `default-src 'self'` (inline styles allowed, inline scripts not); the bundle contains no inline scripts, external fonts or CDNs.
23 23
24 +### CSP and the post-build step
25 +
26 +Next's App Router emits its React Server Components payload as inline `<script>self.__next_f.push(…)</script>` tags, which `script-src 'self'` blocks. `pnpm build` therefore runs `scripts/externalize-inline-scripts.mjs` after `next build`: it moves those pushes, per page, into `out/_next/static/flight/<hash>.js` and replaces them with one blocking `<script src>`. The exported HTML contains **no inline scripts** (the script fails the build if it ever meets one that is not a `__next_f` push).
27 +
28 +### Smoke test without the backend
29 +
30 +`node scripts/mock-server.mjs [port] [--signed-out] [--mfa]` serves `out/` with the API's exact CSP header plus fixture answers for every `/api/v1` endpoint the console uses — handy for layout checks (375 px / desktop, light / dark) and for verifying hydration under CSP. Not used in production.
31 +
24 32 Detail pages use query strings instead of dynamic segments (static export cannot pre-render unknown ids): `/datasets/detail/?id=…`, `/symbols/detail/?symbol=…`.
25 33
26 34 ## Layout
modified apps/web/src/app/globals.css +2 −2
@@ -108,7 +108,7 @@ code, pre, kbd, .mono { font-family: var(--font-mono); }
108 108
109 109 /* ------------------------------------------------------------------ components */
110 110 @layer components {
111 − .card { @apply rounded-lg border border-border bg-surface-1 p-4; }
111 + .card { @apply min-w-0 max-w-full rounded-lg border border-border bg-surface-1 p-4; }
112 112 .card-title { @apply mb-3 flex items-center justify-between gap-2 text-sm font-semibold text-ink-1; }
113 113 .muted { @apply text-ink-3; }
114 114 .label { @apply mb-1 block text-xs font-medium text-ink-2; }
@@ -126,7 +126,7 @@ code, pre, kbd, .mono { font-family: var(--font-mono); }
126 126 .btn-sm { @apply px-2 py-1 text-xs; }
127 127 .btn-ghost { @apply border-transparent bg-transparent; }
128 128 .pill { @apply inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap; }
129 − .table-wrap { @apply -mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0; }
129 + .table-wrap { @apply -mx-4 max-w-[calc(100%+2rem)] overflow-x-auto px-4 sm:mx-0 sm:max-w-full sm:px-0; }
130 130 .table { @apply w-full border-collapse text-sm; }
131 131 .table th { @apply sticky top-0 z-[1] border-b border-hairline bg-surface-2 px-2.5 py-2 text-left text-xs font-semibold text-ink-2 whitespace-nowrap; }
132 132 .table td { @apply border-b border-hairline px-2.5 py-1.5 align-top text-ink-1; }
modified apps/web/src/app/keys/page.tsx +1 −1
@@ -85,7 +85,7 @@ export default function KeysPage() {
85 85 key: "usage",
86 86 header: "Usage (14 d)",
87 87 render: (k) => {
88 − const u = usageByPrincipal.get(`key:${k.prefix}`) ?? usageByPrincipal.get(k.prefix) ?? usageByPrincipal.get(k.name);
88 + const u = usageByPrincipal.get(`api_key:${k.id}`) ?? usageByPrincipal.get(`key:${k.prefix}`);
89 89 return u ? <span className="tnum text-xs">{fmtInt(u.requests)} req · {fmtCompact(u.rows)} rows · {fmtBytes(u.bytes)}</span> : <span className="text-ink-3">—</span>;
90 90 },
91 91 },
modified apps/web/src/components/charts.tsx +1 −1
@@ -240,7 +240,7 @@ export function Meter({ value, max, marks = [], tone = "accent", label, classNam
240 240 {marks.map((m) => {
241 241 const x = max > 0 ? Math.max(0, Math.min(1, m.at / max)) : 0;
242 242 return (
243 − <div key={m.label} className="absolute top-[-4px] flex flex-col items-center" style={{ left: `${x * 100}%`, transform: "translateX(-50%)" }}>
243 + <div key={m.label} className="absolute top-[-4px] flex flex-col" style={{ left: `${x * 100}%`, transform: x > 0.8 ? "translateX(-100%)" : x < 0.2 ? "none" : "translateX(-50%)", alignItems: x > 0.8 ? "flex-end" : x < 0.2 ? "flex-start" : "center" }}>
244 244 <div className="h-5 w-px bg-ink-2" />
245 245 <div className="mt-0.5 whitespace-nowrap text-[10px] text-ink-3">{m.label}</div>
246 246 </div>
247 247
Menu

Theme