bun scripts/check-runtime-origins.ts (bun run check:origins) holds the
branch's rule:
- nuxt.config declares neither per-deployment address;
- under app/, only the runtime loader reads dxMapsBase, and only it and the
menu read researchGalleryUrl;
- no source file or static page names a deployment host (nuxtbase.app,
sslip.io, a *.localhost address or a 46xx laptop port).
It refuses to pass on fewer than 50 files.
This branch: PASS, 306 files. The subdomain branch it was cut from fails
with 12 problems: two addresses in nuxt.config, six reads under app/, and
the hardcoded maps host in nuxt.config and in the two static pages.
A second compose profile beside "subdomain":
- dx-maps-studio: the dx-maps container with ACCESS=public on research.localhost:4616, the
PUBLIC Plugin Studio. There is no sign-in provider; publishing and PocketBase's dashboard ask
for ADMIN_PASSWORD. Its /cdn is built from main with EMBED_BASE_URL=http://research.localhost:4616/cdn.
- dxlife-studio: this site's research-gallery-plugin-studio branch on :4614. Its two settings,
NUXT_PUBLIC_RESEARCH_GALLERY_URL and NUXT_PUBLIC_DX_MAPS_BASE (names agreed with dx-base), point
at the Studio and its /cdn, and are read at run time.
The two routes use separate data volumes. .env.laptop gains a generated ADMIN_PASSWORD (never
printed). laptop-dxlife.sh takes DXLIFE_PORT (4613 subdomain, 4614 plugin-studio).
The Research Gallery now has two routes: drop-in (this site alone, no Docker, the default)
and subdomain (the dx-maps team container with PocketBase). The CDN route is retired.
compose.laptop.yaml runs the subdomain route on 127.0.0.1:
- dx-maps: the image the team's server runs, built from dx-maps' root Dockerfile. It carries
the Plugin Studio, PocketBase and the research site on research.localhost:4615, and
publishes main by itself.
- a local sign-in provider with admin, designer and plain-member users. It runs inside dx-maps'
network, so localhost:4620 is the same address for the browser and the container.
- this site on the subdomain branch, pointed at the research site.
scripts/laptop.ts makes .env.laptop (secrets, never committed), checks Docker's memory (and
names what to close or the lighter route), starts the three, waits for each and prints the
addresses. LAPTOP-6WAY.md describes the two routes.
Checked on a Docker host tunnelled to a laptop-like browser: 20/20 route checks (sign-in,
roles, PocketBase's team API, the research site, /regions) and check-embed-6way 7/7 at 6/6.
The importer and menu-order sync wrote pages.parent_slug, dropped in
migration 010; the scraper fed the importer and read the site being
replaced; po-to-json rebuilt the locale JSON from the WP theme's .po
files, which the glossary now owns. Content lives in the database and
is edited through the doxa-cms MCP server.
config/glossaries.ts was generated from the retired translation repo's DeepL
TSVs, so it had no source left and only the German, Spanish and Portuguese
rows had been hand-patched. Both it and its build script are gone.
- fetch GET /api/glossary/{lang} at runtime, cached ten minutes, serving the
last good answer when the request fails and an empty one when it never
succeeded. A reviewer's correction now reaches auto-translate without a
deploy here
- carry the glossary's notes as well as its terms: register, acronym policy
and number format, placed after the term list so a rule about inflection
governs the pairs above it
- glossary_used on the MCP translate tool now means the locale's terminology
was available, and reads false for a language whose glossary has not been
started
- add /add-language and /sync-language skills for the site strings, the map
widget locales and bundles, the /terms edition, and the CMS pages
- translateText/translateTexts call OpenRouter chat-completions; glossary
terms are prompt-injected from config/glossaries.ts, generated from the
translation repo's TSVs by scripts/build-glossaries.ts
- model resolution: per-language override in config/languages.ts →
translation.model site setting (new admin field) → TRANSLATION_MODEL env
→ anthropic/claude-opus-5, at low reasoning effort
- blank fragments are held back from the model, which drops them and breaks
the one-translation-per-fragment count the caller maps results back on
- OpenRouter failures map to a typed error with a human-readable message;
an unrecoverable one (no credits, bad key, rejected model) stops the run
instead of repeating the same call for every remaining locale
- machine translations record source 'auto-translate'; 'deepl' stays
readable for snapshots written before the switch