Update from source ccc2b1bb
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

This commit is contained in:
Doxa Maps 2026-09-26 08:35:22 +00:00
parent 8757b31228
commit 92abbb468b
50 changed files with 6281 additions and 395 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,14 @@
{
"file": "gb-IND-ADM1-full.geojson",
"units": 36,
"source": "geoBoundaries gbOpen (www.geoboundaries.org)",
"boundaryID": "IND-ADM1-1811400",
"boundarySource": "DataMeet India community, Election Commission of India",
"licence": "Creative Commons Attribution 2.5 India (CC BY 2.5 IN)",
"licenceSource": "github.com/datameet/maps",
"yearRepresented": "2011",
"sourceUpdated": "Thu Jan 19 07:31:04 2023",
"fetchedFrom": "https://github.com/wmgeolab/geoBoundaries/raw/9469f09/releaseData/gbOpen/IND/ADM1/geoBoundaries-IND-ADM1_simplified.geojson",
"processing": "coordinates rounded to 3 decimals, consecutive duplicate points dropped (library/scripts/boundaries/source-full-adm1.mjs)",
"note": "Complete ADM1. The curated gb-IND-ADM1.geojson (the 100-list partition, listId-tagged) is a different file and is not replaced."
}

23
data/internet/README.md Normal file
View file

@ -0,0 +1,23 @@
# data/internet/ — internet use below the country level
Source files for `data/tables/internet-by-adm1.json`, which
`plugin-designs/cities-internet/scripts/build-adm1-internet.mjs` writes. Edit the sources here and
re-run the script; never edit the table by hand.
## nfhs5-internet-by-state.csv
- **What:** India, National Family Health Survey 5 (NFHS-5, 2019–21, IIPS / Ministry of Health and
Family Welfare), indicators 18 and 19: *Women / Men (age 15–49) who have ever used the internet (%)*,
by state/UT, urban / rural / total (plus the NFHS-4 total).
- **From:** the two internet rows of `NFHS-5-States.csv` in https://github.com/pratapvardhan/NFHS-5 at
commit `93c67fed2403c8e533d178d293c5ba0411892b1e`, a CSV transcription of the official state fact sheets
(http://rchiips.org/nfhs/). The transcriber notes that quality checks were minimal; the figures
here were spot-checked against the national fact sheet (India: women 33.3%, men 57.1%).
- **Licence:** the CSV is **CC BY 4.0**. Attribution: "NFHS-5 (IIPS/MoHFW) state fact sheets, via
pratapvardhan/NFHS-5 (CC BY 4.0)".
- **Measure:** a SURVEY SHARE OF PEOPLE (0–100%): has this person ever used the internet. It is
not a subscription count (TRAI's "subscribers per 100" is a different measure and can exceed 100).
## adm1-crosswalk-IND.json
Source state names → the `shapeName` of `data/boundaries/gb-IND-ADM1-full.geojson` (all 36 states,
served as `assets/boundaries/gb-IND-ADM1-full.geojson`; source and licence in its `.meta.json`). Every source name must appear here, with either
the boundary name or `null` plus a reason. The build fails on anything else, never guesses.

View file

@ -0,0 +1,118 @@
{
"_about": "NFHS-5 state name -> gb-IND-ADM1-full.geojson shapeName. null = no geometry to draw it on, with the reason. Checked by build-adm1-internet.mjs.",
"map": {
"Andaman & Nicobar Islands": {
"adm1": "Andaman and Nicobar Islands"
},
"Andhra Pradesh": {
"adm1": "Andhra Pradesh"
},
"Arunachal Pradesh": {
"adm1": "Arunāchal Pradesh"
},
"Assam": {
"adm1": "Assam"
},
"Bihar": {
"adm1": "Bihār"
},
"Chandigarh": {
"adm1": "Chandīgarh"
},
"Chhattisgarh": {
"adm1": "Chhattīsgarh"
},
"Dadra & Nagar Haveli and Daman & Diu": {
"adm1": "Dādra and Nagar Haveli and Damān and Diu"
},
"Goa": {
"adm1": "Goa"
},
"Gujarat": {
"adm1": "Gujarāt"
},
"Haryana": {
"adm1": "Haryāna"
},
"Himachal Pradesh": {
"adm1": "Himāchal Pradesh"
},
"India": {
"adm1": null,
"national": true,
"reason": "the national row, kept as the national reference"
},
"Jammu & Kashmir": {
"adm1": "Jammu and Kashmīr"
},
"Jharkhand": {
"adm1": "Jhārkhand"
},
"Karnataka": {
"adm1": "Karnātaka"
},
"Kerala": {
"adm1": "Kerala"
},
"Ladakh": {
"adm1": "Ladākh"
},
"Lakshadweep": {
"adm1": "Lakshadweep"
},
"Madhya Pradesh": {
"adm1": "Madhya Pradesh"
},
"Maharashtra": {
"adm1": "Mahārāshtra"
},
"Manipur": {
"adm1": "Manipur"
},
"Meghalaya": {
"adm1": "Meghālaya"
},
"Mizoram": {
"adm1": "Mizoram"
},
"NCT Delhi": {
"adm1": "Delhi"
},
"Nagaland": {
"adm1": "Nāgāland"
},
"Odisha": {
"adm1": "Odisha"
},
"Puducherry": {
"adm1": "Puducherry"
},
"Punjab": {
"adm1": "Punjab"
},
"Rajasthan": {
"adm1": "Rājasthān"
},
"Sikkim": {
"adm1": "Sikkim"
},
"Tamil Nadu": {
"adm1": "Tamil Nādu"
},
"Telangana": {
"adm1": "Telangāna"
},
"Tripura": {
"adm1": "Tripura"
},
"Uttar Pradesh": {
"adm1": "Uttar Pradesh"
},
"Uttarakhand": {
"adm1": "Uttarākhand"
},
"West Bengal": {
"adm1": "West Bengal"
}
}
}

View file

@ -0,0 +1,75 @@
state,state_code,indicator,nfhs5_urban,nfhs5_rural,nfhs5_total,nfhs4_total
India,,18. Women who have ever used the internet (%),51.8,24.6,33.3,
India,,19. Men who have ever used the internet (%),72.5,48.7,57.1,
Andaman & Nicobar Islands,AN,18. Women who have ever used the internet (%),44.1,27.9,34.8,
Andaman & Nicobar Islands,AN,19. Men who have ever used the internet (%),54.6,41.1,46.5,
Andhra Pradesh,AP,18. Women who have ever used the internet (%),33.9,15.4,21.0,
Andhra Pradesh,AP,19. Men who have ever used the internet (%),65.1,41.5,48.8,
Arunachal Pradesh,AR,18. Women who have ever used the internet (%),70.0,49.6,52.9,
Arunachal Pradesh,AR,19. Men who have ever used the internet (%),86.9,68.5,71.6,
Assam,AS,18. Women who have ever used the internet (%),49.0,24.4,28.2,
Assam,AS,19. Men who have ever used the internet (%),67.4,37.8,42.3,
Bihar,BR,18. Women who have ever used the internet (%),38.4,17.0,20.6,
Bihar,BR,19. Men who have ever used the internet (%),58.4,39.4,43.6,
Chandigarh,CH,18. Women who have ever used the internet (%),75.2,,75.2,
Chandigarh,CH,19. Men who have ever used the internet (%),91.9,,91.9,
Chhattisgarh,CT,18. Women who have ever used the internet (%),44.5,20.8,26.7,
Chhattisgarh,CT,19. Men who have ever used the internet (%),75.4,50.4,56.3,
Dadra & Nagar Haveli and Daman & Diu,DD,18. Women who have ever used the internet (%),49.4,23.8,36.7,
Dadra & Nagar Haveli and Daman & Diu,DD,19. Men who have ever used the internet (%),76.2,61.3,68.3,
NCT Delhi,DL,18. Women who have ever used the internet (%),63.7,69.2,63.8,
NCT Delhi,DL,19. Men who have ever used the internet (%),85.1,87.4,85.2,
Goa,GA,18. Women who have ever used the internet (%),78.1,68.3,73.7,
Goa,GA,19. Men who have ever used the internet (%),86.1,76.6,82.9,
Gujarat,GJ,18. Women who have ever used the internet (%),48.9,17.5,30.8,
Gujarat,GJ,19. Men who have ever used the internet (%),72.9,48.0,58.9,
Himachal Pradesh,HP,18. Women who have ever used the internet (%),78.9,45.2,49.7,
Himachal Pradesh,HP,19. Men who have ever used the internet (%),83.7,65.1,67.9,
Haryana,HR,18. Women who have ever used the internet (%),60.2,42.8,48.4,
Haryana,HR,19. Men who have ever used the internet (%),79.7,68.8,72.4,
Jharkhand,JH,18. Women who have ever used the internet (%),57.8,22.7,31.4,
Jharkhand,JH,19. Men who have ever used the internet (%),70.8,53.2,58.0,
Jammu & Kashmir,JK,18. Women who have ever used the internet (%),55.0,38.9,43.3,
Jammu & Kashmir,JK,19. Men who have ever used the internet (%),79.4,68.8,72.0,
Karnataka,KA,18. Women who have ever used the internet (%),50.1,24.8,35.0,
Karnataka,KA,19. Men who have ever used the internet (%),71.5,55.6,62.4,
Kerala,KL,18. Women who have ever used the internet (%),64.9,57.5,61.1,
Kerala,KL,19. Men who have ever used the internet (%),78.3,74.2,76.1,
Lakshadweep,LD,18. Women who have ever used the internet (%),61.8,36.0,56.4,
Lakshadweep,LD,19. Men who have ever used the internet (%),81.5,77.0,80.3,
Ladakh,LH,18. Women who have ever used the internet (%),66.5,54.0,56.4,
Ladakh,LH,19. Men who have ever used the internet (%),82.9,64.3,67.8,
Maharashtra,MH,18. Women who have ever used the internet (%),54.3,23.7,38.0,
Maharashtra,MH,19. Men who have ever used the internet (%),76.8,47.2,61.5,
Meghalaya,ML,18. Women who have ever used the internet (%),57.8,28.0,34.7,
Meghalaya,ML,19. Men who have ever used the internet (%),59.2,38.5,42.1,
Manipur,MN,18. Women who have ever used the internet (%),50.8,40.4,44.8,
Manipur,MN,19. Men who have ever used the internet (%),81.5,68.2,73.9,
Madhya Pradesh,MP,18. Women who have ever used the internet (%),46.5,20.1,26.9,
Madhya Pradesh,MP,19. Men who have ever used the internet (%),72.7,49.3,55.7,
Mizoram,MZ,18. Women who have ever used the internet (%),83.8,48.0,67.6,
Mizoram,MZ,19. Men who have ever used the internet (%),92.7,63.9,79.7,
Nagaland,NL,18. Women who have ever used the internet (%),66.5,40.3,49.9,
Nagaland,NL,19. Men who have ever used the internet (%),81.0,55.2,64.6,
Odisha,OR,18. Women who have ever used the internet (%),39.7,21.3,24.9,
Odisha,OR,19. Men who have ever used the internet (%),64.2,47.2,50.7,
Punjab,PB,18. Women who have ever used the internet (%),64.1,48.8,54.8,
Punjab,PB,19. Men who have ever used the internet (%),85.6,73.0,78.2,
Puducherry,PY,18. Women who have ever used the internet (%),66.9,50.4,61.9,
Puducherry,PY,19. Men who have ever used the internet (%),85.1,69.4,80.7,
Rajasthan,RJ,18. Women who have ever used the internet (%),56.1,30.8,36.9,
Rajasthan,RJ,19. Men who have ever used the internet (%),81.7,59.4,65.2,
Sikkim,SK,18. Women who have ever used the internet (%),90.0,68.1,76.7,
Sikkim,SK,19. Men who have ever used the internet (%),94.2,69.5,78.2,
Telangana,TG,18. Women who have ever used the internet (%),43.9,15.8,26.5,
Telangana,TG,19. Men who have ever used the internet (%),72.3,46.7,57.4,
Tamil Nadu,TN,18. Women who have ever used the internet (%),55.8,39.2,46.9,
Tamil Nadu,TN,19. Men who have ever used the internet (%),76.1,64.9,70.2,
Tripura,TR,18. Women who have ever used the internet (%),36.6,17.7,22.9,
Tripura,TR,19. Men who have ever used the internet (%),47.0,45.2,45.7,
Uttar Pradesh,UP,18. Women who have ever used the internet (%),50.2,24.5,30.6,
Uttar Pradesh,UP,19. Men who have ever used the internet (%),72.4,54.2,59.1,
Uttarakhand,UT,18. Women who have ever used the internet (%),58.4,39.4,45.1,
Uttarakhand,UT,19. Men who have ever used the internet (%),82.1,71.2,74.6,
West Bengal,WB,18. Women who have ever used the internet (%),48.1,14.0,25.5,
West Bengal,WB,19. Men who have ever used the internet (%),64.6,38.3,46.7,
1 state state_code indicator nfhs5_urban nfhs5_rural nfhs5_total nfhs4_total
2 India 18. Women who have ever used the internet (%) 51.8 24.6 33.3
3 India 19. Men who have ever used the internet (%) 72.5 48.7 57.1
4 Andaman & Nicobar Islands AN 18. Women who have ever used the internet (%) 44.1 27.9 34.8
5 Andaman & Nicobar Islands AN 19. Men who have ever used the internet (%) 54.6 41.1 46.5
6 Andhra Pradesh AP 18. Women who have ever used the internet (%) 33.9 15.4 21.0
7 Andhra Pradesh AP 19. Men who have ever used the internet (%) 65.1 41.5 48.8
8 Arunachal Pradesh AR 18. Women who have ever used the internet (%) 70.0 49.6 52.9
9 Arunachal Pradesh AR 19. Men who have ever used the internet (%) 86.9 68.5 71.6
10 Assam AS 18. Women who have ever used the internet (%) 49.0 24.4 28.2
11 Assam AS 19. Men who have ever used the internet (%) 67.4 37.8 42.3
12 Bihar BR 18. Women who have ever used the internet (%) 38.4 17.0 20.6
13 Bihar BR 19. Men who have ever used the internet (%) 58.4 39.4 43.6
14 Chandigarh CH 18. Women who have ever used the internet (%) 75.2 75.2
15 Chandigarh CH 19. Men who have ever used the internet (%) 91.9 91.9
16 Chhattisgarh CT 18. Women who have ever used the internet (%) 44.5 20.8 26.7
17 Chhattisgarh CT 19. Men who have ever used the internet (%) 75.4 50.4 56.3
18 Dadra & Nagar Haveli and Daman & Diu DD 18. Women who have ever used the internet (%) 49.4 23.8 36.7
19 Dadra & Nagar Haveli and Daman & Diu DD 19. Men who have ever used the internet (%) 76.2 61.3 68.3
20 NCT Delhi DL 18. Women who have ever used the internet (%) 63.7 69.2 63.8
21 NCT Delhi DL 19. Men who have ever used the internet (%) 85.1 87.4 85.2
22 Goa GA 18. Women who have ever used the internet (%) 78.1 68.3 73.7
23 Goa GA 19. Men who have ever used the internet (%) 86.1 76.6 82.9
24 Gujarat GJ 18. Women who have ever used the internet (%) 48.9 17.5 30.8
25 Gujarat GJ 19. Men who have ever used the internet (%) 72.9 48.0 58.9
26 Himachal Pradesh HP 18. Women who have ever used the internet (%) 78.9 45.2 49.7
27 Himachal Pradesh HP 19. Men who have ever used the internet (%) 83.7 65.1 67.9
28 Haryana HR 18. Women who have ever used the internet (%) 60.2 42.8 48.4
29 Haryana HR 19. Men who have ever used the internet (%) 79.7 68.8 72.4
30 Jharkhand JH 18. Women who have ever used the internet (%) 57.8 22.7 31.4
31 Jharkhand JH 19. Men who have ever used the internet (%) 70.8 53.2 58.0
32 Jammu & Kashmir JK 18. Women who have ever used the internet (%) 55.0 38.9 43.3
33 Jammu & Kashmir JK 19. Men who have ever used the internet (%) 79.4 68.8 72.0
34 Karnataka KA 18. Women who have ever used the internet (%) 50.1 24.8 35.0
35 Karnataka KA 19. Men who have ever used the internet (%) 71.5 55.6 62.4
36 Kerala KL 18. Women who have ever used the internet (%) 64.9 57.5 61.1
37 Kerala KL 19. Men who have ever used the internet (%) 78.3 74.2 76.1
38 Lakshadweep LD 18. Women who have ever used the internet (%) 61.8 36.0 56.4
39 Lakshadweep LD 19. Men who have ever used the internet (%) 81.5 77.0 80.3
40 Ladakh LH 18. Women who have ever used the internet (%) 66.5 54.0 56.4
41 Ladakh LH 19. Men who have ever used the internet (%) 82.9 64.3 67.8
42 Maharashtra MH 18. Women who have ever used the internet (%) 54.3 23.7 38.0
43 Maharashtra MH 19. Men who have ever used the internet (%) 76.8 47.2 61.5
44 Meghalaya ML 18. Women who have ever used the internet (%) 57.8 28.0 34.7
45 Meghalaya ML 19. Men who have ever used the internet (%) 59.2 38.5 42.1
46 Manipur MN 18. Women who have ever used the internet (%) 50.8 40.4 44.8
47 Manipur MN 19. Men who have ever used the internet (%) 81.5 68.2 73.9
48 Madhya Pradesh MP 18. Women who have ever used the internet (%) 46.5 20.1 26.9
49 Madhya Pradesh MP 19. Men who have ever used the internet (%) 72.7 49.3 55.7
50 Mizoram MZ 18. Women who have ever used the internet (%) 83.8 48.0 67.6
51 Mizoram MZ 19. Men who have ever used the internet (%) 92.7 63.9 79.7
52 Nagaland NL 18. Women who have ever used the internet (%) 66.5 40.3 49.9
53 Nagaland NL 19. Men who have ever used the internet (%) 81.0 55.2 64.6
54 Odisha OR 18. Women who have ever used the internet (%) 39.7 21.3 24.9
55 Odisha OR 19. Men who have ever used the internet (%) 64.2 47.2 50.7
56 Punjab PB 18. Women who have ever used the internet (%) 64.1 48.8 54.8
57 Punjab PB 19. Men who have ever used the internet (%) 85.6 73.0 78.2
58 Puducherry PY 18. Women who have ever used the internet (%) 66.9 50.4 61.9
59 Puducherry PY 19. Men who have ever used the internet (%) 85.1 69.4 80.7
60 Rajasthan RJ 18. Women who have ever used the internet (%) 56.1 30.8 36.9
61 Rajasthan RJ 19. Men who have ever used the internet (%) 81.7 59.4 65.2
62 Sikkim SK 18. Women who have ever used the internet (%) 90.0 68.1 76.7
63 Sikkim SK 19. Men who have ever used the internet (%) 94.2 69.5 78.2
64 Telangana TG 18. Women who have ever used the internet (%) 43.9 15.8 26.5
65 Telangana TG 19. Men who have ever used the internet (%) 72.3 46.7 57.4
66 Tamil Nadu TN 18. Women who have ever used the internet (%) 55.8 39.2 46.9
67 Tamil Nadu TN 19. Men who have ever used the internet (%) 76.1 64.9 70.2
68 Tripura TR 18. Women who have ever used the internet (%) 36.6 17.7 22.9
69 Tripura TR 19. Men who have ever used the internet (%) 47.0 45.2 45.7
70 Uttar Pradesh UP 18. Women who have ever used the internet (%) 50.2 24.5 30.6
71 Uttar Pradesh UP 19. Men who have ever used the internet (%) 72.4 54.2 59.1
72 Uttarakhand UT 18. Women who have ever used the internet (%) 58.4 39.4 45.1
73 Uttarakhand UT 19. Men who have ever used the internet (%) 82.1 71.2 74.6
74 West Bengal WB 18. Women who have ever used the internet (%) 48.1 14.0 25.5
75 West Bengal WB 19. Men who have ever used the internet (%) 64.6 38.3 46.7

File diff suppressed because it is too large Load diff

View file

@ -32,7 +32,23 @@ import { resolve, dirname } from 'node:path'
import { pathToFileURL, fileURLToPath } from 'node:url'
import { requirePathInThisCheckout } from './_serves-this-tree.mjs'
const target = process.argv[2] || process.env.PACKET_HTML
// ★ --expect-funnel TURNS AN OPTIONAL PAGE INTO A DECLARED REQUIREMENT, and it exists because of a
// real near-miss (regions-plugin, 2026-09-26, reported unprompted). A merge resolved with
// `git checkout --ours` silently deleted the printPacket call site that supplies the funnel data —
// 60 lines — AND EVERY GATE STAYED GREEN. It had to: the funnel page is optional by design
// (buildPacket omits it when the arg is null), so a packet with no page 2 is not malformed, just
// smaller. My own design choice is what made the deletion invisible.
//
// ⚠ THE FIX IS NOT "ALWAYS REQUIRE IT". That would fail the 11 pre-funnel laos packets and any host
// that legitimately has no funnel data — a false red, which costs as much as a false green. Instead
// the CALLER, who knows whether this packet was built with funnel data, says so. An absence is then
// either DECLARED (no flag, fine) or a FAILURE (flag passed, page missing) — never a silent nothing.
// ⚠ Flags are filtered out of the positional read: a bare `--expect-funnel` was otherwise taken as
// the packet path, which is the flag-eaten-as-positional trap this repo has hit three times.
const EXPECT_FUNNEL = process.argv.includes('--expect-funnel')
|| process.env.PACKET_EXPECT_FUNNEL === '1'
const positional = process.argv.slice(2).filter((a) => !a.startsWith('--'))
const target = positional[0] || process.env.PACKET_HTML
if (!target) {
console.log('PACKET-RENDER: SKIP — no packet given (pass a path or set PACKET_HTML).')
console.log(' The packet is generated on demand, so this gate is opt-in rather than')
@ -220,6 +236,29 @@ const r = await page.evaluate(() => {
// exact about list pages whether or not this page is there.
funnel: document.querySelectorAll('.funnel-title').length,
funnelSteps: document.querySelectorAll('.funnel-mine').length,
// ★ MEASURED SO THE GATE CAN REPORT WHAT IT ASSERTED. rega: "the gate prints no funnel line, even
// with --expect-funnel ... a reader cannot see WHAT it asserted about page 2." A silent pass is
// the mirror of a printed-but-unasserted number: the check is real and the evidence is invisible.
// ⚠ ZERO POPULATION IS COUNTED ON PURPOSE. The reader's row printed "0 people" in every packet
// (Number(null) is 0, so the finite-number guard admitted a deliberate no-data marker). Fixed in
// the renderer — and counted here so the fix cannot silently regress.
funnelRows: (() => {
const sheet = document.querySelector('.funnel-title')?.closest('.sheet')
return sheet ? sheet.querySelectorAll('div[style*="line-height:1"]').length : 0
})(),
funnelBars: (() => {
const sheet = document.querySelector('.funnel-title')?.closest('.sheet')
return sheet ? sheet.querySelectorAll('div[style*="height:16px"]').length : 0
})(),
funnelZeroPop: (() => {
const sheet = document.querySelector('.funnel-title')?.closest('.sheet')
if (!sheet) return 0
return [...sheet.querySelectorAll('div')]
.filter((d) => !d.children.length && /(^|\D)0(\D|$)/.test(d.textContent.trim())
&& d.textContent.trim().length < 24 && /\d/.test(d.textContent))
.filter((d) => /^0(\.0+)?\s+\D/.test(d.textContent.trim())).length
})(),
funnelShare: (document.querySelector('.funnel-share')?.textContent || '').trim().slice(0, 80),
// ★ A CARD MUST NAME ITS PEOPLE GROUP. Counting .card proves the element exists; the spec says
// pages 2..n carry "the people groups, 20 per page, with photos" — a card with a photo and a
// BLANK NAME satisfies every count here: the total holds, the photo/typographic split holds,
@ -591,6 +630,29 @@ if (r.funnel && !r.funnelSteps) {
fails.push('the funnel page rendered its title but no share figure (.funnel-mine) — '
+ 'page 2 is asserting a narrowing it never states')
}
// ★ AND IF THE CALLER DECLARED IT, ITS ABSENCE IS A FAILURE. See the --expect-funnel note at the top:
// a lost call site produces a packet that is CORRECT but missing page 2, which is why nothing caught
// a merge deleting one.
// ★ EVERY FUNNEL ROW MUST HAVE A BAR. rega measured the first version: the reader's step had no bar,
// so the funnel never visibly narrowed to "yours" — a funnel whose final term is not drawn as part of
// the funnel fails at the one thing it is for. Rows and bars are counted separately so the next
// regression is a mismatch rather than an invisible omission.
if (r.funnel && r.funnelRows !== r.funnelBars) {
fails.push(`the funnel page has ${r.funnelRows} step row(s) but ${r.funnelBars} bar(s) — `
+ 'every step must be drawn as part of the funnel, including the reader\'s own')
}
// ★ AND NO STEP MAY PRINT A ZERO POPULATION. "0 people" appeared on the reader's row of every packet
// because Number(null) is 0 and the renderer's guard tested finiteness rather than PRESENCE. A people
// group with no people is not a fact; it is missing data wearing a number's clothes.
if (r.funnel && r.funnelZeroPop) {
fails.push(`${r.funnelZeroPop} funnel row(s) print a ZERO population — absent data must be hidden, `
+ 'not rendered as the number 0 (Number(null) === 0, so test presence, not finiteness)')
}
if (EXPECT_FUNNEL && !r.funnel) {
fails.push('--expect-funnel was passed but this packet has NO funnel page (.funnel-title) — '
+ 'the caller that supplies funnel data to buildPacket is missing or passing null, '
+ 'and a packet without page 2 is otherwise indistinguishable from a correct one')
}
// ── ...AND THAT ALONE IS NOT ENOUGH: IT ONLY PROVES SELF-CONSISTENCY ────────
// ⚠ MUTATION-TESTED, AND THE FIRST VERSION OF THIS GATE FAILED THE TEST. Deleting a
@ -646,6 +708,9 @@ console.log(` sheets/PDF pages : ${r.sheets} / ${pages}`)
console.log(` printed artefact : ${(pdfBytes / 1024 / 1024).toFixed(1)}MB, ${pdfFonts} embedded font(s), `
+ `${pdfImages} image(s) for ${r.distinctImgSrc} distinct source(s)`)
console.log(` people cards : ${r.cards} (${r.imgs} photos, ${r.nophoto} typographic)`)
console.log(` funnel (page 2) : ${r.funnel
? `${r.funnelRows} row(s), ${r.funnelBars} bar(s)${r.funnelShare ? ` — "${r.funnelShare}"` : ' — no share line'}`
: 'absent' + (EXPECT_FUNNEL ? ' (REQUIRED — see FAIL above)' : ' (not required)')}`)
console.log(` prayer points : ${r.points} / ${POINTS_EXPECT}`)
console.log(` wheel segments : ${r.wheelSegs} (${r.wheelLabels}/${WHEEL_LABELS} labelled)`)
console.log(` Mark 5 in full : ${r.mark5Len} chars`

View file

@ -43,9 +43,32 @@ if (!files.length) {
}
const gate = join(HERE, 'check-packet-render.mjs')
// ★ THE FUNNEL PAGE IS REQUIRED HERE, and this line is the half of the fix that was missing.
// check-packet-render gained --expect-funnel after a merge deleted the call site supplying the funnel
// data and every gate stayed green (regions-plugin, 2026-09-26). But a DECLARABLE expectation that
// nothing declares is not a closed hole — it is an invisible defect traded for a visible obligation.
// I said as much to regions and then owed the second half.
//
// ⚠ WHY IT CAN BE UNCONDITIONAL HERE. Every packet in out/ is produced by
// library/scripts/posters/packet.mjs, which always passes FUNNEL to buildPacket. So a packet in this
// directory WITHOUT page 2 was either generated before the funnel existed or generated by a caller
// that dropped the argument — and both are things this sweep should say out loud. Verified
// 2026-09-26 across all 16 packets in out/ (4 fixtures, the ar tibet, and all 11 laos locales
// regenerated): every one has page 2, so this requires a property the pipeline actually holds rather
// than one it aspires to.
// ⚠ ESCAPE HATCH, deliberately awkward to reach: PACKETS_ALL_NO_FUNNEL=1. If a legitimate host ever
// generates funnel-less packets, that is a real state and this flag records it — but it must be a
// decision someone takes, not a default that quietly tolerates a missing page.
const EXPECT_FUNNEL = process.env.PACKETS_ALL_NO_FUNNEL !== '1'
if (!EXPECT_FUNNEL) {
console.log('PACKETS-ALL: ⚠ PACKETS_ALL_NO_FUNNEL=1 — page 2 is NOT required this run.')
}
const failed = []
for (const f of files) {
const res = spawnSync(process.execPath, [gate, join(DIR, f)], { encoding: 'utf8' })
const args = EXPECT_FUNNEL ? [gate, join(DIR, f), '--expect-funnel'] : [gate, join(DIR, f)]
const res = spawnSync(process.execPath, args, { encoding: 'utf8' })
const ok = res.status === 0
console.log(` ${ok ? '✓' : '✗'} ${f}`)
if (!ok) {

View file

@ -1515,6 +1515,10 @@ function main() {
[join(dataDir, 'adm0', 'world-adm0.geojson'), 'world-adm0.geojson'],
[join(dataDir, 'gb-IND-ADM1.geojson'), 'gb-IND-ADM1.geojson'],
[join(dataDir, 'gb-CHN-ADM1.geojson'), 'gb-CHN-ADM1.geojson'],
// COMPLETE India ADM1 (36 units, geoBoundaries gbOpen, CC BY 2.5 IN; source + licence in its meta),
// for maps that colour every state. The curated file above is UPG-100's 34-unit partition.
[resolve(__dirname, '..', 'data', 'boundaries', 'gb-IND-ADM1-full.geojson'), 'gb-IND-ADM1-full.geojson'],
[resolve(__dirname, '..', 'data', 'boundaries', 'gb-IND-ADM1-full.meta.json'), 'gb-IND-ADM1-full.meta.json'],
]) if (existsSync(from)) cpSync(from, join(bDir, to))
console.log('[generate-tree] gathered boundary geojson')
@ -1573,6 +1577,8 @@ function main() {
'boundaries/world-adm0.geojson',
'boundaries/gb-IND-ADM1.geojson',
'boundaries/gb-CHN-ADM1.geojson',
'boundaries/gb-IND-ADM1-full.geojson', // all 36 states (Cities with Internet's NFHS layer)
'boundaries/gb-IND-ADM1-full.meta.json', // its source and licence, served beside it
// people-groups — all-people-groups.vue (the slim JSON only; NOT the 16 MB CSV,
// which no element fetches and which dropin.exclude already withholds).
'people-groups/people-groups-slim.json',

View file

@ -54,5 +54,15 @@
"messaging": "تطبيقات المراسلة"
},
"loading": "جارٍ تحميل {name}…"
},
"adm1": {
"title": "الهند حسب الولاية: سبق لهم استخدام الإنترنت",
"women": "النساء",
"men": "الرجال",
"total": "الإجمالي",
"urban": "الحضر",
"rural": "الريف",
"source": "المسح NFHS-5 (2019–2021)، الأشخاص من 15 إلى 49 عامًا: نسبة من سبق لهم استخدام الإنترنت. مسح للأشخاص، لا عدد للاشتراكات.",
"missing": "لا توجد بعد حدود ولاية لـ {names}: أرقامها موجودة في البيانات، لا على الخريطة."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Messenger"
},
"loading": "{name} wird geladen…"
},
"adm1": {
"title": "Indien nach Bundesstaat: haben das Internet schon genutzt",
"women": "Frauen",
"men": "Männer",
"total": "Gesamt",
"urban": "Stadt",
"rural": "Land",
"source": "NFHS-5 (2019–21), Personen im Alter von 15–49 Jahren: Anteil derer, die das Internet schon einmal genutzt haben. Eine Befragung von Menschen, keine Zählung von Anschlüssen.",
"missing": "Noch keine Staatsgrenze für {names}: Ihre Werte stehen in den Daten, nicht auf der Karte."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Messaging Apps"
},
"loading": "Loading {name}…"
},
"adm1": {
"title": "India by state: ever used the internet",
"women": "Women",
"men": "Men",
"total": "Total",
"urban": "Urban",
"rural": "Rural",
"source": "NFHS-5 (2019–21), people aged 15–49: the share who have ever used the internet. A survey of people, not a count of subscriptions.",
"missing": "No state boundary yet for {names}: their figures are in the data, not on the map."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Mensajería"
},
"loading": "Cargando {name}…"
},
"adm1": {
"title": "India por estado: han usado internet alguna vez",
"women": "Mujeres",
"men": "Hombres",
"total": "Total",
"urban": "Urbano",
"rural": "Rural",
"source": "NFHS-5 (2019–21), personas de 15 a 49 años: proporción que ha usado internet alguna vez. Una encuesta a personas, no un recuento de suscripciones.",
"missing": "Aún no hay límite estatal para {names}: sus cifras están en los datos, no en el mapa."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Messageries"
},
"loading": "Chargement de {name}…"
},
"adm1": {
"title": "Inde par État : ont déjà utilisé Internet",
"women": "Femmes",
"men": "Hommes",
"total": "Total",
"urban": "Urbain",
"rural": "Rural",
"source": "NFHS-5 (2019–21), personnes de 15 à 49 ans : part de celles qui ont déjà utilisé Internet. Une enquête auprès des personnes, pas un décompte d’abonnements.",
"missing": "Pas encore de limite d’État pour {names} : leurs chiffres figurent dans les données, pas sur la carte."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "मैसेजिंग ऐप"
},
"loading": "{name} लोड हो रहा है…"
},
"adm1": {
"title": "भारत राज्य अनुसार: कभी इंटरनेट का उपयोग किया",
"women": "महिलाएँ",
"men": "पुरुष",
"total": "कुल",
"urban": "शहरी",
"rural": "ग्रामीण",
"source": "NFHS-5 (2019–21), 15–49 वर्ष के लोग: जिन्होंने कभी इंटरनेट का उपयोग किया है उनका प्रतिशत। यह लोगों का सर्वेक्षण है, सब्सक्रिप्शन की गिनती नहीं।",
"missing": "{names} की राज्य सीमा अभी उपलब्ध नहीं है: उनके आँकड़े डेटा में हैं, नक्शे पर नहीं।"
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Messaggistica"
},
"loading": "Caricamento di {name}…"
},
"adm1": {
"title": "India per Stato: hanno mai usato internet",
"women": "Donne",
"men": "Uomini",
"total": "Totale",
"urban": "Urbano",
"rural": "Rurale",
"source": "NFHS-5 (2019–21), persone di 15–49 anni: quota di chi ha mai usato internet. Un’indagine sulle persone, non un conteggio di abbonamenti.",
"missing": "Ancora nessun confine statale per {names}: i loro dati sono nel dataset, non sulla mappa."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Mensagens"
},
"loading": "A carregar {name}…"
},
"adm1": {
"title": "Índia por estado: já usaram a internet",
"women": "Mulheres",
"men": "Homens",
"total": "Total",
"urban": "Urbano",
"rural": "Rural",
"source": "NFHS-5 (2019–21), pessoas de 15 a 49 anos: proporção que já usou a internet. Uma pesquisa com pessoas, não uma contagem de assinaturas.",
"missing": "Ainda não há limite estadual para {names}: os números estão nos dados, não no mapa."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Aplicații de mesagerie"
},
"loading": "Se încarcă {name}…"
},
"adm1": {
"title": "India pe state: au folosit vreodată internetul",
"women": "Femei",
"men": "Bărbați",
"total": "Total",
"urban": "Urban",
"rural": "Rural",
"source": "NFHS-5 (2019–21), persoane de 15–49 de ani: ponderea celor care au folosit vreodată internetul. Un sondaj despre oameni, nu o numărătoare de abonamente.",
"missing": "Încă nu există limită de stat pentru {names}: cifrele lor sunt în date, nu pe hartă."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "Мессенджеры"
},
"loading": "Загрузка: {name}…"
},
"adm1": {
"title": "Индия по штатам: когда-либо пользовались интернетом",
"women": "Женщины",
"men": "Мужчины",
"total": "Всего",
"urban": "Город",
"rural": "Село",
"source": "NFHS-5 (2019–21), люди 15–49 лет: доля тех, кто когда-либо пользовался интернетом. Опрос людей, а не подсчёт подписок.",
"missing": "Для {names} ещё нет границы штата: их данные есть в таблице, но не на карте."
}
}

View file

@ -54,5 +54,15 @@
"messaging": "即时通讯"
},
"loading": "正在加载{name}…"
},
"adm1": {
"title": "印度各邦:曾经使用过互联网",
"women": "女性",
"men": "男性",
"total": "合计",
"urban": "城市",
"rural": "农村",
"source": "NFHS-5(2019–21),15–49 岁人群:曾经使用过互联网的比例。这是对人的调查,而不是订阅数统计。",
"missing": "{names} 尚无邦界:其数据在数据集中,但未显示在地图上。"
}
}

View file

@ -237,6 +237,17 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
// never claims that band; it states the share the data can defend. Shepherd approved exactly this
// (ENDTOKEN-DXS-UPG-KDECIDE), K=27 including the two residual buckets until the agent rules.
//
// ⚠ "NUMERALS UNTOUCHED" IS ABOUT DIGITS, NOT GROUPING, and those are two properties. I verified the
// digits and assumed the rest. rega measured the rendered hi sheet and found Indian grouping —
// "8,28,85,63,969 लोग" (lakh/crore, 2-2-3) rather than Western 3-3-3. That is CORRECT hi-IN
// formatting, so changing it would be worse than leaving it, and the real risk was never the
// grouping itself but INCONSISTENCY: the same number printed two ways in one document.
// Measured 2026-09-26, and it is consistent: num() is the ONLY formatter in this renderer, so the
// funnel's populations and the people-group cards' populations group identically. Confirmed against
// the rendered hi packet — card populations 1,70,000 / 3,37,000 / 8,10,000 are Indian-grouped like
// the funnel's. (Only values at or above 100,000 can distinguish the two systems at all; my first
// check classified "1,400" and read a mismatch that did not exist.)
//
// ⚠ NUMERALS STAY WESTERN. num() is toLocaleString(lang), and for the bare `ar`/`hi` tags CLDR's
// default numbering system is `latn` — verified: 12,379 and 2,107 in both. Arabic-Indic digits
// need an explicit -u-nu-arab, which nothing here asks for. The brief requires numerals untouched,
@ -250,25 +261,23 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
const BAR_W = W - 2 * M - 300;
const width = (n) => Math.max(26, Math.round(BAR_W * (Number(n) / Number(top || 1))));
const rows = steps.map((s, i) => {
const label = t(`funnel_${s.id}`) !== undefined ? t(`funnel_${s.id}`) : s.id;
const pop = Number.isFinite(Number(s.population))
? `<div style="font:400 15px Georgia,serif;color:#a89f8c;margin-top:1px">${num(s.population)} ${esc(t('funnel_people'))}</div>`
: '';
// The last IMB step is the network's own list — tint it so the eye lands where the reader is.
const fillCol = i === steps.length - 1 ? '#7a3b2e' : '#cdbf9e';
return `<div style="margin-bottom:22px">
<div style="font:700 34px Georgia,serif;color:#1b1a17;line-height:1">${num(s.groups)}</div>
<div style="font:400 18px Georgia,serif;color:#3b3226;margin-top:2px">${esc(label)}</div>
${pop}
<div style="height:12px;width:${width(s.groups)}px;background:${fillCol};margin-top:6px;border-radius:2px"></div>
</div>`;
}).join('');
// THIS list, the step the reader is holding. `groups.length` is the packet's own row count, so
// the page cannot disagree with the pages that follow it.
const mine = groups.length;
const total = Number(funnel.listTotal ?? (steps[steps.length - 1] || {}).groups) || 0;
// ⚠ THE DENOMINATOR MUST BE THE LIST THE PACKET IS ACTUALLY DRAWN FROM, and it is an EXPLICIT
// input for that reason. An earlier version fell back to the last funnel step
// (`funnel.listTotal ?? steps[last].groups`), which is 2,107 — DOXA's list. That is right today
// because every map shows DOXA, and it becomes WRONG the moment the universal `list` prop lands
// (exp-shepherd, library/lists/peopleGroupList.js): under ?list=frontier a Tier A map shows the
// DOXA groups that are ALSO frontier, which SPEC-PEOPLE-GROUP-LIST-PROP.md measures at 1,753. The
// page would have printed "N of the 2,107" with total confidence while the map showed a different
// set — a faithful proxy that silently changes meaning when a new path appears, which is the same
// family as inferring "did it work" from a field that only answers "did it isolate".
//
// So a caller that knows the active list says so. A caller that does not gets NO SHARE LINE rather
// than a plausible wrong one: `mine` is always trustworthy (it is this packet's own row count) but
// a percentage needs both terms, and half a fraction is not a fact.
const total = Number(funnel.listTotal) || 0;
const pct = total ? (100 * mine / total) : 0;
// ⚠ A SHARE MUST NEVER ROUND TO ZERO — zero reads as "this packet covers none of them", which is
// false for every list. Measured against the real catalogue (counts 1..118 of 2,107): one decimal
@ -279,30 +288,130 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
? String(Math.round(pct))
: pct.toFixed(pct >= 1 ? 1 : 2).replace(/\.?0+$/, '');
const share = funnel.listCount
? `<div class="funnel-share" style="font:700 22px Georgia,serif;color:#7a3b2e;margin-top:4px">
${esc(fill(t('funnel_share'), { k: num(funnel.listCount), pct: pctStr, total: num(total) }))}</div>`
: '';
const foot = (funnel.sources && funnel.sources.imb)
? esc(fill(t('funnel_foot'), {
imb_pub: funnel.sources.imb.publisher || '',
imb_date: funnel.sources.imb.captured || '',
unassigned: num(funnel.unassigned ?? 0),
// ⚠ THE SHARE HAS TWO CLAUSES AND THEY ARE PROVABLE SEPARATELY. "1 of K lists" needs the zone
// catalogue, which only describes the DEFAULT list; "{pct}% of the {total}" needs only this
// packet's own row count and the active list's total, both of which are always known. A first
// version suppressed the whole line whenever listCount was absent, which threw away a true
// statement to avoid an unprovable one — the same over-correction as defaulting the unassigned
// count to 0, in the opposite direction.
const share = !total ? '' : `<div class="funnel-share" style="font:700 26px Georgia,serif;color:#7a3b2e;margin-top:10px">
${esc(funnel.listCount
? fill(t('funnel_share'), { k: num(funnel.listCount), pct: pctStr, total: num(total) })
: fill(t('funnel_share_pct'), { pct: pctStr, total: num(total) }))}</div>`;
// ⚠ THE FOOTNOTE IS TWO INDEPENDENT FACTS AND ONLY ONE IS ALWAYS KNOWABLE. Provenance ("counts
// from X, captured Y") holds for every list; coverage ("N of the M are not yet in any list") is a
// fact about the UPG-100 zone catalogue, which partitions DOXA and nothing else. The first version
// filled it with `funnel.unassigned ?? 0`, so a non-default list would have printed
// "0 of the 1,745 are not yet in any list" — a confident falsehood, and the zero makes it read as
// a completeness claim rather than a missing measurement.
// So an unknown coverage drops to the provenance-only string rather than defaulting the number.
// ⚠ NOT BY SPLITTING THE TRANSLATED SENTENCE on punctuation: hi ends with "।" and zh with "。",
// and parsing translated prose for structure is the same trap as reading a localized page header
// positionally (see check-packet-render's "PARSE THE NUMBERS, NEVER THE WORD ORDER").
const imbSrc = funnel.sources && funnel.sources.imb;
const foot = !imbSrc ? '' : esc(
Number.isFinite(Number(funnel.unassigned))
? fill(t('funnel_foot'), {
imb_pub: imbSrc.publisher || '',
imb_date: imbSrc.captured || '',
unassigned: num(funnel.unassigned),
total: num(total),
}))
})
: fill(t('funnel_foot_source'), {
imb_pub: imbSrc.publisher || '',
imb_date: imbSrc.captured || '',
}),
);
// ★ THE READER'S OWN STEP IS THE LAST ROW OF THE FUNNEL, NOT A FOOTER. rega measured the first
// version in print (2026-09-26): the four global steps ended at y≈330 of 1056 while "43 · are in
// THIS packet" sat alone at y≈965, leaving the middle ~60% of the sheet empty and making the
// reader's own step read as a detached footnote. Worse, it had NO BAR, so the funnel never
// visibly narrowed to "yours" — a funnel whose final term is not drawn as part of the funnel
// fails at the one thing it is for.
// So the five steps are ONE list, the reader's included, and the row rhythm is derived from the
// space actually available rather than a fixed 22px gap: measured, four rows filled 451 of the
// 1605px between the intro and the footer, i.e. 28% of the page.
const ROWS_TOP = M + 110;
const FOOT_RESERVE = 210; // share line + source footnote
// ★ A NON-DEFAULT LIST GETS ITS OWN STEP, so the narrowing does not jump from one list's total to
// another list's packet. The four IMB steps stay true under every list — they are global counts,
// unaffected by which list the reader holds — but the LAST one is DOXA's 2,107, and going straight
// from it to "71 in THIS packet" implies DOXA is the parent set of the reader's rows. It is not:
// under ?list=frontier-500k the parent is that list's own DOXA-side total. packetFunnel supplies
// it as listTotal + listLabel when the active list is not the default; when it IS the default,
// listTotal equals the DOXA step and inserting it would repeat the row.
// ⚠ AND IT IS OMITTED WHEN IT WOULD REPEAT A NUMBER ALREADY ON THE PAGE — either the DOXA step
// above it (the default list, where listTotal IS that step) or the reader's row below it. Measured:
// under ?list=frontier-500k the whole list is 71 DOXA groups and a Laos packet drawn from it holds
// all 71, so the page rendered "71 -> 71", two identical bars reading as a funnel that narrows to
// nothing. A step that repeats its neighbour is not a step.
const lastImb = (steps[steps.length - 1] || {}).groups;
const ownStep = (funnel.listLabel && total && total !== lastImb && total !== mine)
? [{ groups: total, population: null, label: funnel.listLabel, tint: true }]
: [];
const allRows = [
...steps.map((s, i) => ({
groups: s.groups,
population: s.population,
label: t(`funnel_${s.id}`) !== undefined ? t(`funnel_${s.id}`) : s.id,
// The network's own list is the hinge between "the world" and "yours" — tint it, and tint
// the reader's row below it the same, so the last two read as one thought. With a
// non-default list the hinge moves down to that list's own row instead.
tint: i === steps.length - 1 && !ownStep.length,
})),
...ownStep,
{ groups: mine, population: null, label: t('funnel_this'), tint: true, mine: true },
];
// ⚠ THE GAP IS DERIVED FROM THE MEASURED ROW HEIGHTS, NOT FROM A GUESS AT THEM. A first version
// subtracted a magic 118 as "about one row" and left 418px of dead space at the foot — the same
// emptiness rega reported, just redistributed. Summing the real heights closes it.
//
// ⚠ AND IT DELIBERATELY KEEPS SLACK, because two runtime facts are unknowable here: whether a
// step carries a population line, and how many LINES its translated label wraps to. Arabic and
// Hindi labels are longer than the English ("مجموعات حدودية — 0.1% أو أقل، ولا يكاد أحد يذهب
// إليها" against "are frontier"), so a label that wraps on two rows must not push the footnote
// off the sheet. LINE_SLACK is that room: one extra wrapped line per row. Tight-packing this
// page would trade rega's "60% empty" for an overflow in exactly the locales I cannot eyeball.
const LINE_SLACK = 30;
const rowHeight = (r) => (r.mine ? 46 : 40) + 3 + (r.mine ? 22 : 20)
+ (Number.isFinite(Number(r.population)) ? 19 : 0) + 8 + 16 + LINE_SLACK;
const rowsHeight = allRows.reduce((sum, r) => sum + rowHeight(r), 0);
const GAP = Math.max(18, Math.floor((H - ROWS_TOP - FOOT_RESERVE - rowsHeight) / (allRows.length - 1)));
const rows = allRows.map((r) => {
// ⚠ `Number.isFinite(Number(x))` IS THE WRONG TEST and it printed "0 people" on the reader's row
// of every packet (rega, rendered layer, 2026-09-26). Number(null) is 0 — only Number(undefined)
// is NaN — so the deliberate `population: null` marker on the reader's row passed the guard and
// rendered as the number zero. I asked "is this a finite number?" when the question was "did
// anyone give me a population?". Presence and value are different properties; this is the same
// confusion as inferring "did it work" from a field that answers "did it isolate".
// AND ZERO IS ALSO SUPPRESSED: a people group with no people is not a fact, so a genuine 0 is
// missing data wearing a number's clothes.
const popN = (r.population === null || r.population === undefined || r.population === '')
? null : Number(r.population);
const pop = (popN !== null && Number.isFinite(popN) && popN > 0)
? `<div style="font:400 17px Georgia,serif;color:#a89f8c;margin-top:2px">${num(popN)} ${esc(t('funnel_people'))}</div>`
: '';
// ⚠ THE LAST ROW DOES NOT KEEP THE FULL GAP. Measured in print (rega): the share line sat a full
// 218px below the reader's row but only 42px above the footnote, so it read as part of the
// FOOTNOTE rather than as the conclusion of the "43" row. The gap belongs BETWEEN rows; the share
// is that last row's conclusion and should sit close under it. Proximity is the only thing
// grouping them, so an even gap everywhere groups the share with the wrong element.
return `<div style="margin-bottom:${r.mine ? 14 : GAP}px"${r.mine ? ' class="funnel-mine-row"' : ''}>
<div${r.mine ? ' class="funnel-mine"' : ''} style="font:700 ${r.mine ? 46 : 40}px Georgia,serif;color:${r.tint ? '#7a3b2e' : '#1b1a17'};line-height:1">${num(r.groups)}</div>
<div style="font:400 ${r.mine ? 22 : 20}px Georgia,serif;color:#3b3226;margin-top:3px">${esc(r.label)}</div>
${pop}
<div style="height:16px;width:${width(r.groups)}px;background:${r.tint ? '#7a3b2e' : '#cdbf9e'};margin-top:8px;border-radius:2px"></div>
</div>`;
}).join('');
return sheet(`
<div style="position:absolute;left:${M}px;top:44px;right:${M}px">
<div class="funnel-title" style="font:700 40px Georgia,serif;color:#1b1a17">${esc(t('funnel_title'))}</div>
<div style="font:italic 18px Georgia,serif;color:#6d766e;margin-top:6px">${esc(t('funnel_intro'))}</div>
</div>
<div style="position:absolute;left:${M}px;top:${M + 96}px;right:${M}px">${rows}</div>
<div style="position:absolute;left:${M}px;right:${M}px;bottom:74px">
<div class="funnel-mine" style="font:700 34px Georgia,serif;color:#7a3b2e;line-height:1">${num(mine)}</div>
<div style="font:400 18px Georgia,serif;color:#3b3226;margin-top:2px">${esc(t('funnel_this'))}</div>
${share}
</div>
<div style="position:absolute;left:${M}px;top:${ROWS_TOP}px;right:${M}px">${rows}${share}</div>
<div class="fine">${foot}</div>`);
}

View file

@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* source-full-adm1.mjs — a COMPLETE ADM1 layer for one country, beside (never replacing) the curated file.
*
* WHY: the shipped boundaries/gb-IND-ADM1.geojson is UPG-100's 100-list PARTITION (34 of India's 36 units;
* Mizoram and Nagaland excluded, and listId-tagged, see plugin-designs/my-upg-100-list/data/
* source-boundaries.mjs). A map that colours EVERY state (Cities with Internet's NFHS-5 layer) had data for
* 36 states and shapes for 34. This writes data/boundaries/gb-<ISO3>-ADM1-full.geojson, all units, from
* geoBoundaries gbOpen, with the source and licence in a .meta.json beside it; generate-tree stages both.
*
* Schema { iso3, shapeGroup, shapeName, shapeISO } (the curated file's, minus the curation's listId).
* Coordinates rounded to 3 decimals (~110 m, far finer than a state choropleth needs) and consecutive
* duplicate points dropped, to keep the served file small.
*
* USAGE node library/scripts/boundaries/source-full-adm1.mjs [ISO3=IND] (network, build time only)
*/
import { writeFileSync, mkdirSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
const ISO3 = (process.argv[2] || 'IND').toUpperCase()
const API = `https://www.geoboundaries.org/api/current/gbOpen/${ISO3}/ADM1/`
const meta = await (await fetch(API)).json()
const url = meta.simplifiedGeometryGeoJSON || meta.gjDownloadURL
const src = await (await fetch(url)).json()
const r = (n) => Math.round(n * 1000) / 1000
const ring = (pts) => {
const out = []
for (const [x, y] of pts) { const p = [r(x), r(y)]; const q = out[out.length - 1]; if (!q || q[0] !== p[0] || q[1] !== p[1]) out.push(p) }
if (out.length && (out[0][0] !== out[out.length - 1][0] || out[0][1] !== out[out.length - 1][1])) out.push(out[0].slice())
return out.length >= 4 ? out : null
}
const poly = (rings) => { const rs = rings.map(ring).filter(Boolean); return rs.length ? rs : null }
const geom = (g) => {
if (g.type === 'Polygon') { const p = poly(g.coordinates); return p && { type: 'Polygon', coordinates: p } }
const ps = g.coordinates.map(poly).filter(Boolean)
return ps.length ? { type: 'MultiPolygon', coordinates: ps } : null
}
const features = []
for (const f of src.features) {
const g = geom(f.geometry)
if (!g) throw new Error(`${f.properties.shapeName}: geometry vanished when rounded`)
features.push({ type: 'Feature', properties: { iso3: ISO3, shapeGroup: f.properties.shapeGroup || ISO3, shapeName: f.properties.shapeName, shapeISO: f.properties.shapeISO || '' }, geometry: g })
}
if (String(features.length) !== String(meta.admUnitCount)) throw new Error(`expected ${meta.admUnitCount} units, got ${features.length}`)
const dir = resolve(ROOT, 'data/boundaries'); mkdirSync(dir, { recursive: true })
const out = resolve(dir, `gb-${ISO3}-ADM1-full.geojson`)
writeFileSync(out, JSON.stringify({ type: 'FeatureCollection', features }))
writeFileSync(resolve(dir, `gb-${ISO3}-ADM1-full.meta.json`), JSON.stringify({
file: `gb-${ISO3}-ADM1-full.geojson`, units: features.length,
source: 'geoBoundaries gbOpen (www.geoboundaries.org)', boundaryID: meta.boundaryID,
boundarySource: meta.boundarySource, licence: meta.boundaryLicense, licenceSource: meta.licenseSource,
yearRepresented: meta.boundaryYearRepresented, sourceUpdated: meta.sourceDataUpdateDate, fetchedFrom: url,
processing: 'coordinates rounded to 3 decimals, consecutive duplicate points dropped (library/scripts/boundaries/source-full-adm1.mjs)',
note: 'Complete ADM1. The curated gb-' + ISO3 + '-ADM1.geojson (the 100-list partition, listId-tagged) is a different file and is not replaced.',
}, null, 2) + '\n')
console.log(`${out}: ${features.length} units`)

View file

@ -175,7 +175,9 @@
"funnel_this": "are in THIS packet",
"funnel_share": "This packet is 1 of {k} lists · {pct}% of the {total}",
"funnel_foot": "Counts from {imb_pub}, captured {imb_date}. {unassigned} of the {total} are not yet in any list.",
"funnel_people": "people"
"funnel_people": "people",
"funnel_foot_source": "Counts from {imb_pub}, captured {imb_date}.",
"funnel_share_pct": "This packet is {pct}% of the {total}"
},
"es": {
"map_page_kicker": "GRUPOS ÉTNICOS NO EVANGELIZADOS",
@ -343,7 +345,9 @@
"funnel_this": "están en ESTE cuadernillo",
"funnel_share": "Este cuadernillo es 1 de {k} listas · {pct}% de los {total}",
"funnel_foot": "Cifras de {imb_pub}, tomadas el {imb_date}. {unassigned} de los {total} aún no están en ninguna lista.",
"funnel_people": "personas"
"funnel_people": "personas",
"funnel_foot_source": "Cifras de {imb_pub}, tomadas el {imb_date}.",
"funnel_share_pct": "Este cuadernillo es el {pct}% de los {total}"
},
"fr": {
"map_page_kicker": "GROUPES ETHNIQUES NON ATTEINTS",
@ -511,7 +515,9 @@
"funnel_this": "sont dans CE livret",
"funnel_share": "Ce livret est 1 des {k} listes · {pct}% des {total}",
"funnel_foot": "Chiffres de {imb_pub}, relevés le {imb_date}. {unassigned} des {total} ne figurent encore sur aucune liste.",
"funnel_people": "personnes"
"funnel_people": "personnes",
"funnel_foot_source": "Chiffres de {imb_pub}, relevés le {imb_date}.",
"funnel_share_pct": "Ce livret représente {pct}% des {total}"
},
"pt": {
"map_page_kicker": "GRUPOS ÉTNICOS NÃO ALCANÇADOS",
@ -679,7 +685,9 @@
"funnel_this": "estão NESTE caderno",
"funnel_share": "Este caderno é 1 de {k} listas · {pct}% dos {total}",
"funnel_foot": "Números de {imb_pub}, coletados em {imb_date}. {unassigned} dos {total} ainda não estão em nenhuma lista.",
"funnel_people": "pessoas"
"funnel_people": "pessoas",
"funnel_foot_source": "Números de {imb_pub}, coletados em {imb_date}.",
"funnel_share_pct": "Este caderno é {pct}% dos {total}"
},
"de": {
"map_page_kicker": "UNERREICHTE VOLKSGRUPPEN",
@ -847,7 +855,9 @@
"funnel_this": "sind in DIESEM Heft",
"funnel_share": "Dieses Heft ist 1 von {k} Listen · {pct}% der {total}",
"funnel_foot": "Zahlen von {imb_pub}, erhoben am {imb_date}. {unassigned} der {total} sind noch auf keiner Liste.",
"funnel_people": "Menschen"
"funnel_people": "Menschen",
"funnel_foot_source": "Zahlen von {imb_pub}, erhoben am {imb_date}.",
"funnel_share_pct": "Dieses Heft ist {pct}% der {total}"
},
"it": {
"map_page_kicker": "GRUPPI ETNICI NON RAGGIUNTI",
@ -1015,7 +1025,9 @@
"funnel_this": "sono in QUESTO libretto",
"funnel_share": "Questo libretto è 1 di {k} liste · {pct}% dei {total}",
"funnel_foot": "Dati da {imb_pub}, rilevati il {imb_date}. {unassigned} dei {total} non sono ancora in nessuna lista.",
"funnel_people": "persone"
"funnel_people": "persone",
"funnel_foot_source": "Dati da {imb_pub}, rilevati il {imb_date}.",
"funnel_share_pct": "Questo libretto è il {pct}% dei {total}"
},
"ro": {
"map_page_kicker": "GRUPURI ETNICE NEATINSE",
@ -1183,7 +1195,9 @@
"funnel_this": "sunt în ACEASTĂ broșură",
"funnel_share": "Această broșură este 1 din {k} liste · {pct}% din {total}",
"funnel_foot": "Cifre de la {imb_pub}, colectate la {imb_date}. {unassigned} din {total} nu sunt încă pe nicio listă.",
"funnel_people": "persoane"
"funnel_people": "persoane",
"funnel_foot_source": "Cifre de la {imb_pub}, colectate la {imb_date}.",
"funnel_share_pct": "Această broșură reprezintă {pct}% din {total}"
},
"ru": {
"map_page_kicker": "НЕОХВАЧЕННЫЕ НАРОДЫ",
@ -1351,7 +1365,9 @@
"funnel_this": "в ЭТОМ буклете",
"funnel_share": "Этот буклет — 1 из {k} списков · {pct}% от {total}",
"funnel_foot": "Данные {imb_pub}, собраны {imb_date}. {unassigned} из {total} пока не входят ни в один список.",
"funnel_people": "человек"
"funnel_people": "человек",
"funnel_foot_source": "Данные {imb_pub}, собраны {imb_date}.",
"funnel_share_pct": "Этот буклет — {pct}% от {total}"
},
"ar": {
"map_page_kicker": "شعوب غير مبلَّغة",
@ -1519,7 +1535,9 @@
"funnel_this": "في هذا الكُتيّب",
"funnel_share": "هذا الكُتيّب هو 1 من {k} قائمة · {pct}% من {total}",
"funnel_foot": "الأرقام من {imb_pub}، جُمعت في {imb_date}. {unassigned} من {total} ليست بعد في أي قائمة.",
"funnel_people": "نسمة"
"funnel_people": "نسمة",
"funnel_foot_source": "الأرقام من {imb_pub}، جُمعت في {imb_date}.",
"funnel_share_pct": "هذا الكُتيّب يمثل {pct}% من {total}"
},
"hi": {
"map_page_kicker": "अनछुए जनसमूह",
@ -1687,7 +1705,9 @@
"funnel_this": "इस पुस्तिका में हैं",
"funnel_share": "यह पुस्तिका {k} सूचियों में से 1 है · {total} का {pct}%",
"funnel_foot": "आंकड़े {imb_pub} से, {imb_date} को लिए गए। {total} में से {unassigned} अभी किसी सूची में नहीं हैं।",
"funnel_people": "लोग"
"funnel_people": "लोग",
"funnel_foot_source": "आंकड़े {imb_pub} से, {imb_date} को लिए गए।",
"funnel_share_pct": "यह पुस्तिका {total} का {pct}% है"
},
"zh": {
"map_page_kicker": "未得之民",
@ -1855,6 +1875,8 @@
"funnel_this": "在这本祷告册里",
"funnel_share": "这本祷告册是 {k} 份名单之一 · 占 {total} 的 {pct}%",
"funnel_foot": "数据来自 {imb_pub},采集于 {imb_date}。{total} 中有 {unassigned} 还不在任何名单上。",
"funnel_people": "人"
"funnel_people": "人",
"funnel_foot_source": "数据来自 {imb_pub},采集于 {imb_date}。",
"funnel_share_pct": "这本祷告册占 {total} 的 {pct}%"
}
}

View file

@ -123,17 +123,24 @@ const clusters = JSON.parse(readFileSync(join(DATA, 'clusters.json'), 'utf8'));
// them, at build time, so no figure on the page is a typed constant.
// A missing file yields null, and buildPacket omits page 2 rather than printing numbers it could
// not read.
const FUNNEL = (() => {
// ⚠ A FUNCTION, NOT A CONSTANT, because it depends on PACKET_LIST — which is resolved further down,
// after the records load. It mirrors upg-100-list.vue's packetFunnel() exactly: the DEFAULT list is the
// only one the zone catalogue describes, so it is the only one that may claim "1 of K lists" and an
// unassigned count; a non-default list states its OWN total and goes quiet on the rest.
function buildFunnel(membership) {
try {
const raw = JSON.parse(readFileSync(join(DATA, '../../../data/people-groups/vision-funnel.json'), 'utf8'));
if (!raw || !Array.isArray(raw.steps) || !raw.steps.length) return null;
if (membership) {
return { ...raw, listTotal: membership.total, listId: LIST, listLabel: membership.label };
}
const lists = [...(clusters.lists || []), ...(clusters.asia_lists || []),
...(clusters.uncovered_lists || [])];
const listTotal = Number(raw.steps[raw.steps.length - 1].groups) || 0;
const assigned = lists.reduce((s, l) => s + (Number(l.count) || 0), 0);
return { ...raw, listCount: lists.length, listTotal, unassigned: Math.max(0, listTotal - assigned) };
} catch (_) { return null; }
})();
}
const { evaluate } = await import(join(DATA, 'selector-evaluator.js'));
const { normalizeRecords, extractRecords } = await import(join(DATA, 'normalize.js'));
@ -229,7 +236,45 @@ async function buildPacket(zone, groups, mapSvg) {
}
// ── MAIN ────────────────────────────────────────────────────────────────────
const records = await loadRecords();
const allRecords = await loadRecords();
// ★ PACKET_LIST MAKES THE NON-DEFAULT-LIST PATH REACHABLE FROM THIS GENERATOR, and it needed to be.
// The BROWSER path honours the universal `list` prop — upg-100-list.vue calls applyPeopleGroupList()
// at every fetch site — but this generator had no list support at all, so the SIX-ROW funnel (a list's
// own total inserted before the reader's row) could not be rendered by the only pipeline rega can
// drive. Its only evidence was a harness I wrote about it, which is the state I have been objecting to
// elsewhere: a code path whose sole witness is its author's own test.
//
// ⚠ IT READS THE MEMBERSHIP FILE FROM DISK rather than through loadListMembership(), which fetches
// from the asset pool over HTTP — there is no server here. Same file, same `slugs`, same counts.doxa.
// ⚠ AND IT FAILS CLOSED. An unknown or unreadable list exits non-zero instead of quietly producing a
// packet of every group: peopleGroupList.js makes the same choice for the same reason ("a map that
// silently showed all 2,107 while an embed asked for Project 700 would be showing the wrong list with
// no sign of it"). A packet is worse — it is printed and handed to someone.
const LIST = process.env.PACKET_LIST || '';
let LIST_MEMBERSHIP = null;
if (LIST) {
const mf = join(__dirname, `../../../data/people-groups/lists/${LIST}.json`);
if (!existsSync(mf)) {
console.error(`[packet] PACKET_LIST=${LIST}: no membership file at ${mf}`);
console.error(' Run library/scripts/people-groups/build-lists.mjs, or use the default list.');
process.exit(1);
}
const m = JSON.parse(readFileSync(mf, 'utf8'));
LIST_MEMBERSHIP = { slugs: new Set(m.slugs || []), total: Number(m?.counts?.doxa) || (m.slugs || []).length, label: m.label || LIST };
if (!LIST_MEMBERSHIP.slugs.size) {
console.error(`[packet] PACKET_LIST=${LIST}: membership file lists no slugs. Failing closed.`);
process.exit(1);
}
}
const records = LIST_MEMBERSHIP
? allRecords.filter((r) => LIST_MEMBERSHIP.slugs.has(r.slug))
: allRecords;
if (LIST_MEMBERSHIP) {
console.log(`[packet] list=${LIST} — ${records.length} of ${allRecords.length} record(s) `
+ `belong to it (list total ${LIST_MEMBERSHIP.total})`);
}
const FUNNEL = buildFunnel(LIST_MEMBERSHIP);
const bySlug = new Map(records.map((r) => [r.slug, r]));
const zones = allZones().filter((z) => !ONLY || z.id === ONLY);
if (!zones.length) { console.error(`[packet] no zone matches ONLY=${ONLY}`); process.exit(1); }

View file

@ -136,6 +136,7 @@
"check:packet-browser": "node internal/check-packet-browser.mjs",
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
"check:packet-render": "node internal/check-packet-render.mjs",
"check:packet-render:expect-funnel": "node internal/check-packet-render.mjs --expect-funnel",
"check:packet-strings-parity": "node internal/check-packet-strings-parity.mjs",
"check:packets-all": "node internal/check-packets-all.mjs",
"check:parse": "node internal/inline-parse-check.mjs",
@ -202,6 +203,7 @@
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
"lists": "node library/scripts/people-groups/build-lists.mjs",
"packet-fixture": "node internal/generate-packet-fixture.mjs",
@ -327,6 +329,7 @@
"check:no-free-identifiers": "every name a .vue script uses is imported, declared, a parameter or a known global (scope-aware, across plugin-designs and library). A free identifier compiles and then throws at runtime in exactly the path nobody clicked.",
"check:packet-browser": "the packet the READER gets — the legend's Export > Prayer packet path, proving the shared renderer runs in a BROWSER. Asserts the same 12 points / 12 wheel labels / page-1 geography / print layer as check:packet-render, which it did not until 2026-09-24. CONDITIONAL: SKIPs without a server over the repo root (python3 -m http.server 4898) and a reachable records API.",
"check:packet-fixture": "the committed fixture must still match clusters.json and must never carry image_url (CI would fetch 43 photos from someone else's CDN).",
"check:packet-render:expect-funnel": "check:packet-render with the page-2 FUNNEL declared REQUIRED. Exists because of a real near-miss: a merge resolved with `git checkout --ours` deleted the printPacket call site that supplies the funnel data (60 lines) and EVERY GATE STAYED GREEN — it had to, because the funnel page is OPTIONAL by design (buildPacket omits it when the arg is null), so a packet without page 2 is not malformed, just smaller. Always requiring it would be the wrong fix: the 11 pre-funnel laos packets and any host with no funnel data would go red, and a false red costs as much as a false green. So the CALLER, which knows whether this packet was built with funnel data, declares it — an absence is then either declared (no flag) or a failure (flag passed, page missing), never a silent nothing. Pass it for any packet from a profile that wires packetFunnel(). Browser gate.",
"check:packet-strings-parity": "the PACKET catalog (packet-strings.json) must be whole in every language: all string keys, all 12 prayer points with every field, all 12 wheel rhythms. check:i18n-parity covers library/i18n/locales only — six gates READ this file and none checked it. Asserts PRESENCE, not translatedness, with point4_passage declared-absent on record.",
"check:packets-all": "runs check:packet-render over EVERY generated packet, not one hand-named file. The single-file gate SKIPs with no argument, so it was in package.json, passing in all 11 locales, and in neither gates:pre nor gates:post — it only ran when someone typed a filename. SKIPs when no packets are generated.",
"check:pin-dim-slider": "the side menu's pin-dim slider and the legend's dimming must COMPOSE, not compete. Both want the pin layer's circle-opacity, so there is ONE writer and the value is the PRODUCT: slider x (1 matched, 0 dimmed). Asserts the LIVE paint property, not the store: flat 1 with clustering off, flat 0.5 with clustering on, ['case', expr, 0.5, 0] once something is dimmed, and ['case', expr, 1, 0] when the slider returns to 1. Dims by RELIGION deliberately - a region selection sets a layer FILTER rather than an opacity expression, so testing that path would report a failure about code that never writes opacity.",
@ -360,11 +363,12 @@
"check:zone-census": "every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.",
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins and go-int can reuse it.",
"library-index": "regenerate documentation/reference-library-index.md — the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
"lists": "regenerate data/people-groups/lists/<id>.json, the DOXA slugs in each people-group list of library/lists/lists.json, from the IMB CSV and the DOXA snapshot. Re-run after either changes.",
"packet-fixture": "regenerate the committed records subset (smallest zone, image_url stripped) that lets CI build a packet at all — out/ is gitignored, so a clean checkout has neither the records cache nor any packet.",
"packet:ci": "build EVERY committed fixture, no network, each renamed to its own output so they coexist. Was one fixture (tibet); packet.mjs names its output for the ZONE not the fixture, so sequential builds overwrote each other and check:packets-all saw a single SHAPE while reporting success. The fixtures are chosen at the PER_PAGE=20 page boundary — exactly 20, 21, and 1 group — because laos (95) and tibet (43) are both mid-range remainder cases and the boundary itself had never rendered.",
"packets": "the prayer packets. READS out/posters/<zone>.html for page 1's map, which `bun run posters` writes — without it the packet still builds and page 1 is simply BLANK, so the script now warns per-zone AND in its summary. PACKET_LANG=<loc> for one language, ONLY=<zoneKey> for one zone, NOPHOTOS=1 to skip the photo fetch.",
"packets": "the prayer packets. READS out/posters/<zone>.html for page 1's map, which `bun run posters` writes — without it the packet still builds and page 1 is simply BLANK, so the script now warns per-zone AND in its summary. PACKET_LANG=<loc> for one language, ONLY=<zoneKey> for one zone, NOPHOTOS=1 to skip the photo fetch. ENV: ONLY=<zone> one zone · PACKET_LANG=<locale> · PACKET_RECORDS=<file> a committed fixture instead of the live API · PACKET_LIST=<list-id> the universal list prop for this generator (doxa default; unreached/frontier/frontier-500k/unengaged read data/people-groups/lists/<id>.json from disk, since there is no server to fetch the asset pool from). PACKET_LIST FAILS CLOSED on an unknown or empty list rather than silently printing every group — a packet that showed the wrong list is printed and handed to someone. It exists because the browser path honours ?list= while this generator did not, so the six-row funnel (a list's own total inserted before the reader's row) was unreachable by the only pipeline that can render it.",
"posters:one": "ONLY=<zoneKey> to build one",
"serve": "serves public/ the way PRODUCTION does: a DIRECTORY request returns 404, no index fallback. This was python3 -m http.server, which DID resolve a directory to index.html while the live static layer does not — so gates passed on URLs that 404 live (measured 2026-09-23; it cost a false \"publish failed\" reading and hid a bad URL in verify-shell-scroll for a whole flight). Loopback only, same as before. Override port/host with PORT/HOST.",
"serve:permissive": "LOOPBACK ONLY. This was --bind 0.0.0.0, which published the built tree on every interface of whatever machine ran a gate; measured on the build VPS it answered HTTP 200 on the box's PUBLIC IP while a browser gate was running. Every consumer (verify-browser-all, check:a11y-button-names, check:nav-reachable, smoke:share-all) already defaults to 127.0.0.1, so nothing needed the wider bind. Override with BASE_URL/BASE if you deliberately want to reach it from another host.",

View file

@ -78,6 +78,10 @@ const rows = ref([])
const selected = ref(null)
const aiNoticeOpen = ref(true) // dismissible AI-research disclaimer in the detail sheet
const err = ref(null)
// India by state (data/tables/internet-by-adm1.json, NFHS-5): the one country with sub-national data.
const adm1 = shallowRef(null) // { meta, byArea: { [adm1]: { name, women: {total,urban,rural}, men: {…} } } }
const adm1Sex = ref('women') // which half of the survey the fill shows; both are in the sheet
const selectedAdm1 = ref(null)
// ── People-group dots (the agent: "red dots on the map to show me where the people groups are")
// The DOXA list from pray-tools, the same list Prayer Progress draws. It carries the slug the
@ -264,12 +268,78 @@ function paint(map, adm0) {
// A tap on a people-group dot belongs to the dot, not the country under it.
if (map.getLayer('ci-pg-dots') && map.queryRenderedFeatures(e.point, { layers: ['ci-pg-dots'] }).length) return
const iso = e.features?.[0]?.properties?.iso3
if (iso && byIso.value[iso]) { uiStore?.selectPeopleGroup(null); selected.value = byIso.value[iso] }
if (iso && byIso.value[iso]) { uiStore?.selectPeopleGroup(null); selectedAdm1.value = null; selected.value = byIso.value[iso] }
})
map.on('mouseenter', 'ci-fill', () => { map.getCanvas().style.cursor = 'pointer' })
map.on('mouseleave', 'ci-fill', () => { map.getCanvas().style.cursor = '' })
}
// ── INDIA BY STATE: "ever used the internet", NFHS-5 2019–21, women and men asked separately ─────
// A share of PEOPLE (0–100%), so it gets its own ramp and its own legend block, never the country
// bands: the bands are World Bank national "internet users", a different measure and source.
// Blues, so no state can be read as one of the band colours.
const ADM1_STEPS = [
{ from: 0, color: '#deebf7' },
{ from: 25, color: '#9ecae1' },
{ from: 40, color: '#6baed6' },
{ from: 55, color: '#3182bd' },
{ from: 70, color: '#08519c' },
]
const adm1Fill = (sex) => ['case', ['==', ['typeof', ['get', sex]], 'number'],
['step', ['get', sex], ...ADM1_STEPS.flatMap((st, i) => (i === 0 ? [st.color] : [st.from, st.color]))],
'rgba(0,0,0,0)']
async function loadAdm1() {
const [table, geo] = await Promise.all([
fetch(`${assetsBase.value}/tables/internet-by-adm1.json`).then((r) => { if (!r.ok) throw new Error('internet-by-adm1 ' + r.status); return r.json() }),
// All 36 states (data/boundaries/gb-IND-ADM1-full.geojson). NOT gb-IND-ADM1.geojson: that one is
// UPG-100's partition, curated to 34 units, and it left Mizoram and Nagaland with no geometry.
fetch(`${assetsBase.value}/boundaries/gb-IND-ADM1-full.geojson`).then((r) => { if (!r.ok) throw new Error('gb-IND-ADM1-full ' + r.status); return r.json() }),
])
const byArea = {}
for (const r of table.rows || []) {
if (r.iso3 !== 'IND' || r.measure !== 'ever_used_internet' || !r.adm1) continue
const a = (byArea[r.adm1] ??= { name: r.adm1_name || r.adm1, women: {}, men: {} })
a[r.sex][r.area] = r.value
}
adm1.value = markRaw({ meta: table.meta || {}, byArea })
return geo
}
function paintAdm1(map, geo) {
const byArea = adm1.value.byArea
const fc = { type: 'FeatureCollection', features: (geo.features || []).map((f) => {
const a = byArea[f.properties?.shapeName]
return { type: 'Feature', geometry: f.geometry,
properties: { adm1: f.properties?.shapeName, women: a?.women.total ?? null, men: a?.men.total ?? null } }
}) }
// Under the people-group dots, which stay tappable on top.
const before = map.getLayer('ci-pg-dots') ? 'ci-pg-dots' : undefined
map.addSource('ci-adm1-src', { type: 'geojson', data: fc })
map.addLayer({ id: 'ci-adm1-fill', type: 'fill', source: 'ci-adm1-src',
paint: { 'fill-color': adm1Fill(adm1Sex.value), 'fill-opacity': 0.85 } }, before)
map.addLayer({ id: 'ci-adm1-line', type: 'line', source: 'ci-adm1-src',
paint: { 'line-color': 'rgba(255,255,255,0.8)', 'line-width': 0.6 } }, before)
// India's national band would sit under its states and show through any state without a
// feature as if it were that state's figure. Take India out of the country layer.
if (map.getLayer('ci-fill')) map.setFilter('ci-fill', ['!=', ['get', 'iso3'], 'IND'])
map.on('click', 'ci-adm1-fill', (e) => {
if (map.getLayer('ci-pg-dots') && map.queryRenderedFeatures(e.point, { layers: ['ci-pg-dots'] }).length) return
const key = e.features?.[0]?.properties?.adm1
const a = key && byArea[key]
if (a) { uiStore?.selectPeopleGroup(null); selected.value = null; selectedAdm1.value = { key, ...a } }
})
map.on('mouseenter', 'ci-adm1-fill', () => { map.getCanvas().style.cursor = 'pointer' })
map.on('mouseleave', 'ci-adm1-fill', () => { map.getCanvas().style.cursor = '' })
}
function showAdm1Sex(sex) {
adm1Sex.value = sex
const map = mapRef.value
if (map?.getLayer('ci-adm1-fill')) map.setPaintProperty('ci-adm1-fill', 'fill-color', adm1Fill(sex))
}
const adm1Missing = computed(() => (adm1.value?.meta?.boundaries?.IND?.missing || []).join(', '))
// ── HONOUR THE PROVIDER SWITCH: this profile builds its map RAW ─────────────
// Every bundle that goes through useMapInstance gets the switch for free. This one calls
// `new gl.Map` directly, so it was invisible to it — MEASURED by sweeping all 16 isMap leaves:
@ -293,6 +363,8 @@ onMounted(async () => {
try {
// The dots are a second layer, not a precondition: if the list fails, the map still shades.
const groupsLoaded = loadGroups().catch((e) => { console.warn('[connectivity-map] people groups:', e?.message || e) })
// India by state is a third layer, and optional like the dots: without it India keeps its band.
const adm1Loaded = loadAdm1().catch((e) => { console.warn('[connectivity-map] India by state:', e?.message || e); return null })
const adm0 = await load()
// The pane can unmount during the awaits above (a tab switch mid-load). Building a map
// into a container that no longer exists throws; this is the raw-map twin of the
@ -325,6 +397,8 @@ onMounted(async () => {
selectedPin.initialize()
await groupsLoaded
paintGroups(map)
const geo = await adm1Loaded
if (geo && !unmounted) paintAdm1(map, geo)
})
} catch (e) { err.value = String(e.message || e) }
})
@ -367,6 +441,16 @@ useShadowStyles(
'.gm-key{display:flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}' +
'.gm-sw{width:12px;height:12px;border-radius:3px;flex:none}' +
'.gm-caveat{margin-top:5px;font-size:10.5px;opacity:.72;line-height:1.4}' +
'.gm-adm1{margin-top:8px;padding-top:7px;border-top:1px solid rgba(0,0,0,.12)}' +
'.gm-adm1-h{font-weight:600;font-size:12px;margin-bottom:5px}' +
'.gm-seg{display:inline-flex;border:1px solid rgba(0,0,0,.2);border-radius:6px;overflow:hidden;margin-bottom:6px}' +
'.gm-seg button{font:inherit;font-size:11px;padding:3px 10px;border:0;background:transparent;color:inherit;cursor:pointer}' +
'.gm-seg button.on{background:#08519c;color:#fff}' +
'.gm-adm1-t{width:100%;border-collapse:collapse;font-size:12px;margin:4px 0 6px}' +
'.gm-adm1-t th,.gm-adm1-t td{padding:3px 4px;text-align:end}' +
'.gm-adm1-t th:first-child{text-align:start}' +
'.is-dark .gm-adm1{border-top-color:rgba(255,255,255,.15)}' +
'.is-dark .gm-seg{border-color:rgba(255,255,255,.3)}' +
// DETAIL: a bottom sheet on a phone, a corner card on desktop.
'.gm-sheet{position:absolute;left:0;right:0;bottom:0;z-index:6;max-height:62%;overflow-y:auto;' +
'background:#fff;color:#0f1419;border-radius:14px 14px 0 0;padding:14px 16px 22px;' +
@ -426,6 +510,21 @@ useShadowStyles(
<div class="gm-caveat">
{{ t('connectivity.caveat') }}
</div>
<!-- India by state: a different measure and source, so its own block and its own ramp. -->
<div v-if="adm1" class="gm-adm1">
<div class="gm-adm1-h">{{ t('connectivity.adm1.title') }}</div>
<div class="gm-seg" role="group" :aria-label="t('connectivity.adm1.title')">
<button type="button" :class="{ on: adm1Sex === 'women' }" :aria-pressed="adm1Sex === 'women'" @click="showAdm1Sex('women')">{{ t('connectivity.adm1.women') }}</button>
<button type="button" :class="{ on: adm1Sex === 'men' }" :aria-pressed="adm1Sex === 'men'" @click="showAdm1Sex('men')">{{ t('connectivity.adm1.men') }}</button>
</div>
<div class="gm-keys">
<span v-for="(st, i) in ADM1_STEPS" :key="st.from" class="gm-key">
<i class="gm-sw" :style="{ background: st.color }"></i>{{ i + 1 < ADM1_STEPS.length ? st.from + '–' + ADM1_STEPS[i + 1].from + '%' : st.from + '%+' }}
</span>
</div>
<div class="gm-caveat">{{ t('connectivity.adm1.source') }}</div>
<div v-if="adm1Missing" class="gm-caveat">{{ t('connectivity.adm1.missing', { names: adm1Missing }) }}</div>
</div>
</div>
<div v-if="err" class="gm-err">{{ err }}</div>
@ -438,6 +537,25 @@ useShadowStyles(
<LegendMobile legend-type="prayer" popup-action="pray" :is-dark="isDark" />
</div>
<!-- Tap an Indian state: the same sheet, with both halves of the survey. -->
<div v-if="selectedAdm1" class="gm-sheet">
<div class="gm-sh-head">
<span class="gm-sh-name">{{ selectedAdm1.name }}</span>
<button type="button" class="gm-x" @click="selectedAdm1 = null">{{ t('connectivity.sheet.close') }}</button>
</div>
<div class="gm-list-h">{{ t('connectivity.adm1.title') }}</div>
<table class="gm-adm1-t">
<thead><tr><th></th><th>{{ t('connectivity.adm1.total') }}</th><th>{{ t('connectivity.adm1.urban') }}</th><th>{{ t('connectivity.adm1.rural') }}</th></tr></thead>
<tbody>
<tr v-for="sex in ['women', 'men']" :key="sex">
<th>{{ t('connectivity.adm1.' + sex) }}</th>
<td v-for="area in ['total', 'urban', 'rural']" :key="area">{{ selectedAdm1[sex][area] != null ? selectedAdm1[sex][area] + '%' : '—' }}</td>
</tr>
</tbody>
</table>
<p class="gm-note">{{ t('connectivity.adm1.source') }}</p>
</div>
<!-- Tap a country: bottom sheet on mobile, corner card on desktop. -->
<div v-if="selected" class="gm-sheet">
<div class="gm-sh-head">

View file

@ -0,0 +1,118 @@
#!/usr/bin/env node
/**
* build-adm1-internet.mjs — internet use by first-level administrative area (ADM1), from data.
*
* node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs
*
* READS data/internet/nfhs5-internet-by-state.csv India, NFHS-5 2019–21 (see data/internet/README.md)
* data/internet/adm1-crosswalk-IND.json source names -> boundary shapeName (strict)
* data/boundaries/gb-IND-ADM1-full.geojson the boundary file the map draws (all 36)
* WRITES data/tables/internet-by-adm1.json served as assets/tables/internet-by-adm1.json
*
* THE TABLE IS GENERIC ON PURPOSE (other plugins and go-int reuse it). One row per
* (iso3, adm1, measure, sex, area): no India-specific shape, so a second country or a second
* measure is more rows, not a new format. `meta.boundaries[iso3]` names the geometry file and the
* property its `adm1` joins on; `meta.measures` and `meta.sources` say what each number is.
*
* STRICT. The run FAILS when a source name is missing from the crosswalk, when a crosswalk target
* is not a shapeName in the boundary file, or when a value is not a number in 0–100. A name is
* never guessed. Source rows that have no geometry (crosswalk `adm1: null`) are KEPT, keyed by
* their source name, and listed in meta.boundaries[iso3].missing, so nothing is silently dropped.
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
const CSV = join(ROOT, 'data/internet/nfhs5-internet-by-state.csv')
const CROSSWALK = join(ROOT, 'data/internet/adm1-crosswalk-IND.json')
const BOUNDARY = join(ROOT, 'data/boundaries/gb-IND-ADM1-full.geojson')
const OUT = join(ROOT, 'data/tables/internet-by-adm1.json')
const fail = (msg) => { console.error('build-adm1-internet: FAIL — ' + msg); process.exit(1) }
// A quoted-field CSV reader (the NFHS file quotes names that contain commas).
function parseCsv(text) {
const rows = []; let row = []; let f = ''; let q = false
for (let i = 0; i < text.length; i++) {
const c = text[i]
if (q) { if (c === '"') { if (text[i + 1] === '"') { f += '"'; i++ } else q = false } else f += c }
else if (c === '"') q = true
else if (c === ',') { row.push(f); f = '' }
else if (c === '\n') { row.push(f); rows.push(row); row = []; f = '' }
else if (c !== '\r') f += c
}
if (f || row.length) { row.push(f); rows.push(row) }
const head = rows.shift()
return rows.filter((r) => r.length === head.length).map((r) => Object.fromEntries(head.map((h, i) => [h, r[i]])))
}
const src = parseCsv(readFileSync(CSV, 'utf8'))
const crosswalk = JSON.parse(readFileSync(CROSSWALK, 'utf8')).map
const shapeNames = new Set(JSON.parse(readFileSync(BOUNDARY, 'utf8')).features.map((f) => f.properties.shapeName))
const SEX = { 'Women who have ever used the internet (%)': 'women', 'Men who have ever used the internet (%)': 'men' }
const AREAS = { total: 'nfhs5_total', urban: 'nfhs5_urban', rural: 'nfhs5_rural' }
const rows = []
const national = []
const missing = new Set()
for (const r of src) {
const sex = SEX[r.indicator.replace(/^\d+\.\s*/, '')]
if (!sex) fail(`unexpected indicator "${r.indicator}"`)
const cw = crosswalk[r.state]
if (!cw) fail(`"${r.state}" is not in data/internet/adm1-crosswalk-IND.json`)
if (cw.adm1 && !shapeNames.has(cw.adm1)) fail(`crosswalk maps "${r.state}" to "${cw.adm1}", which is not a shapeName in gb-IND-ADM1-full.geojson`)
if (!cw.adm1 && !cw.reason) fail(`crosswalk entry for "${r.state}" has no adm1 and no reason`)
for (const [area, col] of Object.entries(AREAS)) {
const raw = String(r[col] ?? '').trim()
if (raw === '' || raw === '*' || raw === 'na') continue // not reported for this cell
const value = Number(raw)
if (!Number.isFinite(value) || value < 0 || value > 100) fail(`"${r.state}" ${sex} ${area}: "${raw}" is not a share in 0–100`)
const row = { iso3: 'IND', measure: 'ever_used_internet', sex, area, value, source: 'nfhs5', year: '2019-21' }
if (cw.national) { national.push(row); continue }
if (!cw.adm1) missing.add(r.state)
rows.push({ ...row, adm1: cw.adm1 || null, adm1_name: r.state })
}
}
if (!rows.length) fail('no rows (vacuous run)')
const out = {
meta: {
title: 'Internet use by first-level administrative area',
grain: 'one row per iso3 x adm1 x measure x sex x area',
measures: {
ever_used_internet: {
label: 'Ever used the internet',
unit: '%',
population: 'people aged 15–49, women and men asked separately',
note: 'A survey SHARE OF PEOPLE (0–100). Not a subscription count: never put it on one scale with subscriptions per 100.',
},
},
sources: {
nfhs5: {
name: 'National Family Health Survey 5 (NFHS-5), IIPS / Ministry of Health and Family Welfare, India',
year: '2019-21',
url: 'http://rchiips.org/nfhs/',
via: 'https://github.com/pratapvardhan/NFHS-5 (CSV transcription of the state fact sheets)',
licence: 'CC BY 4.0',
attribution: 'NFHS-5 (IIPS/MoHFW) state fact sheets, via pratapvardhan/NFHS-5 (CC BY 4.0)',
},
},
boundaries: {
IND: {
file: 'boundaries/gb-IND-ADM1-full.geojson',
key: 'shapeName',
licence: 'geoBoundaries gbOpen IND-ADM1-1811400 (DataMeet / ECI): CC BY 2.5 IN',
missing: [...missing].sort(),
missing_reason: 'no feature for it in the boundary file',
},
},
national,
generated_by: 'plugin-designs/cities-internet/scripts/build-adm1-internet.mjs',
},
rows,
}
writeFileSync(OUT, JSON.stringify(out, null, 1) + '\n')
const drawn = new Set(rows.filter((r) => r.adm1).map((r) => r.adm1)).size
console.log(`build-adm1-internet: ${rows.length} rows · IND ${drawn} ADM1 areas drawn · ${missing.size} without geometry (${[...missing].join(', ') || 'none'}) · national ${national.length} rows`)

View file

@ -23,7 +23,11 @@
*/
import { inject, provide, ref, shallowRef, markRaw, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
// The page's people-group LIST (library/lists/): every people-group fetch below keeps only its groups.
import { applyPeopleGroupList } from '@map/lists/peopleGroupList.js'
// resolvePeopleGroupList/listInfo/LIST_DEFAULT are for the PACKET'S FUNNEL page, which must state the
// total of the list the packet was actually drawn from — see packetFunnel().
import {
applyPeopleGroupList, resolvePeopleGroupList, listInfo, LIST_DEFAULT,
} from '@map/lists/peopleGroupList.js'
import { retryableAsyncComponent } from '@map/composables/retryableAsyncComponent.js'
import SemanticTreeLegend from '@map/components/legends/SemanticTreeLegend.vue'
// The shared mobile bottom-sheet. Fed this profile's own tree through the `nodes`
@ -1326,19 +1330,58 @@ function allLists() {
// said "~30 lists of 50-100" — only about half sit in that band, so no copy here claims it. The
// 27 lists account for 2,086 of the 2,107, leaving 21 in no list; the footnote states that plainly
// rather than letting a partner discover it by adding up rows (shepherd, ENDTOKEN-DXS-UPG-KDECIDE).
// ⚠ IT IS LIST-AWARE, AND IT HAS TO BE — THIS WAS A LIVE WRONG NUMBER, not a future risk. This
// profile already honours the universal `list` prop: applyPeopleGroupList() filters the rows at all
// four fetch sites, including packetRecords (:1258), and the membership files are on disk. So under
// ?list=frontier-500k the packet's rows are the 71 DOXA groups that are Project 700 — while the
// funnel, which took its denominator from the last step of vision-funnel.json, printed
// "of the 2,107". Off by a factor of 30, with nothing on the page looking broken.
// Measured 2026-09-26 from data/people-groups/lists/*.json (counts.doxa, what a Tier A map shows):
// frontier-500k 71 · frontier 1,745 · unengaged 1,929 · unreached 2,082 · doxa (default) 2,107
//
// ★ THREE NUMBERS WERE WRONG, NOT ONE, because all three were derived from DOXA's total:
// listTotal — the share's denominator;
// unassigned — the footnote's "N are in no list", computed as listTotal minus the assigned sum;
// listCount — 27, which is a property of the UPG-100 ZONE CATALOGUE, not of the active list. The
// zones partition DOXA; they do not partition frontier.
// So under a non-default list this states only what it can prove: the real total from the membership
// file, and NO listCount and NO unassigned — funnelPage omits the share line and the footnote rather
// than print a plausible wrong one. Half a fraction is not a fact.
async function packetFunnel() {
try {
const raw = (await import('../../data/people-groups/vision-funnel.json')).default;
if (!raw || !Array.isArray(raw.steps) || !raw.steps.length) return null;
const listId = resolvePeopleGroupList(profileConfig);
const doxaTotal = Number(raw.steps[raw.steps.length - 1].groups) || 0;
// THE DEFAULT LIST is the one the zone catalogue actually describes, so it is the only one that
// can honestly claim "1 of K lists" and an unassigned count.
if (listId === LIST_DEFAULT) {
const lists = allLists();
const listTotal = Number(raw.steps[raw.steps.length - 1].groups) || 0;
const assigned = lists.reduce((sum, l) => sum + (Number(l.count) || 0), 0);
return {
...raw,
listCount: lists.length,
listTotal,
unassigned: Math.max(0, listTotal - assigned),
listTotal: doxaTotal,
unassigned: Math.max(0, doxaTotal - assigned),
};
}
// A NON-DEFAULT LIST: read its own DOXA-side total from the membership file the build writes.
// counts.doxa is precisely "how many of this list DOXA carries", i.e. what a Tier A map shows.
// If it cannot be read, the share line is omitted — the steps above it are still true.
let listTotal = 0;
try {
const base = resolveAssetsBase(profileConfig.value);
const res = await fetch(`${base}/people-groups/lists/${listId}.json`);
if (res.ok) {
const m = await res.json();
listTotal = Number(m?.counts?.doxa) || (Array.isArray(m?.slugs) ? m.slugs.length : 0);
}
} catch (_) { /* leave listTotal 0 -> no share line */ }
return { ...raw, listTotal, listId, listLabel: (listInfo(listId) || {}).label || listId };
} catch (_) {
// A missing funnel file omits page 2 rather than failing the packet — see buildPacket.
return null;
@ -1461,14 +1504,33 @@ function packetLang() {
return (typeof l === 'string' && l) || 'en';
}
// ★ IT REPORTS WHETHER IT ACTED. Returns true when the selection was applied, false when it bailed.
//
// WHY: a silent `return` on a precondition is INDISTINGUISHABLE AT THE RENDERED LAYER from the
// request never arriving (regions-plugin + upg100, 2026-09-26). The "Show on map" interop bug had
// three layers stacked under one symptom — an element event with no replay, an id resolved against
// unloaded legendNodes, and THIS function no-op'ing on a missing layer — and each fix looked
// sufficient because all three produced the same nothing. Two were fixed by retaining the request;
// this one is the third, and it will grow a fourth layer for whoever comes next unless the applier
// says when it did not apply.
//
// ⚠ THE EARLY RETURN STAYS, and it stays SILENT of side effects — it is a documented readiness gate
// (see the comment at :630, "THE REAL READINESS SIGNAL"), and other code relies on it bailing rather
// than throwing or queueing. Returning a boolean is the smallest change that makes the bail
// OBSERVABLE without changing when it happens: a caller that cares can retain and retry, and the two
// callers that do not (paintAll's re-apply, the un-isolate at :945) are unaffected by a value they
// ignore.
function onLegendSelect(node) {
if (!map || !map.getLayer('tapestry-fill')) return
if (!map || !map.getLayer('tapestry-fill')) return false
// R1.2 D-6: in batch mode the legend feeds the selection union, NOT the isolate.
// (node is non-null on a row tap; the legend emits null when a row is re-tapped
// to deselect — in batch mode that null is a no-op, the union persists.)
if (batchMode.value) {
if (node) toggleZoneSelection(node)
return
// TRUE: batch mode HANDLED the request — it fed the union, which is the correct behaviour here
// rather than a failure to isolate. A caller retrying because it read `false` would toggle the
// zone back OUT of the union, so this distinction is load-bearing, not cosmetic.
return true
}
if (!node) {
// restore the full 26-zone tapestry
@ -1478,7 +1540,7 @@ function onLegendSelect(node) {
map.setFilter('tapestry-line', null)
// restore FULL opacity to match the new visible-dots baseline (BUG-UPG100-DOTS 2026-09-19)
map.setPaintProperty('upg-pins', 'circle-opacity', 1)
return
return true // the un-isolate IS the action; it succeeded
}
_isolatedNode = node
const keys = [...descendantKeys(node)]
@ -1493,6 +1555,10 @@ function onLegendSelect(node) {
}
const b = boundsFor((f) => keys.includes(f.properties.zoneKey))
if (b) map.fitBounds(b, { padding: 40, duration: 700, maxZoom: 6 })
// EXPLICIT, not an implicit undefined: the isolate was applied. An implicit fall-through returns
// undefined, which is falsy, so a caller checking the result would read the SUCCESS path as a
// failure and retry — the opposite of the bug this return value exists to expose.
return true
}
// ── ONE paint entrypoint (P0 4188af3d / LESSON-03 L7+L10) ────────────────────
@ -1602,12 +1668,18 @@ let _selectZoneObserver = null; // watches the retained `select-zone` at
// onLegendSelect and invisible: nothing throws, and "selected but not visibly selected" is
// indistinguishable at the rendered layer from "never selected". Writing the outcome onto the HOST
// makes it readable by rega's existing 50 ms poll, with no CDP and no new instrument.
// ★ NAMESPACED, on upg100-plugin's point: the host element is SHARED SURFACE. A partner page
// embedding this custom element could reasonably own an attribute called `data-select-zone-state`,
// and a diagnostic has no business squatting a generic name on someone else's element. The
// data-doxa- prefix makes the owner obvious and the collision risk nil.
// Renamed AFTER rega's re-shoot rather than before, deliberately: their probe polls these names, and
// renaming mid-flight would have burned a measurement instead of costing a few minutes.
function _markZoneState(state, zoneId) {
try {
const h = mapEl.value?.getRootNode?.()?.host;
if (!h) return;
h.setAttribute('data-select-zone-state', state);
if (zoneId) h.setAttribute('data-select-zone-applied', String(zoneId));
h.setAttribute('data-doxa-select-zone-state', state);
if (zoneId) h.setAttribute('data-doxa-select-zone-applied', String(zoneId));
} catch (_) { /* never let a diagnostic break the apply */ }
}
@ -1615,7 +1687,7 @@ function applySelectZone(zoneId) {
if (!zoneId) { onLegendSelect(null); _pendingZoneId = undefined; _markZoneState('cleared', ''); return true; }
const node = findLegendNode(legendNodes.value, zoneId);
if (!node) { _pendingZoneId = zoneId; _markZoneState('unresolved', zoneId); return false; } // not ready -- REMEMBER it
onLegendSelect(node);
const applied = onLegendSelect(node);
// selectedKey (:163, "currently isolated zoneKey (or null)") is this component's own record of the
// isolation, so 'applied-but-no-selection' means the path was REACHED and its EFFECT lost -- a
// different bug from delivery, and the one rega's timeline points at.
@ -1629,7 +1701,22 @@ function applySelectZone(zoneId) {
// map's tapestry layer is not up, which at t=4.9s it was not. So the readiness signal was never
// legendNodes or mapRef -- it is THE LAYER, and the old code cleared _pendingZoneId here as though
// the work were done. Keeping it means ensureLayers() can drain it the moment the layer exists.
if (!selectedKey.value) { _pendingZoneId = zoneId; _markZoneState('applied-but-no-selection', zoneId); return false; }
// ★ ASK THE APPLIER, DO NOT INFER FROM A SIDE EFFECT -- and the inference was WRONG IN BATCH MODE.
// upg100-plugin caught this prospectively: onLegendSelect feeds the selection UNION in batch mode
// and returns without ever setting selectedKey, because isolating is not what it should do there.
// So `!selectedKey.value` read that correct behaviour as a failure, and retaining on it would have
// re-applied the zone and toggled it back OUT of the union. Un-isolating (node == null) is the same
// shape: the un-isolate IS the action, and selectedKey is null afterwards BY DESIGN.
// They have made onLegendSelect return a boolean for exactly this (fb076c8b on packet/vertical),
// where false means one thing only: the map was not ready.
// FORWARD-COMPATIBLE ON PURPOSE. That change is not on main yet, and a function with no explicit
// return yields undefined, which is FALSY -- so depending on it today would read every success as a
// failure and retry it, which is the very trap they warned about. So the boolean is authoritative
// WHEN IT IS A BOOLEAN, and until then the old inference stands with batch mode excluded.
const ok = (typeof applied === 'boolean')
? applied
: (batchMode.value ? true : !!selectedKey.value);
if (!ok) { _pendingZoneId = zoneId; _markZoneState('applied-but-no-selection', zoneId); return false; }
_markZoneState('applied', zoneId);
return true;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,14 @@
{
"file": "gb-IND-ADM1-full.geojson",
"units": 36,
"source": "geoBoundaries gbOpen (www.geoboundaries.org)",
"boundaryID": "IND-ADM1-1811400",
"boundarySource": "DataMeet India community, Election Commission of India",
"licence": "Creative Commons Attribution 2.5 India (CC BY 2.5 IN)",
"licenceSource": "github.com/datameet/maps",
"yearRepresented": "2011",
"sourceUpdated": "Thu Jan 19 07:31:04 2023",
"fetchedFrom": "https://github.com/wmgeolab/geoBoundaries/raw/9469f09/releaseData/gbOpen/IND/ADM1/geoBoundaries-IND-ADM1_simplified.geojson",
"processing": "coordinates rounded to 3 decimals, consecutive duplicate points dropped (library/scripts/boundaries/source-full-adm1.mjs)",
"note": "Complete ADM1. The curated gb-IND-ADM1.geojson (the 100-list partition, listId-tagged) is a different file and is not replaced."
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <cities-internet-map> custom element. -->
<script src="../cities-internet.js" integrity="sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb" crossorigin="anonymous"></script>
<script src="../cities-internet.js" integrity="sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
<script src="../country-posters.js" integrity="sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/" crossorigin="anonymous"></script>
<script src="../country-posters.js" integrity="sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -104,7 +104,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "dx-maps/cities-internet/cities-internet.js",
"integrity": "sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb",
"integrity": "sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w",
"elements": [
"cities-internet-map"
],
@ -120,7 +120,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -134,7 +134,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -148,7 +148,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
}
]
@ -157,7 +157,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/",
"integrity": "sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw",
"elements": [
"country-posters-map"
],
@ -177,7 +177,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -191,7 +191,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-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=\"./country-posters/country-posters.js\" integrity=\"sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -205,7 +205,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -663,7 +663,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1",
"integrity": "sha384-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ",
"elements": [
"my-upg-100-list-map"
],
@ -683,7 +683,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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1\" 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-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ\" 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>"
}
},
{
@ -697,7 +697,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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1\" 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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ\" 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>"
}
},
{
@ -711,7 +711,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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1\" 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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ\" 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>"
}
}
]
@ -887,7 +887,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy",
"integrity": "sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R",
"elements": [
"regions-dashboard"
],
@ -907,7 +907,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -921,7 +921,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -935,7 +935,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -104,7 +104,7 @@
"bundle": "cities-internet",
"scriptUrl": "./cities-internet/cities-internet.js",
"output": "dx-maps/cities-internet/cities-internet.js",
"integrity": "sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb",
"integrity": "sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w",
"elements": [
"cities-internet-map"
],
@ -120,7 +120,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -134,7 +134,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/connectivity-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"connectivity-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
},
{
@ -148,7 +148,7 @@
"scriptUrl": "./cities-internet/cities-internet.js",
"embed": {
"iframe": "<iframe src=\"./cities-internet/data-tables/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=\"./cities-internet/cities-internet.js\" integrity=\"sha384-dmLMT9Nj5CcCrJ+Fd2rJSTeFPIp6IdZMwlDWGuUZRaRZuekeTVJgFd4uXUuYcXxb\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./cities-internet/cities-internet.js\" integrity=\"sha384-K1YI/L1c574EYK5Wrr8omwHK1BExIjd/348gi1L8H/WSs5whTjEH4pkESO/QHR+w\" crossorigin=\"anonymous\"></script>\n<cities-internet-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></cities-internet-map>"
}
}
]
@ -157,7 +157,7 @@
"bundle": "country-posters",
"scriptUrl": "./country-posters/country-posters.js",
"output": "dx-maps/country-posters/country-posters.js",
"integrity": "sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/",
"integrity": "sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw",
"elements": [
"country-posters-map"
],
@ -177,7 +177,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -191,7 +191,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-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=\"./country-posters/country-posters.js\" integrity=\"sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
},
{
@ -205,7 +205,7 @@
"scriptUrl": "./country-posters/country-posters.js",
"embed": {
"iframe": "<iframe src=\"./country-posters/country-posters-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-riJmSVRhsdkMQSbHHdt4vHW6xSJ5/O5sx67LIesHNfUjKTf5H2IFw0R7YCJEWPc/\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-/7gss6lpFoPS1ww0mbVdAPmmzAwIG2q0WFaiSD25s7i+8Of+qQ9RNDYgrnMF07Pw\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-posters-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
}
}
]
@ -663,7 +663,7 @@
"bundle": "my-upg-100-list",
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
"output": "dx-maps/my-upg-100-list/my-upg-100-list.js",
"integrity": "sha384-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1",
"integrity": "sha384-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ",
"elements": [
"my-upg-100-list-map"
],
@ -683,7 +683,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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1\" 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-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ\" 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>"
}
},
{
@ -697,7 +697,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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1\" 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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ\" 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>"
}
},
{
@ -711,7 +711,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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1\" 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": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<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-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ\" 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>"
}
}
]
@ -887,7 +887,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "dx-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy",
"integrity": "sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R",
"elements": [
"regions-dashboard"
],
@ -907,7 +907,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -921,7 +921,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-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=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -935,7 +935,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<!-- Mapbox engine, fetched early. Keep it only with a real Mapbox token in \"tk\": without one (or with \"provider\":\"maplibre\") the map draws with MapLibre and this line must go -->\n<link rel=\"preload\" as=\"script\" href=\"https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js\" integrity=\"sha384-tO0clamXONKwbiz7O6xJpUeyJbTSPugC+xM9clvfFvKTH3i8SmCawHGdf5quMWmZ\" crossorigin=\"anonymous\">\n<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,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-VhZri8Qh1TSJ2JVPDVrMqkcgZXtV8omcW9R6b8qk/aSkyGqtsZm5+hjLXha3W/o1" crossorigin="anonymous"></script>
<script src="../my-upg-100-list.js" integrity="sha384-GuGN5aNC2EkSR4BDhl2MVTmSnuNW2Faob6rFzHHCwy7e71YVMkKxQKcINpt4EDiZ" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -36,7 +36,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -32,7 +32,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -47,7 +47,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-lVTa9tfmmNWafuXn4kn4kekzcN/nm2TrZp1HJTgTxpMZ+kyvT0x6XT/KeW9u+jMy" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-geDFTb1+yi9+SR7Inll5n1WSorDYVa3k+FRsOhrTLytgTtztkGMKnSXWGtdZxO3R" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: