embedBase: the asset pool is INSIDE the tree, not beside it
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
resolveAssetsBase computed `<treeRoot>/../assets` -- one path segment too high.
This reached the live site. MEASURED by publishing to the catalog and reverting:
leaf .../doxa-maps/my-upg-100-list/upg-100-list/index.html + '../../assets'
-> /plugin-assets/doxa-maps/assets 200
code root '/plugin-assets/doxa-maps' + '/../assets'
-> /plugin-assets/assets 404
The UPG-100 map went from "195 polys · 27/27 zones · 2107 pins" to
"⚠ boundary fetch 404 world-adm0.geojson" -- a basemap with none of our data on
it. Reverted within minutes; live is healthy.
★ THE COMMENT JUSTIFIED THE OFF-BY-ONE IN PROSE, which is how it survived. It
said the pool is "a SIBLING of the tree -- <root>/../assets -- which is why the
leaf page's generated value is '../../assets' (two up from
<tree>/<bundle>/<profile>/)". Two up from <tree>/<bundle>/<profile>/ is <tree>,
not the tree's parent. The arithmetic was wrong and the sentence made it read
deliberate. generate-tree stages assets INSIDE the tree on purpose -- "staged N
runtime asset(s) -> doxa-maps/assets", so a dropped folder is self-contained.
⚠ WHY FIFTEEN TICKS OF GREEN GATES MISSED IT, three independent reasons:
1. INVISIBLE LOCALLY. The dev web-root has BOTH public/assets (a sibling) and
public/doxa-maps/assets (inside), each holding the boundaries -- so the
wrong path RESOLVES on 127.0.0.1. The catalog publishes app/doxa-maps only;
no sibling exists there. No local run can tell the answers apart.
2. THE BYTE GATES PASSED. check:published and check:catalog-currency both went
GREEN on the broken publish -- 22 of 22 byte-identical, 0 stale. They prove
the bytes match, not that the page works.
3. NO GATE REFERENCED THE FUNCTION, while 10+ profiles across 6 bundles call
it.
So the fix ships with check:embed-base, which is PURE ARITHMETIC -- no browser,
no network, no served tree, so it cannot SKIP itself into irrelevance. It
computes what a leaf's own '../../assets' means and asserts the resolver agrees:
the oracle is what the generated HTML already says, not an opinion about where
assets belong. It also pins the explicit ladder rungs, so a narrow fix cannot
swallow a host-supplied assetsBase.
MUTATION-TESTED: restoring the original '/../assets' fails all four tree roots
(live catalog, dev, doxa.life drop-in, deep subpath) in milliseconds.
This commit is contained in:
parent
379f959038
commit
852310d05e
9 changed files with 140 additions and 16 deletions
105
internal/check-embed-base-resolution.mjs
Normal file
105
internal/check-embed-base-resolution.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-embed-base-resolution — the shared URL resolver, pinned by arithmetic.
|
||||
*
|
||||
* THE DEFECT THIS CATCHES, and it reached the live site. resolveAssetsBase() computed
|
||||
* `<treeRoot>/../assets` — one path segment too high. MEASURED 2026-09-25 by publishing to the
|
||||
* live catalog and reverting:
|
||||
* leaf .../doxa-maps/my-upg-100-list/upg-100-list/index.html + '../../assets'
|
||||
* -> /plugin-assets/doxa-maps/assets 200 what the leaf means
|
||||
* code root '/plugin-assets/doxa-maps' + '/../assets'
|
||||
* -> /plugin-assets/assets 404 what it returned
|
||||
* The UPG-100 map went from "195 polys · 27/27 zones · 2107 pins" to
|
||||
* "⚠ boundary fetch 404 world-adm0.geojson" — a basemap with none of our data on it.
|
||||
*
|
||||
* ★ WHY NOTHING CAUGHT IT. Three reasons, each worth knowing on its own:
|
||||
* 1. IT IS INVISIBLE LOCALLY. The dev web-root has BOTH public/assets (a sibling) and
|
||||
* public/doxa-maps/assets (inside the tree), each holding the boundaries — so the wrong
|
||||
* sibling path RESOLVES on 127.0.0.1. The catalog publishes app/doxa-maps only; there is no
|
||||
* sibling to find. No local run can tell the two answers apart.
|
||||
* 2. THE BYTE GATES PASSED. check:published and check:catalog-currency both went GREEN on the
|
||||
* bad publish — 22 of 22 bundles byte-identical, 22 current / 0 stale. They prove the bytes
|
||||
* match; they do not prove the page works.
|
||||
* 3. NO GATE REFERENCED THE FUNCTION AT ALL, while 10+ profiles across 6 bundles call it.
|
||||
*
|
||||
* ⚠ IT IS PURE ARITHMETIC, deliberately — no browser, no network, no served tree. The defect was
|
||||
* a path calculation, so the check is a path calculation: it runs in milliseconds and cannot be
|
||||
* SKIPped by an absent server, which is how a gate quietly stops covering anything.
|
||||
*
|
||||
* ⚠ AND IT ASSERTS THE LEAF'S OWN MEANING AS THE ORACLE. The generated leaf writes
|
||||
* assetsBase '../../assets' from <tree>/<bundle>/<profile>/, which resolves to <tree>/assets. That
|
||||
* is not an opinion about where assets belong — it is what the HTML already says, so the resolver
|
||||
* agreeing with it is the property. The previous comment claimed the pool was a SIBLING of the
|
||||
* tree and justified the off-by-one in prose, which is how it survived review.
|
||||
*
|
||||
* Usage: node internal/check-embed-base-resolution.mjs
|
||||
*/
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
|
||||
const MOD = pathToFileURL(resolve(REPO, 'library/utils/embedBase.js')).href
|
||||
|
||||
let resolveAssetsBase
|
||||
try {
|
||||
({ resolveAssetsBase } = await import(MOD))
|
||||
} catch (e) {
|
||||
console.error(`EMBED-BASE: FAIL — cannot import ${MOD}: ${String(e.message).slice(0, 90)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Each case pairs a tree root with the URL a LEAF under it would resolve '../../assets' to.
|
||||
// The leaf's own arithmetic is the oracle — computed here rather than hardcoded, so the
|
||||
// expectation cannot drift from what the generated HTML actually means.
|
||||
const TREES = [
|
||||
['live catalog', 'https://nuxtbase.app/plugin-assets/doxa-maps'],
|
||||
['dev tree', 'http://127.0.0.1:4173/doxa-maps'],
|
||||
['doxa.life drop-in', 'https://doxa.life/js/doxa-maps-build/doxa-maps'],
|
||||
['deep subpath', 'https://cdn.example.org/a/b/c/doxa-maps'],
|
||||
]
|
||||
|
||||
const fails = []
|
||||
console.log('EMBED-BASE')
|
||||
|
||||
for (const [name, root] of TREES) {
|
||||
// What a leaf at <tree>/<bundle>/<profile>/index.html means by '../../assets':
|
||||
const leaf = `${root}/some-bundle/some-profile/index.html`
|
||||
const oracle = new URL('../../assets', leaf).href.replace(/\/+$/, '')
|
||||
const got = resolveAssetsBase({ treeBase: root })
|
||||
const ok = got === oracle
|
||||
console.log(` ${ok ? '✓' : '✗'} ${name.padEnd(18)} ${got}`)
|
||||
if (!ok) {
|
||||
fails.push(`${name}: resolver returned ${got}, but a leaf's own '../../assets' means ${oracle}`)
|
||||
}
|
||||
}
|
||||
|
||||
// The explicit rungs of the ladder must still win — a fix to the derived path must not swallow
|
||||
// a host-supplied answer, which is the regression a narrow fix would introduce.
|
||||
const LADDER = [
|
||||
['absolute assetsBase wins', { assetsBase: 'https://cdn.example.org/a', treeBase: 'https://x.test/t' },
|
||||
'https://cdn.example.org/a'],
|
||||
['root-relative assetsBase wins', { assetsBase: '/shared/assets', treeBase: 'https://x.test/t' },
|
||||
'/shared/assets'],
|
||||
['trailing slash stripped', { assetsBase: 'https://cdn.example.org/a/' },
|
||||
'https://cdn.example.org/a'],
|
||||
['no base at all falls back', {}, './assets'],
|
||||
]
|
||||
for (const [name, cfg, want] of LADDER) {
|
||||
const got = resolveAssetsBase(cfg)
|
||||
const ok = got === want
|
||||
console.log(` ${ok ? '✓' : '✗'} ${name.padEnd(30)} ${got}`)
|
||||
if (!ok) fails.push(`${name}: returned ${got}, expected ${want}`)
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nEMBED-BASE: FAIL — ${fails.length} resolution(s) wrong.`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
console.error('\n This resolver is called by 10+ profiles across 6 bundles, and a wrong answer')
|
||||
console.error(' does not fail loudly — it fetches a 404 and the map paints a basemap with none')
|
||||
console.error(' of our data on it. It is also INVISIBLE LOCALLY: the dev web-root has assets')
|
||||
console.error(' both inside and beside the tree, so a wrong path still resolves on 127.0.0.1.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`\nEMBED-BASE: PASS — ${TREES.length} tree root(s) resolve to the same place a leaf's own `
|
||||
+ `'../../assets' means, and all ${LADDER.length} ladder rung(s) behave.`)
|
||||
|
|
@ -124,17 +124,35 @@ export function resolveAssetsBase(profileConfig) {
|
|||
if (/^https?:\/\//i.test(v) || v.startsWith('/')) return v
|
||||
}
|
||||
|
||||
// The tree root is <root>/<bundle>/<bundle>.js, and the shared pool is a SIBLING of the tree —
|
||||
// <root>/../assets — which is why the leaf page's generated value is '../../assets' (two up from
|
||||
// <tree>/<bundle>/<profile>/). Resolve it to an absolute URL so no host can re-interpret it.
|
||||
// The tree root is <root>/<bundle>/<bundle>.js, and the asset pool lives INSIDE the tree at
|
||||
// <root>/assets — generate-tree.mjs stages it there precisely so a dropped folder is
|
||||
// self-contained ("staged N runtime asset(s) -> doxa-maps/assets").
|
||||
//
|
||||
// ⚠ THIS SAID SIBLING AND COMPUTED <root>/../assets, WHICH IS ONE LEVEL TOO HIGH. The comment
|
||||
// justified it as "why the leaf page's generated value is '../../assets' (two up from
|
||||
// <tree>/<bundle>/<profile>/)" — but two up from <tree>/<bundle>/<profile>/ is <tree>, not the
|
||||
// tree's PARENT. The arithmetic was off by one and the prose made it look deliberate.
|
||||
//
|
||||
// MEASURED 2026-09-25 by publishing to the live catalog and reverting:
|
||||
// leaf .../doxa-maps/my-upg-100-list/upg-100-list/index.html + '../../assets'
|
||||
// -> /plugin-assets/doxa-maps/assets 200 (what the leaf means)
|
||||
// code root '/plugin-assets/doxa-maps' + '/../assets'
|
||||
// -> /plugin-assets/assets 404 (what this returned)
|
||||
// The UPG-100 map went from "195 polys · 27/27 zones · 2107 pins" to
|
||||
// "⚠ boundary fetch 404 world-adm0.geojson" — a basemap with none of our data on it.
|
||||
//
|
||||
// ⚠ AND IT IS INVISIBLE LOCALLY, which is why every gate stayed green. The dev web-root has BOTH
|
||||
// public/assets (a sibling) and public/doxa-maps/assets (inside), each holding the boundaries, so
|
||||
// the sibling path resolves on 127.0.0.1. The catalog publishes app/doxa-maps ONLY — there is no
|
||||
// sibling to find. A local run cannot distinguish the two answers; only the live prefix can.
|
||||
const root = stripSlashes(profileConfig?.treeBase || '') || stripSlashes(bundleTreeRoot() || '')
|
||||
if (root) {
|
||||
try {
|
||||
const base = typeof document !== 'undefined' ? document.baseURI : undefined
|
||||
return stripSlashes(new URL(root + '/../assets', base).href)
|
||||
return stripSlashes(new URL(root + '/assets', base).href)
|
||||
} catch {
|
||||
// A non-URL treeBase (a bare path) still yields a usable string by plain joining.
|
||||
return stripSlashes(root.replace(/\/[^/]*$/, '') + '/assets')
|
||||
return stripSlashes(root + '/assets')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,8 @@
|
|||
"check:published": "node internal/check-published-matches-build.mjs # what readers are SERVED must be what we BUILT. Every other gate measures the local tree, so all of them stay green while a stale publish serves something else — and manufactures bug reports that cannot be reproduced. Compares the sha384 per bundle. SKIPs when the published host is unreachable (a network gate that goes red on flaky wifi gets ignored).",
|
||||
"check:shared-reached": "node internal/check-shared-component-reached-bundles.mjs # a shared library fix is only as deployed as the last rebuild of EVERY bundle that consumes it. Each bundle ships its own COMPILED copy, so a one-line fix in library/ is live in whichever bundles happened to be rebuilt after it and silently absent from the rest — and every other gate measures ONE leaf, which renders the old behaviour perfectly. Asserts a distinctive marker string per tracked change; consumers are DERIVED from imports, never hardcoded.",
|
||||
"check:tree-weight": "node internal/check-published-tree-weight.mjs # the tree we publish has a weight and nothing was watching it. A publish was ALREADY BLOCKED once at a 74MB asset tree (Forgejo rejected the pack); it has since grown to 132MB unnoticed. Reports the SPLIT, because a bare total invites deleting the wrong half: the 176 city posters are the heavy half AND the half that cannot be regenerated (they need OSM tiles). Raw bytes, not packed — see publish-html.mjs for why a raw saving is not a shipped one.",
|
||||
"check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs # a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings."
|
||||
"check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs # a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings.",
|
||||
"check:embed-base": "node internal/check-embed-base-resolution.mjs # the shared URL resolver, pinned by ARITHMETIC — no browser, no network, no served tree, so it cannot SKIP itself into irrelevance. It computes what a leaf's own \"../../assets\" means and asserts the resolver agrees. Caught nothing for fifteen ticks because it did not exist: resolveAssetsBase computed one segment too high, every local gate passed (the dev web-root has assets BOTH inside and beside the tree), and the byte-level publish gates went green on the broken publish."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -681,7 +681,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1",
|
||||
"integrity": "sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -701,7 +701,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -715,7 +715,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -729,7 +729,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-AMIwL3sB+dFKS1OkVcGIX1lDbK1ZB5RyvjfhVNexuEe12HIjB1GToYbTaL2ZF5x1" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-U/2QjOxuZgh3qDjndls1kpI2DfYMCsQODBSgXYhWMFrt6kACquiu6+ZXja0Okr0J" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue