Keep the site's own files as upstream has them

Outside the maps folder, the two hidden pages and the three maps, the site is upstream's again: the card styles, the embeddables' legend and the devtools setting are restored, the laptop notes and the embed test page are removed, and DX-MAPS-GALLERY.md is the guide to what this branch changes.
This commit is contained in:
Doxa Life 2026-10-01 22:11:27 +00:00
parent da3312ba17
commit abbd86d5e7
8 changed files with 44 additions and 480 deletions

View file

@ -1,51 +1,48 @@
# Deep Research (the Research Gallery) on a hidden page, and every map served from /dx-maps/
# Deep Research and Posterverse: the dx-maps output folder dropped into the site
This branch adds the **Deep Research** page, `/deep-research`: the Research Gallery (the research
maps, dashboards and lists in one app), filling the page below the site header, on desktop and
phone. The page is **hidden**: it is in no menu, and robots are told not to index it; it is reached
by its address. The Research page is DX Life's own again, with the production simple map. Every map
on the site loads from the same build tree.
This branch drops the **dx-maps output folder** into `public/dx-maps/` and adds two **hidden pages** that
show it as full-page apps. Every map on the site loads from that folder, on the site's own origin.
## What changed
## What changes
| | |
|---|---|
| `public/dx-maps/` | **The dx-maps output folder, dropped in** (doxa-maps `development` 53fcf6d09, `public/dx-maps/`, copied unchanged). Every map on this site loads from it, same-origin. |
| `app/pages/deep-research/index.vue` | **Deep Research: the Research Gallery.** The page loads one script (`/dx-maps/doxa-maps-spa/doxa-maps-spa.js`) and adds one element (`<doxa-maps-spa>`), sized to the viewport below the header; its `tree-base` is built at runtime from the page's own origin. Every section of the gallery shows, the Experimental section included. Hidden: in no menu, `robots: noindex`. |
| `app/pages/research/index.vue` | **The Research page, DX Life production's (fc4d622):** the heading, the "By Region" link, the research map on its own (`bundle="research-map"`, `<doxa-research-map>`) and the UUPG list. One addition, under the map: "Want Deeper Research? See our Deep-Research Maps →", linking to `/deep-research` (the locale prefix kept). The two phrases, and the page name "Deep Research", are in `i18n/locales/<code>/common.json` for all 11 locales; "Deep Research" stays a name in every language, as the gallery's own heading does. |
| `app/pages/regions/index.vue`, `app/utils/nav.ts`, `nuxt.config.ts` | `/regions` is DX Life's own index again, with its menu item, and `/countries` goes to it (`USE_LEGACY_REGIONS_PAGE` and `LEGACY_REGIONS_PAGE` on). Set both off to send `/regions` to a gallery tab instead. |
| `app/composables/useDoxaMap.ts` | The home (engagement), pray (prayer) and adopt (adoption) maps now load `/dx-maps/doxa-simple-map/doxa-simple-map.js`; the research-map key loads `/dx-maps/doxa-research-map/doxa-research-map.js`. Mapbox GL moves from v3.3.0 to v3.24.0, the version the dx-maps bundles are built and tested with. |
| `app/components/DoxaMapSlot.vue` | With no Mapbox token configured, it asks the map for the keyless MapLibre engine (see **Map engine**). With a token, the config passes through unchanged. The map fills the slot's box, not the viewport (`--profile-min-h: 0`), so the slot never cuts off the map's attribution and bottom controls. |
| `public/embed/map/index.html`, `public/research/doxa-research-map.html` | The same: bundles from `/dx-maps/`, Mapbox GL v3.24.0. |
| `public/js/doxa-simple-map.js`, `public/js/doxa-research-map.js` | **Kept, unused.** No page loads them any more (every map loads from `/dx-maps/`), but they stay in place so nothing DX-life relied on is deleted. |
| `public/dx-maps/` | **The dx-maps output folder, dropped in unchanged** (doxa-maps 53fcf6d09). Every map on the site loads from it. |
| `app/pages/deep-research/index.vue` | **Deep Research** (`/deep-research`): every research map, dashboard and list in one app (`<doxa-maps-spa>`). A full-page app on every screen: no site header or footer, and the page itself never scrolls. The way back to the site is the app's own **Back to homepage** button, above **Collapse** in its menu. |
| `app/pages/posterverse/index.vue` | **Posterverse** (`/posterverse`): the poster plugins in one app (`<posterverse-app>`), the same full-page way. |
| `app/pages/research/index.vue` | The Research page keeps its research map. Under it, one new line, "Want Deeper Research? Deep Research app →", links to `/deep-research` in the reader's language. |
| `app/composables/useDoxaMap.ts`, `app/components/DoxaMapSlot.vue`, `app/utils/mapProvider.ts` | The home (engagement), pray (prayer), adopt and research maps load their bundles from `/dx-maps/`: the same maps and presets as before. Their engine is MapLibre unless Mapbox is chosen (see **Map engine**): to keep today's Mapbox look, set `NUXT_PUBLIC_MAP_PROVIDER=mapbox` with the existing token. |
| `public/embed/map/index.html`, `public/research/doxa-research-map.html` | The same: bundles from `/dx-maps/`. |
| `nuxt.config.ts` | Old `/js/doxa-maps/…` addresses redirect to `/dx-maps/`, and the map settings below. |
| `i18n/locales/*/common.json` | The new strings, in all 11 languages. |
Unchanged on purpose: the element names (`<doxa-map>`, `<doxa-research-map>`, `<doxa-maps-spa>`)
and `nuxt.config.ts` (its `isCustomElement` lists only those names). `public/js/pplr-data-maps.iife.js`
and `/research/pplr-data-maps.html` are a separate showcase with no dx-maps equivalent, so they are
also unchanged.
Unchanged: the regions index, the region and country pages and their maps, the header, the footer and the layout.
`public/js/doxa-simple-map.js` and `public/js/doxa-research-map.js` stay in place, unused.
**Language.** The gallery follows the site's language and the language switcher (all 11 locales;
Arabic lays out right-to-left).
**Hidden pages.** `/deep-research` and `/posterverse` are in no menu, and robots are told not to index them.
`/deep-research` is reached by the link under the Research page's map; `/posterverse` by its address.
**Map engine.** Every map on the site (the Deep Research gallery and the home, pray, adopt, research
and country maps) uses the keyless MapLibre engine by default, so the whole site's maps work with no
secrets at all. To switch back to Mapbox, as doxa.life uses it today, set
`NUXT_PUBLIC_MAP_PROVIDER=mapbox` together with `NUXT_PUBLIC_MAPBOX_TOKEN`; a token alone does not
switch, and "mapbox" without a token falls back to MapLibre. The page loads only the map bundle; each
map loads its own engine, so nothing is fetched from api.mapbox.com unless Mapbox is chosen.
**Language.** Both apps follow the site's language (all 11 locales; Arabic lays out right to left). Each app
also has its own language menu (in the phone bar, and at the bottom of the menu on larger screens). A pick
there fires a cancelable `doxa-language-request` event (`event.detail.language`). Both pages cancel it and
switch the site's language (the language is in the address, `/ar/…`), so the site stays the one owner of the
language, and the tab that was open carries over.
**What comes from elsewhere.** The map code and the data folder are this site's own (`/dx-maps/`). The
MapLibre engine comes from jsDelivr, the base-map tiles from OpenFreeMap and VersaTiles, one
dashboard's country figures from the World Bank API, and the maps' people-group data from the prayer
API (`NUXT_PUBLIC_PRAY_BASE_URL`, default `https://pray.doxa.life`).
**Map engine.** Every dx-maps map uses the keyless MapLibre engine by default, so the maps need no
secrets. To use Mapbox, set `NUXT_PUBLIC_MAP_PROVIDER=mapbox` together with `NUXT_PUBLIC_MAPBOX_TOKEN`; a
token alone does not switch, and "mapbox" without a token falls back to MapLibre.
**One scrollbar.** The gallery is a full-page app: it fills exactly the space between the header and
the bottom of the screen. The Deep Research page has no footer and does not scroll itself, so the
only scrollbar is the gallery's own.
**Settings** (all optional):
- `NUXT_PUBLIC_DX_MAPS_BASE`: load the maps from another copy of the folder (a URL; that host must send
CORS headers). Unset = this site's own `/dx-maps/`.
- `NUXT_PUBLIC_MAP_PROVIDER`, `NUXT_PUBLIC_MAPBOX_TOKEN`: the map engine, as above.
- `NUXT_PUBLIC_PRAY_BASE_URL`: the prayer API the maps read people-group data from (default
`https://pray.doxa.life`).
**Phone.** The gallery opens straight on a map (Simple Research). Its list of views sits behind the
☰ button. Big Picture is marked **Desktop only** there (it needs a wide screen); every other view
works on a phone.
**What comes from elsewhere.** The map code and its data folder are this site's own (`/dx-maps/`). The
MapLibre engine comes from jsDelivr, the base-map tiles from OpenFreeMap and VersaTiles, one dashboard's
country figures from the World Bank API, the people-group data from the prayer API, and the Bible Posters'
Bible text from bible.helloao.org (public-domain translations, credited to ebible.org).
## Run it
@ -54,37 +51,13 @@ bun install
bun run dev
```
Then open <http://localhost:3000/deep-research/> (Nuxt prints the port if 3000 is taken). No database
or `.env` is needed for this page. Without `DATABASE_URL`, the dev server only logs
"skipping migrations".
Then open `/deep-research` and `/posterverse` (Nuxt prints the address). No database or `.env` is needed for
these pages; without `DATABASE_URL` the dev server only logs "skipping migrations".
- Arabic: <http://localhost:3000/ar/deep-research/>
- Open a specific view: `/deep-research/?tab=prayer-map` (also `research-map-plus-grid`,
`regions-dashboard`, `upg-100`, and so on).
- The Research page with the simple map: <http://localhost:3000/research/>
- Share and embed links in the maps point at this site's own `/dx-maps/` pages.
- To run it against a **local** dx-maps, and to try all three ways a site can take the gallery (drop-in,
CDN, subdomain) with the exact ports and settings: [documentation/LAPTOP-6WAY.md](documentation/LAPTOP-6WAY.md).
## Deployment method: the dx-maps output folder dropped in (this branch, deep-research, from research-gallery-drop-in)
Identical to research-gallery-js (maps from the CDN), except that the dx-maps **output folder is dropped into `public/dx-maps/`** and
every map loads **same-origin** from it: `NUXT_PUBLIC_DX_MAPS_BASE` defaults to empty, which means this
site's own `/dx-maps/`. That covers the Research Gallery, the home (engagement), pray (prayer) and adopt maps
(`useDoxaMap`, `DoxaMapSlot`), and the static pages `public/embed/map/` and
`public/research/doxa-research-map.html` (which carry `/dx-maps` as a constant). Each map is told its
`assetsBase` in the tree. No iframe is used, and no other host is involved.
## /embed-test
`/embed-test` embeds **every web plugin** in the dx-maps tree, as JavaScript (no iframe). The list is read
at runtime from the tree's `manifest.json`, never typed: section 1 is every profile with `stage: production`,
section 2 every other profile. A profile whose `targets` leave out `web` (a desktop-only plugin, such as
Rudder) is skipped. Each card loads its bundle's script and adds the element that bundle defines. A card
mounts its plugin when it scrolls into view and removes it when it leaves (browsers allow only a few WebGL
maps at once). `?only=<bundle>/<profile>` (or a bundle name, comma-separated) shows just those.
- Arabic: `/ar/deep-research`, `/ar/posterverse`
- A specific view: `/deep-research?tab=prayer-map` (also `research-map-plus-grid`, `regions-dashboard`,
`upg-100`, …), `/posterverse?tab=country-posters`
## Updating the maps
Replace `public/dx-maps/` with a newer build of the tree, from the doxa-maps repo's `public/dx-maps/`.
No page needs to change.
Replace `public/dx-maps/` with the `public/dx-maps/` folder of a newer doxa-maps build. No page needs to change.

View file

@ -1,70 +0,0 @@
# Deep Research on a laptop
Run this branch (`deep-research`) of the DX Life site on your own machine and look at the
hidden Deep Research page and the Research page. No database, no Docker, no secrets.
## You need
- **bun 1.3+** and **node 20+**
- **git**, and the address of the repository that holds this branch
## Steps
```bash
git clone -b deep-research --single-branch <repository-url> dx-life-deep-research
cd dx-life-deep-research
bun install
PORT=4611 bun run dev
```
The first start compiles for about a minute; a page may answer 503 until it is done. Without a
database the server only logs "skipping migrations" and the oauth sweeps' "DATABASE_URL is not set";
the map pages do not use a database.
No settings are needed. The maps use MapLibre, which needs no key, and read their data from DX Life's
prayer API (`https://pray.doxa.life`, the default).
## Open
| Page | Address |
|---|---|
| **Deep Research** (hidden: in no menu, not indexed) | <http://localhost:4611/deep-research> |
| Deep Research in Arabic (right-to-left) | <http://localhost:4611/ar/deep-research> |
| One view of it, e.g. the Regions dashboard | <http://localhost:4611/deep-research?tab=regions-dashboard> |
| **Research** (as on doxa.life today, with our research map and, under it, the link to Deep Research) | <http://localhost:4611/research> |
| Regions (DX Life's own index, back in the menu) | <http://localhost:4611/regions> |
| Home, Pray, Adopt (DX Life's own maps, unchanged) | <http://localhost:4611/>, [/pray](http://localhost:4611/pray), [/adopt](http://localhost:4611/adopt) |
| Plugin Studio (it is in the maps folder; no page needed) | <http://localhost:4611/dx-maps/plugin-studio/plugin-studio/> |
## Map engine: MapLibre by default, Mapbox by one setting
**Every map on the site uses MapLibre by default**, including DX Life's own maps on the home, pray,
adopt and country pages. MapLibre needs no key, so the site, and any partner site, works with no
token.
**To switch back to Mapbox**, as doxa.life uses it today, set one setting plus the token, then start
the server:
```bash
NUXT_PUBLIC_MAP_PROVIDER=mapbox NUXT_PUBLIC_MAPBOX_TOKEN=pk.your-token PORT=4611 bun run dev
```
On a deployed site, set the same two variables in its environment. Without a token, "mapbox" falls
back to MapLibre, so a map always starts.
## What is in this branch
- `/deep-research`: the Deep Research gallery (the DX Maps mobilization maps in one app, the
Experimental section included), as a full-page app with no site header; its "Back to homepage" button sits
above the menu's Collapse button. Every tab has an info button
that explains what the map does.
- `/research`: DX Life's production page (fc4d622), with the research map on its own, now loaded from
this site's own `/dx-maps/`, and under it one new line: "Want Deeper Research? Deep Research
app →", which links to `/deep-research` in the reader's language. The line is in all
11 of the site's locales.
- `/` and `/pray`: the engagement map and the prayer map (the simple map with those presets), as before.
- `public/dx-maps/`: the DX Maps output folder, from DX Maps `development` 53fcf6d09.
- The page never loads anything from api.mapbox.com; a map loads Mapbox itself, and only when Mapbox
is chosen. A slow or blocked Mapbox cannot stop a map from appearing.
More detail: [DX-MAPS-GALLERY.md](DX-MAPS-GALLERY.md).

View file

@ -56,19 +56,10 @@
position: relative;
overflow: hidden;
border-radius: var(--border-radius-lg);
// An <iframe> is composited in its own layer: `overflow: hidden` + `border-radius` on the
// parent does not clip it in Chrome until the layer tree settles after the frame paints,
// so the map showed square corners until it loaded and rounded ones after. clip-path is
// applied at paint time regardless of compositing, and the iframe carries the radius too.
clip-path: inset(0 round var(--border-radius-lg));
isolation: isolate;
iframe {
display: block;
width: 100%;
height: 20rem;
border: 0;
border-radius: inherit;
}
.overlay {

View file

@ -1,191 +0,0 @@
<script setup lang="ts">
// /embed-test — proves that EVERY dx-maps plugin built for the web embeds on this site as JavaScript
// (no iframe). The list is read at runtime from the dx-maps tree's manifest.json, never typed here:
// section 1 is every profile whose stage is "production", section 2 every other profile. A profile whose
// manifest `targets` leaves out "web" (a desktop-only plugin) is skipped. So a plugin promoted, added or
// retired changes this page by itself. Each card loads its bundle's script from the same tree the rest of the site uses and adds
// the element the bundle defines, the way the manifest's embed.script snippet does.
//
// A map owns a WebGL context and a browser allows only a handful at once, so a card MOUNTS its
// element when it scrolls into view and REMOVES it when it scrolls far away. ?only=<bundle>/<profile>
// (comma-separated, or a bundle name) shows just those cards, for a single-plugin check.
import { mapboxTokenIfChosen } from '~/utils/mapProvider'
useHead({ title: 'Embed test', meta: [{ name: 'robots', content: 'noindex' }] })
interface Card {
key: string
bundle: string
profile: string
tag: string
script: string
integrity: string
description: string
production: boolean
locale: string // the language handed to the element (data-locale, for a probe)
state: 'idle' | 'loading' | 'mounted' | 'error'
error: string
}
const runtimeConfig = useRuntimeConfig()
const route = useRoute()
const { locale } = useI18n() // the site's language, handed to every plugin (it is in the path, not ?lang)
const cards = ref<Card[]>([])
const status = ref('Reading the manifest…')
const hosts = new Map<string, HTMLElement>()
let observer: IntersectionObserver | null = null
// The dx-maps tree: NUXT_PUBLIC_DX_MAPS_BASE when set, otherwise this site's own /dx-maps/ copy.
function treeBase(): string {
const base = ((runtimeConfig.public as { dxMapsBase?: string }).dxMapsBase || '').replace(/\/+$/, '')
return base || `${window.location.origin}/dx-maps`
}
const resolveUrl = (base: string, ref: string) =>
/^(https?:)?\/\//.test(ref) ? ref : ref.startsWith('/') ? new URL(ref, base).href : `${base}/${ref.replace(/^\.\//, '')}`
const scripts = new Map<string, Promise<void>>()
function loadScript(src: string, integrity: string): Promise<void> {
if (!scripts.has(src)) {
scripts.set(src, new Promise((resolve, reject) => {
const s = document.createElement('script')
s.src = src
if (integrity) { s.integrity = integrity; s.crossOrigin = 'anonymous' }
s.onload = () => resolve()
s.onerror = () => reject(new Error(`the script did not load: ${src}`))
document.head.appendChild(s)
}))
}
return scripts.get(src)!
}
// The element's config, in the site's CURRENT language: set on mount and again on every language switch,
// so a mounted plugin follows the switcher (profile-config.locale for the maps, the language attribute for
// the shells).
function applyLocale(el: Element, card: Card) {
const token = mapboxTokenIfChosen(runtimeConfig.public)
el.setAttribute('profile-config', JSON.stringify({
profile: card.profile,
assetsBase: `${treeBase()}/assets`,
locale: locale.value,
// MapLibre by default; Mapbox only when NUXT_PUBLIC_MAP_PROVIDER=mapbox and a token is set
...(token ? { tk: token } : { provider: 'maplibre' })
}))
el.setAttribute('language', locale.value)
card.locale = locale.value
}
watch(locale, () => {
for (const c of cards.value) {
const el = c.state === 'mounted' ? hosts.get(c.key)?.firstElementChild : null
if (el) applyLocale(el, c)
}
})
async function mount(card: Card) {
const host = hosts.get(card.key)
if (!host || card.state === 'mounted' || card.state === 'loading') return
card.state = 'loading'
try {
await loadScript(card.script, card.integrity)
if (!customElements.get(card.tag)) throw new Error(`<${card.tag}> was not defined by its bundle`)
const el = document.createElement(card.tag)
// shells (the Research Gallery, Plugin Studio) find their child maps through the tree's manifest
el.setAttribute('tree-base', treeBase())
applyLocale(el, card)
host.replaceChildren(el)
card.state = 'mounted'
} catch (e) {
card.state = 'error'
card.error = String((e as Error)?.message || e)
}
}
function unmount(card: Card) {
if (card.state !== 'mounted') return
hosts.get(card.key)?.replaceChildren()
card.state = 'idle'
}
onMounted(async () => {
const base = treeBase()
// Must be set before any bundle runs so the maps fetch from the configured prayer API.
const pray = (runtimeConfig.public as { prayBaseUrl?: string }).prayBaseUrl
if (pray) (window as unknown as { MAP_APP_API_URL?: string }).MAP_APP_API_URL = pray
let manifest: { bundles?: Array<Record<string, any>> }
try {
const r = await fetch(`${base}/manifest.json`, { cache: 'no-cache' })
if (!r.ok) throw new Error(`HTTP ${r.status}`)
manifest = await r.json()
} catch (e) {
status.value = `The manifest did not load from ${base}/manifest.json (${(e as Error).message}).`
return
}
const only = String(route.query.only || '').split(',').map((s) => s.trim()).filter(Boolean)
const out: Card[] = []
for (const b of manifest.bundles || []) {
const defined: string[] = b.elements || []
for (const p of b.profiles || []) {
// desktop-only (or any non-web) plugins are not embeddable in a web page
const targets: string[] | undefined = p.targets || b.targets
if (Array.isArray(targets) && !targets.includes('web')) continue
const key = `${b.bundle}/${p.profile}`
if (only.length && !only.includes(key) && !only.includes(b.bundle)) continue
// the tag the bundle actually defines (a profile's own element name can name one it does not)
const tag = p.element && (!defined.length || defined.includes(p.element)) ? p.element : defined[0] || p.element
out.push({ key, bundle: b.bundle, profile: p.profile, tag, script: resolveUrl(base, b.scriptUrl || `${b.bundle}/${b.bundle}.js`),
integrity: b.integrity || '', description: p.description || '', production: (p.stage || b.stage) === 'production', locale: '',
state: 'idle', error: '' })
}
}
cards.value = [...out.filter((c) => c.production), ...out.filter((c) => !c.production)]
const prod = out.filter((c) => c.production).length
status.value = `${out.length} web plugins from ${base}/manifest.json (${prod} production, ${out.length - prod} other), each embedded as JavaScript.`
await nextTick()
observer = new IntersectionObserver((entries) => {
for (const en of entries) {
const card = cards.value.find((c) => c.key === (en.target as HTMLElement).dataset.card)
if (!card) continue
if (en.isIntersecting) mount(card)
else unmount(card)
}
}, { rootMargin: '600px 0px' })
document.querySelectorAll<HTMLElement>('[data-card]').forEach((n) => observer!.observe(n))
})
onBeforeUnmount(() => {
observer?.disconnect()
hosts.forEach((h) => h.replaceChildren())
})
const setHost = (key: string) => (el: unknown) => { if (el) hosts.set(key, el as HTMLElement); else hosts.delete(key) }
</script>
<template>
<div class="container page-content embed-test stack stack--lg">
<h1>Embed test</h1>
<p data-embed-status>{{ status }}</p>
<template v-for="(c, i) in cards" :key="c.key">
<h2 v-if="i === 0 && c.production" data-embed-section="production">1. Production</h2>
<h2 v-if="!c.production && (i === 0 || cards[i - 1].production)" data-embed-section="other">2. Every other web plugin</h2>
<section class="embed-card" :data-card="c.key" :data-state="c.state" :data-production="c.production" :data-locale="c.locale">
<header class="embed-card__head">
<strong>{{ c.bundle }} / {{ c.profile }}</strong>
<code>&lt;{{ c.tag }}&gt;</code>
<span class="embed-card__state">{{ c.state }}</span>
</header>
<p v-if="c.description" class="embed-card__desc">{{ c.description }}</p>
<p v-if="c.error" class="embed-card__error" role="alert">{{ c.error }}</p>
<div :ref="setHost(c.key)" class="embed-card__host" />
</section>
</template>
</div>
</template>
<style scoped>
.embed-card { border: 1px solid #d1d4cd; border-radius: 10px; overflow: hidden; }
.embed-card__head { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: baseline; padding: 10px 12px; background: #f3f3f1; }
.embed-card__state { margin-inline-start: auto; font-size: 12px; color: #6d766e; }
.embed-card__desc { margin: 0; padding: 6px 12px; font-size: 13px; color: #3b463d; }
.embed-card__error { margin: 0; padding: 6px 12px; color: #a0302a; }
.embed-card__host { position: relative; height: 560px; }
.embed-card__host :deep(> *) { display: block; width: 100%; height: 100%; }
@media (max-width: 600px) { .embed-card__host { height: 480px; } }
</style>

View file

@ -1,140 +0,0 @@
# The Research Gallery on your laptop: two routes
DX-life takes the dx-maps Research Gallery in **two routes**, one branch each. This page runs each of them on a
laptop, so you can see and test them before any server is involved. The same two run on a shared server;
together that is the embed test (2 routes × laptop and server).
| Route | DX-life branch | What runs | Docker | PocketBase |
|---|---|---|---|---|
| **Drop-in** (the default) | `research-gallery-drop-in` | this site alone; the maps are its own copy in `public/dx-maps` | no | no |
| **Subdomain** | `research-gallery-subdomain` | the dx-maps **container** (Plugin Studio, PocketBase, the research site) + a local sign-in + this site | yes, about 5 GB | yes, for plugins that have users, an API or a database |
The **CDN** route (`research-gallery-cdn`) was retired on 2026-09-29. Its branch is kept for history.
## Before you start
- **Checkouts side by side:**
```
doxa-maps/ dx-maps, on development (work in progress) or main (production)
doxa-life/ this repo
```
- **bun 1.3+** and **node 20+**. For the subdomain route, **Docker** with Compose: it uses about 1.4 GB, 2.5 GB while
publishing; give Docker 4-6 GB.
- **Once:** `cd doxa-life && bun install`. The branches share one lockfile.
## 1. Drop-in (the default: one server, no Docker)
```bash
cd doxa-life && git switch research-gallery-drop-in
PORT=4611 bun run dev
```
Open http://localhost:4611/research/ (the Research Gallery) and http://localhost:4611/embed-test (every web
plugin, embedded). There is **no setting to change**: the branch loads every map from its own `/dx-maps/` (same
origin), a copy committed with it. The first `bun run dev` compiles for about a minute; a page may answer 503
until it's done.
**To try a newer dx-maps** than the committed copy:
```bash
cd doxa-maps && EMBED_BASE_URL=http://localhost:4611 bun run tree # seconds; only the two manifests change
bun run dropin # copies dx-maps/ into ../doxa-life/public
git restore public # dx-maps back to its committed folder
```
Afterwards, put the site's copy back: `git restore public/dx-maps && git clean -fdq public/dx-maps`.
## 2. Subdomain (the dx-maps container, with PocketBase)
```bash
cd doxa-life && git switch research-gallery-subdomain
bun run laptop subdomain
```
**The first time,** it makes `.env.laptop` (secrets, never committed) and stops if `GIT_TOKEN` is empty.
- Put a read token for the git server that hosts the repositories there and run it again.
- It builds dx-maps' container from `../doxa-maps/Dockerfile`, the image the shared server runs.
- Pull dx-maps `development` first. The first start takes 5-10 minutes: the container clones, installs and
publishes.
It starts three services, all on `127.0.0.1` only:
| Address | What |
|---|---|
| http://localhost:4613/ | this site, subdomain branch. The Research menu and `/regions` open the research site |
| http://research.localhost:4615/ | the research site: the gallery at `/`, `/embed-test`, and the maps under `/dx-maps/`, which this site embeds |
| http://localhost:4616/ | the Plugin Studio, PocketBase (`/pb`), `/cdn`, `/admin` |
| http://localhost:4620/team | the local sign-in provider |
**Sign-in and roles**, the same as on the shared server:
- Sign in to the Studio as **`admin`** (everything, including publishing and the PocketBase dashboard) or
**`designer`** (the Studio and PocketBase's API).
- Any other name is a plain team member, and is refused, as on the server.
- There's no password: the provider is a local stand-in.
**How it is wired:**
- `research.localhost` is your laptop's `research.<host>`. Chrome and Firefox send every `*.localhost` name to
this computer. Safari may not: add `127.0.0.1 research.localhost` to `/etc/hosts`.
- The sign-in provider runs inside the dx-maps container's network, so `localhost:4620` means the same to your
browser and to the container. No hosts-file entry is needed for sign-in.
- Publishing is automatic: a minute after `main` holds a new commit, the container rebuilds `/cdn` and the
research site. The snippets point at `http://research.localhost:4615`.
**If memory is short,** the command says so before starting anything, and names what to close, or the lighter
drop-in route.
**Stop:** `bun run laptop down` keeps PocketBase's data and the published build; `bun run laptop down --wipe`
deletes them. `bun run laptop status` shows what's running.
## What "working" looks like
- **`/embed-test`:** the production cards mount and draw their maps, with 0 page errors and 0 iframes.
`/ar/embed-test` runs the same test right to left.
- **`/research/?tab=regions-dashboard`** opens the gallery on its Regions tab, and `/regions` redirects there.
On the subdomain route that means `http://research.localhost:4615/?tab=regions-dashboard`.
- **Headless**, a checker kept outside this repository (`check-embed-6way.mjs`) checks, for every production plugin:
the script, its integrity hash, its element, its page, CORS, and the copy-paste snippet. The label reads
`<name>=<site origin>@<manifest URL>`:
- drop-in: `drop-in=http://localhost:4611@http://localhost:4611/dx-maps/manifest.json`;
- subdomain: `subdomain=http://research.localhost:4615@http://research.localhost:4615/dx-maps/manifest.json`.
- The **snippet** passes only when the manifests carry the address they're served from:
- the subdomain route bakes it in by itself;
- for the drop-in, regenerate the manifests as shown in 1.
## Without Docker, the subdomain route by hand (no PocketBase)
Two terminals. dx-maps serves its built folder (loopback only), and this site points its research links at it.
```bash
cd doxa-maps && EMBED_BASE_URL=http://localhost:4610 bun run tree && PORT=4610 bun run serve # Terminal A
cd doxa-life && git switch research-gallery-subdomain # Terminal B
NUXT_PUBLIC_DX_MAPS_BASE=http://localhost:4610/dx-maps \
NUXT_PUBLIC_RESEARCH_GALLERY_URL=http://research.localhost:4610/dx-maps/doxa-maps-spa/doxa-maps-spa/index.html \
PORT=4613 bun run dev
```
The dx-maps server does not list directories, so the gallery address ends in `/index.html`. Afterwards:
`cd doxa-maps && git restore public`.
## Checked
The subdomain route was run on 2026-09-29 with this branch's `compose.laptop.yaml`.
- Docker ran on a server, because the checking box has no Docker. Its ports were tunnelled to a Linux box's
`localhost`, standing in for the laptop's browser.
- The dx-maps image was built from `development`'s root `Dockerfile`. The container cloned `development` and, 2.5
minutes after it started, published `main` (93061d2) by itself.
Results:
- **20 of 20 route checks passed:**
- the sign-in provider answers on the same address inside and outside;
- a plain member is refused;
- `designer` gets the Studio, but not the PocketBase dashboard or publishing;
- `admin` gets all three;
- PocketBase's team API is open, with sign-in through the team provider;
- the research site serves the gallery at `/` and a manifest whose snippets point at
`http://research.localhost:4615`;
- `/regions` on this site goes to `http://research.localhost:4615/?tab=regions-dashboard`.
- **`check-embed-6way` on `subdomain=http://research.localhost:4615@…/dx-maps/manifest.json`:** 7 of 7 production
maps pass all 6 checks, the snippet included.
- **Memory once running:** dx-maps 0.3 GB, this site 0.9 GB, the sign-in provider 0.15 GB. The first publish added
about 1 GB for a minute.

View file

@ -26,6 +26,7 @@ dist/
# Internal engineering scratch — never commit
.local-scratch/
.lk10x/
.agent-restricted
build-status.md
embeddables/**/intel/

View file

@ -2,7 +2,8 @@
/**
* SemanticTreeLegend — reusable hierarchical legend for any one-to-many semantic tree.
*
* EXACT CLONE of the upstream PPLR module: a direct clone in which only the API is reduced. Only difference vs.
* EXACT CLONE of the upstream PPLR module
* per user directive: "do a direct clone, only shave the API." Only difference vs.
* upstream is the <style scoped> block converted to useShadowStyles() so the CSS
* lands inside doxa-research-mfe's shadow root.
*

View file

@ -116,8 +116,7 @@ export default defineNuxtConfig({
],
devtools: {
// NUXT_DEVTOOLS=false turns the devtools overlay off (the cloud dev server sets it, for phone testing); unset = on, as before
enabled: process.env.NUXT_DEVTOOLS !== 'false'
enabled: true
},
devServer: {