Cloud container: PocketBase's dashboard renders through /pb, its redirects keep /pb, embed-test reads the data API
Three bugs rega found on a laptop run of the container (2026-09-29): - /pb/_/ never rendered. admin.mjs proxied with fetch(), which returns the body decompressed, but forwarded PocketBase's Content-Encoding: gzip and Content-Length, so the browser tried to gunzip plain HTML. The proxy now drops both headers (for /pb and /api-prod). - PocketBase's own redirects left the mount: /pb/_ went to /_/, and /pb/_/index.html to http://127.0.0.1:8090/_/ (the internal address). Root-relative and upstream-absolute locations are put back under the mount: /pb/_/. - The research host's /embed-test set no MAP_APP_API_URL, so the maps asked that static host's own /api (404s; dashboard-plus-grid drew 489 nodes, not 1,399). The page now sets it from the manifest's runtime.apiBaseUrl before any bundle loads, as every generated page does. Checked both ways against an upstream that answers like PocketBase (gzipped HTML, redirects to /_/). The old proxy sends a gzip label on a plain body and /_/ locations; the new one sends a decodable body and /pb/_/.
This commit is contained in:
parent
93ebe64984
commit
ffebd14377
2 changed files with 18 additions and 1 deletions
|
|
@ -22,10 +22,23 @@ const groupsOf = (req) => (req.headers.get('x-forwarded-groups') || '').split(',
|
|||
const isAdmin = (req) => ADMIN.length > 0 && groupsOf(req).some((g) => ADMIN.includes(g))
|
||||
const text = (s, status = 200) => new Response(s + '\n', { status, headers: { 'content-type': 'text/plain; charset=utf-8' } })
|
||||
|
||||
// fetch() hands back the body DECOMPRESSED, so the upstream Content-Encoding and Content-Length no longer describe
|
||||
// what is sent on: forwarding them made browsers try to gunzip plain HTML, and PocketBase's dashboard never rendered
|
||||
// (rega, 2026-09-29). And PocketBase redirects to its own root (/_ -> /_/), which is put back under the mount (/pb/_/).
|
||||
async function proxy(req, url, prefix, port) {
|
||||
const target = new URL(url.pathname.slice(prefix.length - 1) + url.search, `http://127.0.0.1:${port}`)
|
||||
const r = await fetch(target, { method: req.method, headers: req.headers, body: ['GET', 'HEAD'].includes(req.method) ? undefined : req.body, redirect: 'manual' })
|
||||
return new Response(r.body, { status: r.status, headers: r.headers })
|
||||
const headers = new Headers(r.headers)
|
||||
headers.delete('content-encoding')
|
||||
headers.delete('content-length')
|
||||
const loc = headers.get('location')
|
||||
if (loc) {
|
||||
const mount = prefix.slice(0, -1) // '/pb/' -> '/pb'
|
||||
const upstream = `http://127.0.0.1:${port}`
|
||||
const path = loc.startsWith(upstream) ? loc.slice(upstream.length) || '/' : loc
|
||||
if (path.startsWith('/') && !path.startsWith('//') && !path.startsWith(`${mount}/`)) headers.set('location', mount + path)
|
||||
}
|
||||
return new Response(r.body, { status: r.status, headers })
|
||||
}
|
||||
|
||||
// One public build at a time (build-cdn.sh also holds a lock), started by an admin or by the watcher below.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@
|
|||
const r = await fetch(`${base}/manifest.json`, { cache: 'no-cache' })
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
manifest = await r.json()
|
||||
// The data API the maps read (people groups, statistics). Every generated page and copy-paste snippet sets it
|
||||
// before its bundle loads; this page builds its cards itself, so it sets it from the manifest. Without it the
|
||||
// bundles asked this host's own /api, which the research host doesn't serve (404s, fewer nodes drawn).
|
||||
if (!window.MAP_APP_API_URL && manifest.runtime && manifest.runtime.apiBaseUrl) window.MAP_APP_API_URL = manifest.runtime.apiBaseUrl
|
||||
} catch (e) {
|
||||
status.textContent = `The manifest did not load from ${base}/manifest.json (${e.message}).`
|
||||
return
|
||||
|
|
|
|||
Loading…
Reference in a new issue