Bake the API base into the dev pages so dashboards are not silently empty
Some checks are pending
build-gate / build (push) Waiting to run

The built leaf pages set window.MAP_APP_API_URL; the dev pages set only the Mapbox
token. Every dashboard in dev therefore fell back to a relative /api/people-groups/list,
which Vite answers with SPA-fallback HTML at HTTP 200 — so the fetch "succeeds", the
parse yields nothing, and the facet rail renders zero rows with no console error and no
error card. One studio sweep produced eight such requests. It reads as a broken
component and is really a missing global.

Both dev routes now read the value from the same build config the built leaves use, so
the two surfaces agree by construction rather than through a second hard-coded copy, and
each route resolves the token and the API base from a single config read so they cannot
disagree about their source. Measured after: the research dashboard renders 66 facet
rows and 216 table cells with zero 4xx.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 14:43:24 +00:00
parent afb998f17b
commit 371119a3f8

View file

@ -174,8 +174,12 @@ export function autoSpaDevPlugin(token = '') {
const entry = reg.find(b => b.bundle === bundle)
if (!entry) return json(res, { error: 'unknown bundle', bundle }, 404)
const tag = entry.tag
let leafTok = token
if (!leafTok) { try { leafTok = (readBuildConfig() || {}).token || '' } catch { leafTok = '' } }
// One config read for BOTH globals, so the token and the API base can never disagree about
// which config they came from.
let leafCfg = {}
try { leafCfg = readBuildConfig() || {} } catch { leafCfg = {} }
const leafTok = token || leafCfg.token || ''
const apiBase = leafCfg.apiBaseUrl || ''
// profile-config carries the same shape the built leaves pass, so a profile/instance
// renders the SAME view here as in production rather than the bundle's default.
const pc = JSON.stringify(instance ? { profile, instanceId: instance } : { profile })
@ -188,6 +192,16 @@ export function autoSpaDevPlugin(token = '') {
'<meta name="viewport" content="width=device-width, initial-scale=1.0" />',
'<link rel="icon" href="data:,">',
leafTok ? `<script>window.MAP_TOKEN=${JSON.stringify(leafTok)};</script>` : '',
// ★ MAP_APP_API_URL IS NOT OPTIONAL — without it every dashboard renders EMPTY in dev.
// The BUILT leaves bake `window.MAP_APP_API_URL = "https://pray.doxa.life"`; these dev
// pages baked only the Mapbox token, so the dashboards fell back to a RELATIVE
// `/api/people-groups/list?...`. Vite answers an unknown path with SPA-fallback HTML at
// HTTP 200, so the fetch "succeeds", JSON.parse fails or yields nothing, and the facet
// rail renders ZERO rows with no console error and no error card. Measured 2026-09-22:
// 8 such 404/fallback requests during one studio sweep. That reads as a broken component
// and is really a missing global — the worst kind of dev-only divergence, and exactly the
// four-surface class (see REFERENCE-the-four-surface-contract.md).
apiBase ? `<script>window.MAP_APP_API_URL=${JSON.stringify(apiBase)};</script>` : '',
'<style>html,body{margin:0;padding:0;height:100%;background:#f3f3f1;}',
`${tag}{display:block;width:100%;height:100vh;}</style>`,
'</head><body>',
@ -230,8 +244,11 @@ export function autoSpaDevPlugin(token = '') {
// called HERE rather than reusing page()'s `cfg` — that const lives inside the page()
// arrow function, so it is not in scope in this middleware (an earlier version referenced
// it and the dev server 500'd with "cfg is not defined").
let devTok = token
if (!devTok) { try { devTok = (readBuildConfig() || {}).token || '' } catch { devTok = '' } }
// One config read for BOTH globals (see the leaf route above for why the API base matters).
let devCfg = {}
try { devCfg = readBuildConfig() || {} } catch { devCfg = {} }
const devTok = token || devCfg.token || ''
const devApiBase = devCfg.apiBaseUrl || ''
res.statusCode = 200
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Cache-Control', 'no-cache')
@ -244,6 +261,11 @@ export function autoSpaDevPlugin(token = '') {
// The token ladder's baked rung, exactly as the built leaves receive it, so maps paint
// in dev without anyone appending ?tk=.
devTok ? `<script>window.MAP_TOKEN=${JSON.stringify(devTok)};</script>` : "",
// Same reason as the leaf route above: without MAP_APP_API_URL the dashboards nested in
// the studio fetch a relative /api/... that Vite answers with HTML at 200, so they mount
// and then show nothing. The value comes from the SAME build config the built leaves use,
// so dev and built agree by construction rather than by a second hard-coded copy.
devApiBase ? `<script>window.MAP_APP_API_URL=${JSON.stringify(devApiBase)};</script>` : "",
// No chrome, no prose: the element fills the viewport. "Without a lot of text garb
// that takes up so much space" is a layout requirement, so the host page contributes
// ZERO text — every pixel belongs to the studio element.