Merge main and regenerate the served tree
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
public/doxa-maps/index.html conflicted; generated, so resolved by regenerating. fix:sri reports all 113 hashes already in agreement.
This commit is contained in:
commit
795382fabc
54 changed files with 25123 additions and 615 deletions
24
README.md
24
README.md
|
|
@ -366,6 +366,30 @@ sanity-check one bundle without touching anyone else's output, use
|
|||
|
||||
---
|
||||
|
||||
## The delegation model — one build, many agents, one main
|
||||
|
||||
This repo is designed for **many builders working in parallel without stepping on each
|
||||
other**. The model is a set of concentric rings:
|
||||
|
||||

|
||||
|
||||
- **`main`** — the one shared branch. Everything converges here.
|
||||
- **Capture ring** — before pushing to `main`, a builder must *capture* it: **one builder at
|
||||
a time, first-come-first-serve.** Whoever holds it **resolves all merge conflicts**, lands
|
||||
their work, then releases. The next builder waits and retries. No two pushes race.
|
||||
- **Plugin Studio** — one repo, every plugin visible in one place, so each builder can see
|
||||
what already exists and reuse it.
|
||||
- **Plugins** — each is built in the shared bundler, so its shared, parameterized components
|
||||
become available to every other plugin (private components stay inside each plugin's own
|
||||
folder).
|
||||
- **Agents** — **one builder per plugin.** Each owns their plugin end to end. A cluster of
|
||||
experimental plugins is owned by a single roaming builder.
|
||||
- **Shepherd** — one builder keeps everyone honoring the model: plugins built in the studio,
|
||||
shared components truly shared, and every push serialized through the capture ring.
|
||||
|
||||
The effect is cooperative: each builder races to land the most working features, but the only
|
||||
way to score is to pass through the capture ring one at a time and **leave `main` green.**
|
||||
|
||||
## Repository layout
|
||||
|
||||
Two directories matter to a plugin author:
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@
|
|||
"title": "The gap by country",
|
||||
"grain": "one row per country that has at least one people group on this map",
|
||||
"generated": "2026-09-24",
|
||||
"measured": "242 countries · 2,901 groups with nothing found · 287,644,779 people",
|
||||
"by_region": "Oceania 51.2% (679 of 1325) · Africa 30.0% (923 of 3074) · Asia 19.3% (978 of 5073) · Americas 11.4% (241 of 2117) · Europe 10.1% (80 of 790)",
|
||||
"measured": "242 countries · 2,873 groups with nothing found · 287,305,229 people",
|
||||
"by_region": "Oceania 51.2% (678 of 1325) · Africa 29.9% (919 of 3074) · Asia 19.2% (974 of 5073) · Americas 10.5% (222 of 2117) · Europe 10.1% (80 of 790)",
|
||||
"three_questions": "Groups with nothing, people with nothing, and the share of the country each represents. They rank differently on purpose — Papua New Guinea has the most groups and few people, China the reverse, and Oceania has the worst share of any region while being the smallest by population.",
|
||||
"source": "data/tables/gospel-by-country.json, which is itself refreshed from data/tables/people-group-pins.json — the same index the map draws, so this table and the map cannot disagree.",
|
||||
"curated_columns": "growing_unreached and deaths_day come from the curated country analysis and are null where it does not cover the country. Null, never zero: zero is a claim this build cannot make."
|
||||
|
|
@ -82,9 +82,9 @@
|
|||
"iso3": "PNG",
|
||||
"region": "Oceania",
|
||||
"people_groups": 831,
|
||||
"groups_with_nothing": 469,
|
||||
"pct_with_nothing": 56.4,
|
||||
"people_with_nothing": 2187150,
|
||||
"groups_with_nothing": 468,
|
||||
"pct_with_nothing": 56.3,
|
||||
"people_with_nothing": 2179750,
|
||||
"groups_with_scripture": 332,
|
||||
"groups_with_film": 143,
|
||||
"band": "high",
|
||||
|
|
@ -112,9 +112,9 @@
|
|||
"iso3": "NGA",
|
||||
"region": "Africa",
|
||||
"people_groups": 433,
|
||||
"groups_with_nothing": 188,
|
||||
"pct_with_nothing": 43.4,
|
||||
"people_with_nothing": 9036825,
|
||||
"groups_with_nothing": 187,
|
||||
"pct_with_nothing": 43.2,
|
||||
"people_with_nothing": 8978825,
|
||||
"groups_with_scripture": 105,
|
||||
"groups_with_film": 188,
|
||||
"band": "high",
|
||||
|
|
@ -127,9 +127,9 @@
|
|||
"iso3": "CMR",
|
||||
"region": "Africa",
|
||||
"people_groups": 260,
|
||||
"groups_with_nothing": 130,
|
||||
"pct_with_nothing": 50,
|
||||
"people_with_nothing": 3722425,
|
||||
"groups_with_nothing": 129,
|
||||
"pct_with_nothing": 49.6,
|
||||
"people_with_nothing": 3656425,
|
||||
"groups_with_scripture": 72,
|
||||
"groups_with_film": 84,
|
||||
"band": "high",
|
||||
|
|
@ -262,9 +262,9 @@
|
|||
"iso3": "BRA",
|
||||
"region": "Americas",
|
||||
"people_groups": 270,
|
||||
"groups_with_nothing": 62,
|
||||
"pct_with_nothing": 23,
|
||||
"people_with_nothing": 73130,
|
||||
"groups_with_nothing": 58,
|
||||
"pct_with_nothing": 21.5,
|
||||
"people_with_nothing": 65680,
|
||||
"groups_with_scripture": 186,
|
||||
"groups_with_film": 163,
|
||||
"band": "partial",
|
||||
|
|
@ -292,9 +292,9 @@
|
|||
"iso3": "USA",
|
||||
"region": "Americas",
|
||||
"people_groups": 623,
|
||||
"groups_with_nothing": 44,
|
||||
"pct_with_nothing": 7.1,
|
||||
"people_with_nothing": 473415,
|
||||
"groups_with_nothing": 41,
|
||||
"pct_with_nothing": 6.6,
|
||||
"people_with_nothing": 465640,
|
||||
"groups_with_scripture": 517,
|
||||
"groups_with_film": 549,
|
||||
"band": "partial",
|
||||
|
|
@ -332,21 +332,6 @@
|
|||
"deaths_day": 0.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Vietnam",
|
||||
"iso3": "VNM",
|
||||
"region": "Asia",
|
||||
"people_groups": 74,
|
||||
"groups_with_nothing": 30,
|
||||
"pct_with_nothing": 40.5,
|
||||
"people_with_nothing": 1902675,
|
||||
"groups_with_scripture": 28,
|
||||
"groups_with_film": 38,
|
||||
"band": "high",
|
||||
"growing_unreached": 11,
|
||||
"deaths_day": 5.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Republic of the Congo",
|
||||
"iso3": "COG",
|
||||
|
|
@ -362,6 +347,21 @@
|
|||
"deaths_day": 1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Vietnam",
|
||||
"iso3": "VNM",
|
||||
"region": "Asia",
|
||||
"people_groups": 74,
|
||||
"groups_with_nothing": 29,
|
||||
"pct_with_nothing": 39.2,
|
||||
"people_with_nothing": 1860175,
|
||||
"groups_with_scripture": 28,
|
||||
"groups_with_film": 38,
|
||||
"band": "high",
|
||||
"growing_unreached": 11,
|
||||
"deaths_day": 5.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "New Caledonia",
|
||||
"iso3": "NCL",
|
||||
|
|
@ -397,9 +397,9 @@
|
|||
"iso3": "MMR",
|
||||
"region": "Asia",
|
||||
"people_groups": 110,
|
||||
"groups_with_nothing": 26,
|
||||
"pct_with_nothing": 23.6,
|
||||
"people_with_nothing": 1278900,
|
||||
"groups_with_nothing": 25,
|
||||
"pct_with_nothing": 22.7,
|
||||
"people_with_nothing": 1274400,
|
||||
"groups_with_scripture": 63,
|
||||
"groups_with_film": 64,
|
||||
"band": "partial",
|
||||
|
|
@ -467,36 +467,6 @@
|
|||
"deaths_day": 13.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bhutan",
|
||||
"iso3": "BTN",
|
||||
"region": "Asia",
|
||||
"people_groups": 36,
|
||||
"groups_with_nothing": 20,
|
||||
"pct_with_nothing": 55.6,
|
||||
"people_with_nothing": 240825,
|
||||
"groups_with_scripture": 11,
|
||||
"groups_with_film": 16,
|
||||
"band": "high",
|
||||
"growing_unreached": 13,
|
||||
"deaths_day": 2.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Canada",
|
||||
"iso3": "CAN",
|
||||
"region": "Americas",
|
||||
"people_groups": 304,
|
||||
"groups_with_nothing": 20,
|
||||
"pct_with_nothing": 6.6,
|
||||
"people_with_nothing": 187600,
|
||||
"groups_with_scripture": 256,
|
||||
"groups_with_film": 270,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Afghanistan",
|
||||
"iso3": "AFG",
|
||||
|
|
@ -512,6 +482,21 @@
|
|||
"deaths_day": 17.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bhutan",
|
||||
"iso3": "BTN",
|
||||
"region": "Asia",
|
||||
"people_groups": 36,
|
||||
"groups_with_nothing": 19,
|
||||
"pct_with_nothing": 52.8,
|
||||
"people_with_nothing": 238225,
|
||||
"groups_with_scripture": 11,
|
||||
"groups_with_film": 16,
|
||||
"band": "high",
|
||||
"growing_unreached": 13,
|
||||
"deaths_day": 2.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Ethiopia",
|
||||
"iso3": "ETH",
|
||||
|
|
@ -527,6 +512,21 @@
|
|||
"deaths_day": 130,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Canada",
|
||||
"iso3": "CAN",
|
||||
"region": "Americas",
|
||||
"people_groups": 304,
|
||||
"groups_with_nothing": 18,
|
||||
"pct_with_nothing": 5.9,
|
||||
"people_with_nothing": 183000,
|
||||
"groups_with_scripture": 256,
|
||||
"groups_with_film": 270,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Russian Federation",
|
||||
"iso3": "RUS",
|
||||
|
|
@ -572,21 +572,6 @@
|
|||
"deaths_day": 12.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Peru",
|
||||
"iso3": "PER",
|
||||
"region": "Americas",
|
||||
"people_groups": 90,
|
||||
"groups_with_nothing": 16,
|
||||
"pct_with_nothing": 17.8,
|
||||
"people_with_nothing": 1194475,
|
||||
"groups_with_scripture": 73,
|
||||
"groups_with_film": 57,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 5.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Ghana",
|
||||
"iso3": "GHA",
|
||||
|
|
@ -647,6 +632,21 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Peru",
|
||||
"iso3": "PER",
|
||||
"region": "Americas",
|
||||
"people_groups": 90,
|
||||
"groups_with_nothing": 12,
|
||||
"pct_with_nothing": 13.3,
|
||||
"people_with_nothing": 1160875,
|
||||
"groups_with_scripture": 73,
|
||||
"groups_with_film": 57,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 5.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Oman",
|
||||
"iso3": "OMN",
|
||||
|
|
@ -722,21 +722,6 @@
|
|||
"deaths_day": 0.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Gabon",
|
||||
"iso3": "GAB",
|
||||
"region": "Africa",
|
||||
"people_groups": 45,
|
||||
"groups_with_nothing": 9,
|
||||
"pct_with_nothing": 20,
|
||||
"people_with_nothing": 99600,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 24,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.3,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "French Polynesia",
|
||||
"iso3": "PYF",
|
||||
|
|
@ -767,6 +752,21 @@
|
|||
"deaths_day": 4.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Gabon",
|
||||
"iso3": "GAB",
|
||||
"region": "Africa",
|
||||
"people_groups": 45,
|
||||
"groups_with_nothing": 8,
|
||||
"pct_with_nothing": 17.8,
|
||||
"people_with_nothing": 92800,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 24,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.3,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Namibia",
|
||||
"iso3": "NAM",
|
||||
|
|
@ -797,21 +797,6 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Venezuela",
|
||||
"iso3": "VEN",
|
||||
"region": "Americas",
|
||||
"people_groups": 51,
|
||||
"groups_with_nothing": 7,
|
||||
"pct_with_nothing": 13.7,
|
||||
"people_with_nothing": 311800,
|
||||
"groups_with_scripture": 42,
|
||||
"groups_with_film": 39,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.2,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Cambodia",
|
||||
"iso3": "KHM",
|
||||
|
|
@ -1022,21 +1007,6 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Benin",
|
||||
"iso3": "BEN",
|
||||
"region": "Africa",
|
||||
"people_groups": 57,
|
||||
"groups_with_nothing": 5,
|
||||
"pct_with_nothing": 8.8,
|
||||
"people_with_nothing": 554500,
|
||||
"groups_with_scripture": 29,
|
||||
"groups_with_film": 38,
|
||||
"band": "partial",
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 21.9,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Brunei Darussalam",
|
||||
"iso3": "BRN",
|
||||
|
|
@ -1052,6 +1022,21 @@
|
|||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Venezuela",
|
||||
"iso3": "VEN",
|
||||
"region": "Americas",
|
||||
"people_groups": 51,
|
||||
"groups_with_nothing": 5,
|
||||
"pct_with_nothing": 9.8,
|
||||
"people_with_nothing": 307700,
|
||||
"groups_with_scripture": 42,
|
||||
"groups_with_film": 39,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.2,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Taiwan",
|
||||
"iso3": "TWN",
|
||||
|
|
@ -1172,21 +1157,6 @@
|
|||
"deaths_day": 1.3,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Mexico",
|
||||
"iso3": "MEX",
|
||||
"region": "Americas",
|
||||
"people_groups": 205,
|
||||
"groups_with_nothing": 5,
|
||||
"pct_with_nothing": 2.4,
|
||||
"people_with_nothing": 27700,
|
||||
"groups_with_scripture": 193,
|
||||
"groups_with_film": 175,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Egypt",
|
||||
"iso3": "EGY",
|
||||
|
|
@ -1217,6 +1187,21 @@
|
|||
"deaths_day": 3.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Benin",
|
||||
"iso3": "BEN",
|
||||
"region": "Africa",
|
||||
"people_groups": 57,
|
||||
"groups_with_nothing": 4,
|
||||
"pct_with_nothing": 7,
|
||||
"people_with_nothing": 500000,
|
||||
"groups_with_scripture": 29,
|
||||
"groups_with_film": 38,
|
||||
"band": "partial",
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 21.9,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Comoros",
|
||||
"iso3": "COM",
|
||||
|
|
@ -1322,21 +1307,6 @@
|
|||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bolivia",
|
||||
"iso3": "BOL",
|
||||
"region": "Americas",
|
||||
"people_groups": 39,
|
||||
"groups_with_nothing": 4,
|
||||
"pct_with_nothing": 10.3,
|
||||
"people_with_nothing": 134875,
|
||||
"groups_with_scripture": 34,
|
||||
"groups_with_film": 28,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "French Guiana",
|
||||
"iso3": "GUF",
|
||||
|
|
@ -1562,6 +1532,21 @@
|
|||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Mexico",
|
||||
"iso3": "MEX",
|
||||
"region": "Americas",
|
||||
"people_groups": 205,
|
||||
"groups_with_nothing": 3,
|
||||
"pct_with_nothing": 1.5,
|
||||
"people_with_nothing": 18600,
|
||||
"groups_with_scripture": 193,
|
||||
"groups_with_film": 175,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Wallis and Futuna",
|
||||
"iso3": "WLF",
|
||||
|
|
@ -1727,6 +1712,21 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bolivia",
|
||||
"iso3": "BOL",
|
||||
"region": "Americas",
|
||||
"people_groups": 39,
|
||||
"groups_with_nothing": 2,
|
||||
"pct_with_nothing": 5.1,
|
||||
"people_with_nothing": 120250,
|
||||
"groups_with_scripture": 34,
|
||||
"groups_with_film": 28,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Honduras",
|
||||
"iso3": "HND",
|
||||
|
|
@ -2687,21 +2687,6 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Syrian Arab Republic",
|
||||
"iso3": "SYR",
|
||||
"region": "Asia",
|
||||
"people_groups": 20,
|
||||
"groups_with_nothing": 1,
|
||||
"pct_with_nothing": 5,
|
||||
"people_with_nothing": 16000,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 19,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 25.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Lithuania",
|
||||
"iso3": "LTU",
|
||||
|
|
@ -3392,6 +3377,21 @@
|
|||
"deaths_day": 39.8,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Syrian Arab Republic",
|
||||
"iso3": "SYR",
|
||||
"region": "Asia",
|
||||
"people_groups": 20,
|
||||
"groups_with_nothing": 0,
|
||||
"pct_with_nothing": 0,
|
||||
"people_with_nothing": 0,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 19,
|
||||
"band": "covered",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 25.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Jordan",
|
||||
"iso3": "JOR",
|
||||
|
|
|
|||
|
|
@ -59,14 +59,14 @@
|
|||
"country": "Brazil",
|
||||
"region": "Americas",
|
||||
"groups": 270,
|
||||
"no_resource": 62,
|
||||
"pop_no_resource": 73130,
|
||||
"no_resource": 58,
|
||||
"pop_no_resource": 65680,
|
||||
"scripture": 186,
|
||||
"film": 163,
|
||||
"growing_unreached": 38,
|
||||
"deaths_day": 1.2,
|
||||
"growth": 0.41,
|
||||
"pct_no_resource": 23,
|
||||
"pct_no_resource": 21.5,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -104,14 +104,14 @@
|
|||
"country": "Nigeria",
|
||||
"region": "Africa",
|
||||
"groups": 433,
|
||||
"no_resource": 188,
|
||||
"pop_no_resource": 9036825,
|
||||
"no_resource": 187,
|
||||
"pop_no_resource": 8978825,
|
||||
"scripture": 105,
|
||||
"film": 188,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 44,
|
||||
"growth": null,
|
||||
"pct_no_resource": 43.4,
|
||||
"pct_no_resource": 43.2,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -164,14 +164,14 @@
|
|||
"country": "Bhutan",
|
||||
"region": "Asia",
|
||||
"groups": 36,
|
||||
"no_resource": 20,
|
||||
"pop_no_resource": 240825,
|
||||
"no_resource": 19,
|
||||
"pop_no_resource": 238225,
|
||||
"scripture": 11,
|
||||
"film": 16,
|
||||
"growing_unreached": 13,
|
||||
"deaths_day": 2.1,
|
||||
"growth": 0.65,
|
||||
"pct_no_resource": 55.6,
|
||||
"pct_no_resource": 52.8,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -194,14 +194,14 @@
|
|||
"country": "Vietnam",
|
||||
"region": "Asia",
|
||||
"groups": 74,
|
||||
"no_resource": 30,
|
||||
"pop_no_resource": 1902675,
|
||||
"no_resource": 29,
|
||||
"pop_no_resource": 1860175,
|
||||
"scripture": 28,
|
||||
"film": 38,
|
||||
"growing_unreached": 11,
|
||||
"deaths_day": 5.7,
|
||||
"growth": 0.63,
|
||||
"pct_no_resource": 40.5,
|
||||
"pct_no_resource": 39.2,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -284,14 +284,14 @@
|
|||
"country": "Cameroon",
|
||||
"region": "Africa",
|
||||
"groups": 260,
|
||||
"no_resource": 130,
|
||||
"pop_no_resource": 3722425,
|
||||
"no_resource": 129,
|
||||
"pop_no_resource": 3656425,
|
||||
"scripture": 72,
|
||||
"film": 84,
|
||||
"growing_unreached": 8,
|
||||
"deaths_day": 3.4,
|
||||
"growth": 2.61,
|
||||
"pct_no_resource": 50,
|
||||
"pct_no_resource": 49.6,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -494,14 +494,14 @@
|
|||
"country": "Myanmar",
|
||||
"region": "Asia",
|
||||
"groups": 110,
|
||||
"no_resource": 26,
|
||||
"pop_no_resource": 1278900,
|
||||
"no_resource": 25,
|
||||
"pop_no_resource": 1274400,
|
||||
"scripture": 63,
|
||||
"film": 64,
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 25.8,
|
||||
"growth": 0.67,
|
||||
"pct_no_resource": 23.6,
|
||||
"pct_no_resource": 22.7,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -509,14 +509,14 @@
|
|||
"country": "Benin",
|
||||
"region": "Africa",
|
||||
"groups": 57,
|
||||
"no_resource": 5,
|
||||
"pop_no_resource": 554500,
|
||||
"no_resource": 4,
|
||||
"pop_no_resource": 500000,
|
||||
"scripture": 29,
|
||||
"film": 38,
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 21.9,
|
||||
"growth": 2.46,
|
||||
"pct_no_resource": 8.8,
|
||||
"pct_no_resource": 7,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -674,14 +674,14 @@
|
|||
"country": "Peru",
|
||||
"region": "Americas",
|
||||
"groups": 90,
|
||||
"no_resource": 16,
|
||||
"pop_no_resource": 1194475,
|
||||
"no_resource": 12,
|
||||
"pop_no_resource": 1160875,
|
||||
"scripture": 73,
|
||||
"film": 57,
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 5.4,
|
||||
"growth": 1.09,
|
||||
"pct_no_resource": 17.8,
|
||||
"pct_no_resource": 13.3,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -809,14 +809,14 @@
|
|||
"country": "Gabon",
|
||||
"region": "Africa",
|
||||
"groups": 45,
|
||||
"no_resource": 9,
|
||||
"pop_no_resource": 99600,
|
||||
"no_resource": 8,
|
||||
"pop_no_resource": 92800,
|
||||
"scripture": 13,
|
||||
"film": 24,
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.3,
|
||||
"growth": 2.16,
|
||||
"pct_no_resource": 20,
|
||||
"pct_no_resource": 17.8,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -824,14 +824,14 @@
|
|||
"country": "Venezuela",
|
||||
"region": "Americas",
|
||||
"groups": 51,
|
||||
"no_resource": 7,
|
||||
"pop_no_resource": 311800,
|
||||
"no_resource": 5,
|
||||
"pop_no_resource": 307700,
|
||||
"scripture": 42,
|
||||
"film": 39,
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.2,
|
||||
"growth": 0.37,
|
||||
"pct_no_resource": 13.7,
|
||||
"pct_no_resource": 9.8,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -1049,14 +1049,14 @@
|
|||
"country": "Papua New Guinea",
|
||||
"region": "Oceania",
|
||||
"groups": 831,
|
||||
"no_resource": 469,
|
||||
"pop_no_resource": 2187150,
|
||||
"no_resource": 468,
|
||||
"pop_no_resource": 2179750,
|
||||
"scripture": 332,
|
||||
"film": 143,
|
||||
"growing_unreached": 1,
|
||||
"deaths_day": 0.9,
|
||||
"growth": 1.78,
|
||||
"pct_no_resource": 56.4,
|
||||
"pct_no_resource": 56.3,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -1889,15 +1889,15 @@
|
|||
"country": "Syrian Arab Republic",
|
||||
"region": "Asia",
|
||||
"groups": 20,
|
||||
"no_resource": 1,
|
||||
"pop_no_resource": 16000,
|
||||
"no_resource": 0,
|
||||
"pop_no_resource": 0,
|
||||
"scripture": 13,
|
||||
"film": 19,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 25.7,
|
||||
"growth": 4.47,
|
||||
"pct_no_resource": 5,
|
||||
"band": "partial"
|
||||
"pct_no_resource": 0,
|
||||
"band": "covered"
|
||||
},
|
||||
{
|
||||
"iso3": "JOR",
|
||||
|
|
@ -2264,14 +2264,14 @@
|
|||
"country": "United States of America",
|
||||
"region": "Americas",
|
||||
"groups": 623,
|
||||
"no_resource": 44,
|
||||
"pop_no_resource": 473415,
|
||||
"no_resource": 41,
|
||||
"pop_no_resource": 465640,
|
||||
"scripture": 517,
|
||||
"film": 549,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0.1,
|
||||
"growth": 0.96,
|
||||
"pct_no_resource": 7.1,
|
||||
"pct_no_resource": 6.6,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -2384,14 +2384,14 @@
|
|||
"country": "Bolivia",
|
||||
"region": "Americas",
|
||||
"groups": 39,
|
||||
"no_resource": 4,
|
||||
"pop_no_resource": 134875,
|
||||
"no_resource": 2,
|
||||
"pop_no_resource": 120250,
|
||||
"scripture": 34,
|
||||
"film": 28,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0,
|
||||
"growth": 1.37,
|
||||
"pct_no_resource": 10.3,
|
||||
"pct_no_resource": 5.1,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -2676,14 +2676,14 @@
|
|||
"country": "Mexico",
|
||||
"region": "Americas",
|
||||
"groups": 205,
|
||||
"no_resource": 5,
|
||||
"pop_no_resource": 27700,
|
||||
"no_resource": 3,
|
||||
"pop_no_resource": 18600,
|
||||
"scripture": 193,
|
||||
"film": 175,
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"growth": null,
|
||||
"pct_no_resource": 2.4,
|
||||
"pct_no_resource": 1.5,
|
||||
"band": "partial",
|
||||
"from_pins_only": true
|
||||
},
|
||||
|
|
@ -2868,14 +2868,14 @@
|
|||
"country": "Canada",
|
||||
"region": "Americas",
|
||||
"groups": 304,
|
||||
"no_resource": 20,
|
||||
"pop_no_resource": 187600,
|
||||
"no_resource": 18,
|
||||
"pop_no_resource": 183000,
|
||||
"scripture": 256,
|
||||
"film": 270,
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"growth": null,
|
||||
"pct_no_resource": 6.6,
|
||||
"pct_no_resource": 5.9,
|
||||
"band": "partial",
|
||||
"from_pins_only": true
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7242
data/tables/scriptureearth-index.json
Normal file
7242
data/tables/scriptureearth-index.json
Normal file
File diff suppressed because it is too large
Load diff
BIN
documentation/readme-images/delegation-model.png
Normal file
BIN
documentation/readme-images/delegation-model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
|
|
@ -46,3 +46,42 @@ export async function servesThisTree(base, repo) {
|
|||
}
|
||||
return { ok: true, why: 'same build' }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start a short-lived static server over THIS checkout, so a gate whose default port belongs
|
||||
* to another worktree can still MEASURE instead of skipping.
|
||||
*
|
||||
* Skipping was the honest thing to do once the mismatch was visible, but a gate that skips on
|
||||
* every wave is a gate nobody is running: check:provider-switch and check:packet-browser both
|
||||
* went quiet the moment the guard landed, because their default ports (4173, 4898) are held by
|
||||
* other worktrees on this box. Serving our own tree costs a few hundred milliseconds and gives
|
||||
* the gate the one thing it needs — a server it can prove is this build.
|
||||
*
|
||||
* @param {string} dir absolute directory to serve (public/, or the repo root)
|
||||
* @returns {Promise<{origin:string, stop:() => void}|null>} null when no server could be started
|
||||
*/
|
||||
export async function serveThisTree(dir) {
|
||||
const { spawn } = await import('node:child_process')
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
// An ephemeral high port, re-rolled on collision — two agents doing this at once must not
|
||||
// fight over one number.
|
||||
const port = 20000 + Math.floor(Math.random() * 20000)
|
||||
const child = spawn('python3', ['-m', 'http.server', String(port), '--bind', '127.0.0.1', '--directory', dir],
|
||||
{ stdio: 'ignore', detached: false })
|
||||
const origin = `http://127.0.0.1:${port}`
|
||||
let up = false
|
||||
for (let i = 0; i < 40 && !up; i++) {
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
up = await fetch(origin + '/', { signal: AbortSignal.timeout(500) }).then((r) => r.ok).catch(() => false)
|
||||
}
|
||||
if (up) {
|
||||
// unref so a caller that forgets to stop() still exits — a gate that hangs on its own
|
||||
// helper is a worse failure than the one it was written to catch.
|
||||
child.unref()
|
||||
return { origin, stop: () => { try { child.kill() } catch { /* already gone */ } } }
|
||||
}
|
||||
try { child.kill() } catch { /* nothing to kill */ }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,26 @@ import { chromium } from 'playwright'
|
|||
// train everyone to ignore the wave.
|
||||
const BASE = process.env.BASE_URL || 'http://127.0.0.1:5173'
|
||||
const PATH_ = process.env.PROBE_PATH || '/'
|
||||
|
||||
// ⚠ THE SERVER THAT ANSWERS IS NOT NECESSARILY THIS CHECKOUT. Several worktrees of this repo
|
||||
// run on one machine and a default port belongs to whichever started first — measured
|
||||
// 2026-09-24 on :4173. This gate reports on how a map's source data changes when a facet is
|
||||
// picked, so a run against another tree describes somebody else's data flow. The fingerprint
|
||||
// is public/doxa-maps/manifest.json; a dev server serves it straight off disk, so this stays
|
||||
// quiet in the normal `bun run dev` case and only speaks when the bytes really differ.
|
||||
{
|
||||
const { servesThisTree } = await import('./_serves-this-tree.mjs')
|
||||
const { fileURLToPath } = await import('node:url')
|
||||
const { dirname, resolve } = await import('node:path')
|
||||
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const tree = await servesThisTree(BASE, repo)
|
||||
if (/DIFFERENT build/.test(tree.why)) {
|
||||
console.log('FACET-CHANGES-MAP: SKIP — the server is not serving THIS build.')
|
||||
console.log(` ${tree.why}`)
|
||||
console.log(' Point BASE_URL at this checkout (bun run dev, or bun run serve).')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
const TAB = process.env.PROBE_TAB || 'research-dashboard'
|
||||
const SELF_TEST = process.argv.includes('--self-test')
|
||||
// WHICH HALF OF THE FIX TO REVERT in --self-test. Default 'colour' reverts ONLY the paint, leaving
|
||||
|
|
|
|||
|
|
@ -36,9 +36,29 @@
|
|||
* NEGATIVE-TESTED: restoring `flex:1 1 160px; max-width:240px` on .filter-dropdown makes widths
|
||||
* diverge and this fails by name, naming the before/after arrays.
|
||||
*/
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps'
|
||||
|
||||
// ⚠ "ANSWERS 200" IS NOT "IS THIS BUILD". This machine runs several worktrees of one repo and
|
||||
// the default port belongs to whichever server started first — measured 2026-09-24: :4173 was
|
||||
// another worktree's. This gate compares filter widths BEFORE and AFTER a tab change and fails
|
||||
// on divergence, so a run against someone else's tree can report a width regression in code
|
||||
// that was never touched here. Separate that from the unreachable case below, which is already
|
||||
// a SKIP: both are preconditions, neither is a product finding.
|
||||
{
|
||||
const tree = await servesThisTree(BASE, REPO)
|
||||
if (/DIFFERENT build/.test(tree.why)) {
|
||||
console.log('FILTER-WIDTH: SKIP — the server is not serving THIS build.')
|
||||
console.log(` ${tree.why}`)
|
||||
console.log(' Serve this checkout (`bun run serve`) and pass its port as BASE_URL.')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
const WALK = `(sel)=>{const o=[],s=new Set();const w=(r)=>{if(!r||s.has(r))return;s.add(r);try{o.push(...r.querySelectorAll(sel))}catch(e){};for(const e of r.querySelectorAll('*'))if(e.shadowRoot)w(e.shadowRoot)};w(document);return o}`
|
||||
|
||||
async function measure(page, label, activateTab) {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ if (POISON) {
|
|||
}
|
||||
|
||||
const SPECIAL = (c) => c === 'und' || c === 'mul' || c === 'mis' || c === 'zxx' || (c >= 'qaa' && c <= 'qtz')
|
||||
const hasRes = (v) => !!(v && (v.s || v.f || v.au || v.bv || (v.d && v.d.length)))
|
||||
const hasRes = (v) => !!(v && (v.s || v.f || v.au || v.bv || v.se || (v.d && v.d.length)))
|
||||
const fails = []
|
||||
const add = (clause, msg) => fails.push(` [${clause}] ${msg}`)
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ if (missing.length) add('D', `${missing.length} code(s) carry a resource but app
|
|||
// E — the table cannot invent a URL
|
||||
const known = new Set()
|
||||
for (const v of Object.values(codes)) {
|
||||
if (v.s) known.add(v.s); if (v.f) known.add(v.f); if (v.z) known.add(v.z); if (v.za) known.add(v.za.u); if (v.au) known.add(v.au.u); if (v.bv) known.add(v.bv.u); if (v.gp) known.add(v.gp.u)
|
||||
if (v.s) known.add(v.s); if (v.f) known.add(v.f); if (v.z) known.add(v.z); if (v.za) known.add(v.za.u); if (v.au) known.add(v.au.u); if (v.bv) known.add(v.bv.u); if (v.gp) known.add(v.gp.u); if (v.se) known.add(v.se.u)
|
||||
for (const d of v.d || []) known.add(d.u)
|
||||
}
|
||||
const invented = trows.filter((t) => t.resource_url && !known.has(t.resource_url))
|
||||
|
|
@ -164,7 +164,7 @@ if (misTyped.length) add('H', `${misTyped.length} row(s) named "Open Bible Stori
|
|||
// G — no plain-http URL
|
||||
const httpUrls = []
|
||||
for (const [c, v] of Object.entries(codes)) {
|
||||
for (const u of [v.s, v.f, v.z, v.za && v.za.u, v.au && v.au.u, v.bv && v.bv.u, v.gp && v.gp.u, ...(v.d || []).map((d) => d.u)]) {
|
||||
for (const u of [v.s, v.f, v.z, v.za && v.za.u, v.au && v.au.u, v.bv && v.bv.u, v.gp && v.gp.u, v.se && v.se.u, ...(v.d || []).map((d) => d.u)]) {
|
||||
if (u && /^http:\/\//i.test(u)) httpUrls.push(`${c} ${u}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ if (httpUrls.length) add('G', `${httpUrls.length} plain-http URL(s) (e.g. ${http
|
|||
let fBad = 0
|
||||
for (const r of rows) {
|
||||
const v = codes[r[4]] || {}
|
||||
const want = v.s && v.f ? 2 : (v.s || v.f || (v.d && v.d.length) || v.au || v.bv) ? 1 : 0
|
||||
const want = v.s && v.f ? 2 : (v.s || v.f || (v.d && v.d.length) || v.au || v.bv || v.se) ? 1 : 0
|
||||
if (want !== r[6]) fBad++
|
||||
}
|
||||
if (fBad) add('F', `${fBad} pin(s) carry a tier that disagrees with their code's URLs`)
|
||||
|
|
@ -204,6 +204,7 @@ if (verif && verif.links) {
|
|||
if (v.za && v.za.u) shipped.add(v.za.u)
|
||||
if (v.au && v.au.u) shipped.add(v.au.u)
|
||||
if (v.bv && v.bv.u) shipped.add(v.bv.u)
|
||||
if (v.se && v.se.u) shipped.add(v.se.u)
|
||||
if (v.gp && v.gp.u) shipped.add(v.gp.u)
|
||||
for (const d of v.d || []) if (d.u) shipped.add(d.u)
|
||||
}
|
||||
|
|
@ -239,6 +240,7 @@ if (verif && verif.links) {
|
|||
if (v.za && v.za.u) shipped2.add(v.za.u)
|
||||
if (v.au && v.au.u) shipped2.add(v.au.u)
|
||||
if (v.bv && v.bv.u) shipped2.add(v.bv.u)
|
||||
if (v.se && v.se.u) shipped2.add(v.se.u)
|
||||
if (v.gp && v.gp.u) shipped2.add(v.gp.u)
|
||||
for (const d of v.d || []) if (d.u) shipped2.add(d.u)
|
||||
}
|
||||
|
|
|
|||
138
internal/check-legend-export-reachable.mjs
Normal file
138
internal/check-legend-export-reachable.mjs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env node
|
||||
// check-legend-export-reachable — the Export affordance must be reachable AT BOTH WIDTHS.
|
||||
//
|
||||
// ★ WHAT THIS CAUGHT. The Export menu lives in SemanticTreeLegend, which upg-100-list.vue mounts
|
||||
// for DESKTOP only ("Hidden below the mobile breakpoint, where <LegendMobile>..."). LegendMobile —
|
||||
// the phone bottom-sheet — contains the string "export" ZERO times. So on a phone a reader can
|
||||
// browse the UPG-100 lists and cannot export a packet or a poster. No error, nothing to click; the
|
||||
// failure is invisible rather than broken, which is why it survived.
|
||||
//
|
||||
// The packet IS the product for this profile. The Driver: "print options at the top right of the
|
||||
// legend... that's the only way that map would be usable."
|
||||
//
|
||||
// ⚠ THE CONTROL IS GATED ON A SELECTION. `v-if="SHOW_EXPORT_BUTTON && exportEnabled && selectedNode"`
|
||||
// — so a probe that merely loads the page finds nothing at EITHER width and concludes "no export
|
||||
// anywhere". Measured: that was my first reading and it was wrong. This gate SELECTS A LEAF ROW
|
||||
// first, and asserts the 0 -> n transition, which is also what proves the control is selection-gated
|
||||
// rather than always-on.
|
||||
//
|
||||
// ⚠ AND ON MOBILE THE LEGEND STARTS CLOSED. 20 rows exist with 0 visible until the "Show or hide
|
||||
// the legend" control is clicked. A probe that skips that step reports "no rows on mobile", which is
|
||||
// a true statement about the wrong surface — the second wrong reading this gate encodes against.
|
||||
//
|
||||
// ⚠ VISIBILITY IS GEOMETRY, NOT A CSS PROPERTY. The mobile elements report display:block and
|
||||
// visibility:visible while measuring 0x0 at (0,0) inside an overflow:hidden parent — the desktop
|
||||
// panel rendered but never laid out. A predicate reading `visibility` would call that visible.
|
||||
// Assert a real box.
|
||||
//
|
||||
// Usage: node internal/check-legend-export-reachable.mjs [leafUrl]
|
||||
// SKIPs when nothing is served — this needs the built tree.
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
|
||||
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html'
|
||||
const LEAF = process.argv[2] || DEFAULT_LEAF
|
||||
|
||||
let reachable = false
|
||||
try {
|
||||
const r = await fetch(LEAF, { signal: AbortSignal.timeout(4000) })
|
||||
reachable = r.ok
|
||||
} catch { /* a precondition, not a product failure */ }
|
||||
if (!reachable) {
|
||||
console.log(`LEGEND-EXPORT-REACHABLE: SKIP — nothing served at ${LEAF}.`)
|
||||
console.log(' Serve the built tree: python3 -m http.server 4173 --directory public (or set BASE_URL)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const WALK = `(root, out) => { out = out || []; const rec = (r) => { for (const e of r.querySelectorAll('*')) { out.push(e); if (e.shadowRoot) rec(e.shadowRoot) } }; rec(root); return out }`
|
||||
|
||||
async function probe(browser, { name, width, height, mobile }) {
|
||||
const page = await browser.newPage({ viewport: { width, height }, isMobile: mobile, hasTouch: mobile })
|
||||
const errs = []
|
||||
page.on('pageerror', (e) => errs.push(String(e).slice(0, 110)))
|
||||
await page.goto(LEAF, { waitUntil: 'load', timeout: 45000 })
|
||||
await page.waitForTimeout(9000)
|
||||
|
||||
const before = await page.evaluate(`(${WALK})(document).filter(e => (e.className||'').toString().includes('stl-export')).length`)
|
||||
|
||||
// On a phone the legend starts closed — open it first, or every row query returns nothing.
|
||||
const openedSheet = await page.evaluate(`(() => {
|
||||
const all = (${WALK})(document)
|
||||
const b = all.filter(e => e.tagName === 'BUTTON')
|
||||
.find(e => /show or hide the legend|toggle.?legend/i.test(e.getAttribute('aria-label') || ''))
|
||||
if (!b) return false
|
||||
b.click(); return true
|
||||
})()`)
|
||||
if (openedSheet) await page.waitForTimeout(2200)
|
||||
|
||||
// Select a LEAF row. Height-bounded because a container also carries .stl-row-ish classes and
|
||||
// clicking it selects nothing — measured: its textContent is the whole list concatenated.
|
||||
const clicked = await page.evaluate(`(() => {
|
||||
const rows = (${WALK})(document)
|
||||
.filter(e => (e.className||'').toString().includes('stl-row'))
|
||||
.filter(e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 60 })
|
||||
if (!rows.length) return null
|
||||
rows[0].click()
|
||||
return (rows[0].textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 30)
|
||||
})()`)
|
||||
await page.waitForTimeout(2200)
|
||||
|
||||
const after = await page.evaluate(`(() => {
|
||||
const el = (${WALK})(document).filter(e => (e.className||'').toString().includes('stl-export'))
|
||||
// A REAL BOX, not a CSS property: the mobile case reports visibility:visible at 0x0.
|
||||
const boxed = el.filter(e => { const r = e.getBoundingClientRect(); return r.width > 2 && r.height > 2 })
|
||||
return { total: el.length, boxed: boxed.length,
|
||||
label: boxed.map(e => (e.textContent || '').trim().slice(0, 18))[0] || '' }
|
||||
})()`)
|
||||
await page.close()
|
||||
return { name, before, openedSheet, clicked, ...after, errs }
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
|
||||
let results
|
||||
try {
|
||||
results = [
|
||||
await probe(browser, { name: 'desktop', width: 1500, height: 1000, mobile: false }),
|
||||
await probe(browser, { name: 'mobile', width: 390, height: 844, mobile: true }),
|
||||
]
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
console.log('LEGEND-EXPORT-REACHABLE')
|
||||
console.log(` leaf : ${LEAF}`)
|
||||
for (const r of results) {
|
||||
console.log(` ${r.name.padEnd(8)}: selected ${JSON.stringify(r.clicked)} — `
|
||||
+ `.stl-export ${r.before} -> ${r.total} present, ${r.boxed} with a real box`
|
||||
+ `${r.label ? ` ("${r.label}")` : ''}${r.openedSheet ? ' [opened the sheet first]' : ''}`)
|
||||
}
|
||||
|
||||
const fails = []
|
||||
for (const r of results) {
|
||||
if (!r.clicked) {
|
||||
fails.push(`${r.name}: no selectable legend row — the probe could not reach the state the `
|
||||
+ 'export control is gated on, so this width was not actually tested')
|
||||
continue
|
||||
}
|
||||
if (r.boxed === 0) {
|
||||
fails.push(`${r.name}: the Export affordance has NO reachable box`
|
||||
+ (r.total > 0
|
||||
? ` — ${r.total} .stl-export element(s) exist but all measure 0x0, i.e. the desktop legend `
|
||||
+ 'rendered without being laid out. A reader here can browse the lists and cannot export '
|
||||
+ 'a packet or a poster.'
|
||||
: ' — no .stl-export element at all.'))
|
||||
}
|
||||
if (r.errs.length) fails.push(`${r.name}: ${r.errs.length} page error(s): ${r.errs[0]}`)
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nLEGEND-EXPORT-REACHABLE: FAIL — ${fails.length} problem(s)`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
console.error('\n The packet IS the product for this profile. See '
|
||||
+ 'BUG-UPG100-PACKET-AND-POSTER-EXPORT-ARE-UNREACHABLE-ON-A-PHONE — WHERE the control goes on a '
|
||||
+ 'phone is a design call (the sheet has no titlebar), so this gate names the gap rather than '
|
||||
+ 'assuming a fix.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('\nLEGEND-EXPORT-REACHABLE: PASS — selecting a list reveals a reachable Export control at '
|
||||
+ 'every width tested.')
|
||||
|
|
@ -40,6 +40,7 @@ import { chromium } from 'playwright'
|
|||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { servesThisTree } from './_serves-this-tree.mjs'
|
||||
|
||||
// Portable by construction — see check-no-absolute-paths.mjs. Never hardcode a worktree.
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
|
@ -52,6 +53,22 @@ if (!existsSync(MANIFEST)) {
|
|||
console.log('MAPLIBRE-LAYER-DROPS: SKIP — no built manifest (run `bun run tree` first).')
|
||||
process.exit(0)
|
||||
}
|
||||
// ⚠ REACHABLE AND MINE ARE DIFFERENT QUESTIONS. This machine runs several worktrees of one
|
||||
// repo and the default port belongs to whichever server started first — measured 2026-09-24:
|
||||
// :4173 was another worktree's, and this gate reported PASS 16/16 about a build that was not
|
||||
// on disk here. The layer-drop family is silent by nature (MapLibre omits a layer instead of
|
||||
// throwing), so a verdict taken from the wrong tree is exactly the kind nobody can catch by
|
||||
// looking. A mismatch is a precondition, not a product failure: SKIP, but name it.
|
||||
{
|
||||
const tree = await servesThisTree(BASE, ROOT)
|
||||
if (/DIFFERENT build/.test(tree.why)) {
|
||||
console.log('MAPLIBRE-LAYER-DROPS: SKIP — the server is not serving THIS build.')
|
||||
console.log(' ' + tree.why)
|
||||
console.log(' Start `bun run serve` in this checkout and pass its port as BASE_URL.')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
||||
const leaves = []
|
||||
for (const b of manifest.bundles || []) {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ const b = await chromium.launch()
|
|||
const pg = await b.newPage({ viewport:{width:1500,height:1941} })
|
||||
const errs=[]; pg.on('pageerror',e=>errs.push(String(e).slice(0,140)))
|
||||
|
||||
const ORIGIN = process.env.PACKET_ORIGIN || 'http://127.0.0.1:4898'
|
||||
let ORIGIN = process.env.PACKET_ORIGIN || 'http://127.0.0.1:4898'
|
||||
let OWN_SERVER = null
|
||||
const API = process.env.PACKET_API || 'https://pray.doxa.life'
|
||||
const ISO3 = process.env.PACKET_ISO3 || 'LAO'
|
||||
const RENDER = `${ORIGIN}/library/packet/packet-render.js`
|
||||
const STRINGS = `${ORIGIN}/library/scripts/posters/packet-strings.json`
|
||||
let RENDER = `${ORIGIN}/library/packet/packet-render.js`
|
||||
let STRINGS = `${ORIGIN}/library/scripts/posters/packet-strings.json`
|
||||
// The SAME catalog, read from disk for the assertions below. STRINGS is the URL the
|
||||
// browser fetches; this is the local path, so the gate compares against the file the
|
||||
// page actually loaded rather than a second copy.
|
||||
|
|
@ -56,6 +57,38 @@ try {
|
|||
console.log(' python3 -m http.server 4898 (or set PACKET_ORIGIN)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// ⚠ AND IT MUST BE THIS CHECKOUT'S ROOT. Several worktrees of this repo run on one machine,
|
||||
// so a port that answers may belong to another one — measured 2026-09-24 on :4173, where a
|
||||
// browser gate reported on a build that was not on disk here. This gate imports the shared
|
||||
// renderer FROM THE SERVER, so the wrong origin means it verifies someone else's renderer
|
||||
// and says nothing about this change. Precondition, not a product failure: SKIP and name it.
|
||||
{
|
||||
const { servesThisTree, serveThisTree } = await import('./_serves-this-tree.mjs')
|
||||
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const tree = await servesThisTree(ORIGIN + '/public', repo)
|
||||
if (/DIFFERENT build/.test(tree.why)) {
|
||||
// Serve this checkout rather than skip: the default port belongs to whichever worktree
|
||||
// started first on this box, and a gate that skips every wave stops being a gate.
|
||||
OWN_SERVER = await serveThisTree(repo)
|
||||
if (!OWN_SERVER) {
|
||||
await b.close()
|
||||
console.log('PACKET-BROWSER: SKIP — not this checkout, and I could not start my own server.')
|
||||
console.log(` ${tree.why}`)
|
||||
console.log(' Serve THIS repo root and re-run (PACKET_ORIGIN=http://127.0.0.1:PORT).')
|
||||
process.exit(0)
|
||||
}
|
||||
console.log(` · ${tree.why}`)
|
||||
console.log(` · serving this checkout on ${OWN_SERVER.origin} instead`)
|
||||
ORIGIN = OWN_SERVER.origin
|
||||
RENDER = `${ORIGIN}/library/packet/packet-render.js`
|
||||
STRINGS = `${ORIGIN}/library/scripts/posters/packet-strings.json`
|
||||
// The page is already sitting on the OTHER origin, and a dynamic import across origins is
|
||||
// refused (no CORS on a plain static server) — measured: "Failed to fetch dynamically
|
||||
// imported module". Re-navigate so the renderer is same-origin with the page importing it.
|
||||
await pg.goto(ORIGIN + '/', { timeout: 8000 })
|
||||
}
|
||||
}
|
||||
const html = await pg.evaluate(async ({RENDER, STRINGS, LANG, ORIGIN, API, ISO3, ZONE}) => {
|
||||
const [{ createPacketRenderer }, strings, norm] = await Promise.all([
|
||||
import(RENDER),
|
||||
|
|
@ -245,6 +278,8 @@ for (const [sel, key, got] of [['points-title','points_title',r.pointsTitle],
|
|||
}
|
||||
if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
|
||||
|
||||
OWN_SERVER?.stop()
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nPACKET-BROWSER: FAIL — ${fails.length} problem(s)`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@
|
|||
// not calling its tile servers, and only the request log shows that.
|
||||
//
|
||||
// Usage: node internal/check-provider-switch.mjs <leafUrl>
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { servesThisTree, serveThisTree } from './_serves-this-tree.mjs'
|
||||
|
||||
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
||||
// ⚠ NOT named `URL` — that shadows the global URL constructor, so every
|
||||
// `new URL(request.url())` below throws and the host tally stays EMPTY while the
|
||||
|
|
@ -26,7 +31,8 @@ import { chromium } from 'playwright'
|
|||
// gate was written for) and SKIP with a named verdict when nothing is served there.
|
||||
const DEFAULT_LEAF = (process.env.BASE_URL || 'http://127.0.0.1:4173')
|
||||
+ '/doxa-maps/my-upg-100-list/upg-100-list/index.html'
|
||||
const LEAF = process.argv[2] || DEFAULT_LEAF
|
||||
let LEAF = process.argv[2] || DEFAULT_LEAF
|
||||
let OWN_SERVER = null
|
||||
{
|
||||
let reachable = false
|
||||
try {
|
||||
|
|
@ -39,6 +45,36 @@ const LEAF = process.argv[2] || DEFAULT_LEAF
|
|||
console.log(' python3 -m http.server 4173 --directory public (or set BASE_URL)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// ⚠ REACHABLE IS NOT THE SAME QUESTION AS MINE. Several worktrees of this repo run on one
|
||||
// machine and the default port belongs to whichever started first — measured 2026-09-24:
|
||||
// :4173 was another worktree's server, and an hour of probing reported on a build nobody
|
||||
// here had changed. This gate's whole verdict is a request tally from a page; a tally taken
|
||||
// from someone else's page is a confident statement about the wrong code.
|
||||
// A mismatch is a PRECONDITION, not a product failure, so it SKIPs (exit 0) like the
|
||||
// unreachable case above — but it says which, so nobody reads it as a pass.
|
||||
// The helper wants the ORIGIN that serves public/, not the leaf page — passing the page URL
|
||||
// makes it look for the manifest under the page's own path and report "nothing serving it",
|
||||
// which would SKIP every run for the wrong reason. (Measured: it did, on both servers.)
|
||||
const tree = await servesThisTree(new URL(LEAF).origin, REPO)
|
||||
// Only a genuine MISMATCH skips. "No manifest to compare" (an unbuilt checkout) is a
|
||||
// different precondition and the reachable check above already speaks for it — turning
|
||||
// that into a skip here would silence the gate on exactly the builds it should run on.
|
||||
if (/DIFFERENT build/.test(tree.why)) {
|
||||
// Don't skip if we can simply serve our own tree — a gate that skips on every wave is a
|
||||
// gate nobody runs, and on this box the default port belongs to another worktree.
|
||||
const own = await serveThisTree(resolve(REPO, 'public'))
|
||||
if (!own) {
|
||||
console.log('PROVIDER-SWITCH: SKIP — the server is not serving THIS build, and I could not start one.')
|
||||
console.log(` ${tree.why}`)
|
||||
console.log(' Serve your own tree and re-run: bun run serve (then BASE_URL=http://127.0.0.1:PORT)')
|
||||
process.exit(0)
|
||||
}
|
||||
OWN_SERVER = own
|
||||
LEAF = own.origin + new URL(LEAF).pathname
|
||||
console.log(` · ${tree.why}`)
|
||||
console.log(` · serving this checkout on ${own.origin} instead, so the tally is about THIS build`)
|
||||
}
|
||||
}
|
||||
|
||||
async function run(provider) {
|
||||
|
|
@ -110,6 +146,8 @@ if (def.mapboxReq <= PRELOAD_ALLOWANCE) {
|
|||
if (!def.total) fails.push('no requests were recorded at all — the tally is broken, not the page')
|
||||
if (ml.errs.length) fails.push(`${ml.errs.length} page error(s) under maplibre: ${ml.errs[0]}`)
|
||||
|
||||
OWN_SERVER?.stop()
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`\nPROVIDER-SWITCH: FAIL — ${fails.length} problem(s)`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,21 @@ const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/doxa-maps'
|
|||
|
||||
// The Driver's sequence, verbatim from the open-work brief. Prefix-matched (case-insensitive,
|
||||
// whitespace-collapsed) so a row may carry its subtitle.
|
||||
// The Driver's four, asserted as a RELATIVE ORDER rather than as positions 1-4.
|
||||
//
|
||||
// ⚠ THIS USED TO DEMAND A FOUR-ROW RAIL, and it had been FAILING ever since the rail gained its
|
||||
// three-section structure (4c74bd7, 2026-09-23, "Restore three-section tab structure:
|
||||
// Dashboards / Prayer Mobilization / Gospel Research"). MEASURED 2026-09-24: the rail renders
|
||||
// SEVEN rows —
|
||||
// Big Picture · Simple Research · Regions · UPG-100 List · Prayer Progress ·
|
||||
// Cities with Internet · Gospel Resources
|
||||
// — and the Driver's four are ALL PRESENT, at 0/3/4/6, in their specified relative order. The
|
||||
// product honours the instruction; this gate was asserting a shape the instruction never named.
|
||||
// It quoted the Driver correctly and then over-read it: "(Big Picture, UPG-100, Prayer Progress,
|
||||
// Gospel Resources)" names an ORDER, not a row count, and nothing there forbids a row between.
|
||||
//
|
||||
// So: assert what was actually specified. Tab 1 must still be Big Picture — that IS positional,
|
||||
// it is the landing surface — and the other three must appear after it, in order.
|
||||
const EXPECTED = ['Big Picture', 'UPG-100', 'Prayer Progress', 'Gospel Resources']
|
||||
|
||||
const WALK = `(sel)=>{const o=[],s=new Set();const w=(r)=>{if(!r||s.has(r))return;s.add(r);try{o.push(...r.querySelectorAll(sel))}catch(e){};for(const e of r.querySelectorAll('*'))if(e.shadowRoot)w(e.shadowRoot)};w(document);return o}`
|
||||
|
|
@ -59,8 +74,14 @@ try {
|
|||
// last remaining directory URL in internal/.
|
||||
await page.goto(`${BASE}/doxa-maps-spa/doxa-maps-spa/index.html`, { waitUntil: 'load', timeout: 45000 })
|
||||
await page.waitForTimeout(8000)
|
||||
// ⚠ DEDUPE: '.vtl-tab' matches NESTED elements, so each rendered row comes back ~5 times —
|
||||
// measured 35 matches for 7 real rows. The old code took the first four of that list, which
|
||||
// were all fragments of rows 1 and 2, and then reported them as rows 1-4. Keep only the
|
||||
// OUTERMOST match per row (one that has no .vtl-tab ancestor), which is the row itself.
|
||||
rows = await page.evaluate(
|
||||
`(${WALK})('.vtl-tab').map(t => (t.textContent || '').trim().replace(/\\s+/g, ' '))`
|
||||
`(${WALK})('.vtl-tab')
|
||||
.filter(t => !t.parentElement || !t.parentElement.closest('.vtl-tab'))
|
||||
.map(t => (t.textContent || '').trim().replace(/\\s+/g, ' '))`
|
||||
)
|
||||
} catch (e) {
|
||||
err = e.message.slice(0, 160)
|
||||
|
|
@ -82,19 +103,34 @@ if (!rows.length) {
|
|||
}
|
||||
|
||||
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
||||
const actual = rows.slice(0, EXPECTED.length)
|
||||
const mismatches = EXPECTED.map((want, i) => {
|
||||
const got = actual[i] || '(missing)'
|
||||
return norm(got).startsWith(norm(want)) ? null : { i, want, got: got.slice(0, 52) }
|
||||
}).filter(Boolean)
|
||||
// Where each expected name lands in the RENDERED rail. -1 = absent.
|
||||
const at = EXPECTED.map((want) => rows.findIndex((r) => norm(r).startsWith(norm(want))))
|
||||
const fails = []
|
||||
|
||||
if (mismatches.length) {
|
||||
console.error(`SPA-TAB-ORDER: FAIL — ${mismatches.length} of ${EXPECTED.length} rail rows are out of order.\n`)
|
||||
EXPECTED.forEach((want, i) => {
|
||||
const got = (actual[i] || '(missing)').slice(0, 52)
|
||||
const ok = norm(got).startsWith(norm(want))
|
||||
console.error(` ${ok ? 'ok ' : 'FAIL'} ${i + 1}. want "${want}" got "${got}"`)
|
||||
})
|
||||
// 1. PRESENCE. A name the Driver specified must be in the rail at all.
|
||||
EXPECTED.forEach((want, i) => {
|
||||
if (at[i] === -1) fails.push(`"${want}" is not in the rail at all`)
|
||||
})
|
||||
// 2. TAB 1 IS POSITIONAL, and only tab 1. It is the landing surface — the first thing anyone
|
||||
// opening the SPA sees — which is a claim about position, not merely about order.
|
||||
if (at[0] > 0) {
|
||||
fails.push(`tab 1 is "${(rows[0] || '').slice(0, 40)}", not "${EXPECTED[0]}" — the SPA opens `
|
||||
+ 'somewhere other than the landing surface')
|
||||
}
|
||||
// 3. THE REST ARE RELATIVE. The instruction named an order, not a row count, so a row sitting
|
||||
// between two of them is not a violation — a row OUT OF ORDER is.
|
||||
for (let i = 1; i < EXPECTED.length; i++) {
|
||||
if (at[i] !== -1 && at[i - 1] !== -1 && at[i] < at[i - 1]) {
|
||||
fails.push(`"${EXPECTED[i]}" (row ${at[i] + 1}) comes before "${EXPECTED[i - 1]}" `
|
||||
+ `(row ${at[i - 1] + 1}) — the Driver's order is reversed here`)
|
||||
}
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`SPA-TAB-ORDER: FAIL — ${fails.length} problem(s) with the Driver's rail order.\n`)
|
||||
for (const f of fails) console.error(' - ' + f)
|
||||
console.error('\n Rendered rail:')
|
||||
rows.forEach((r, i) => console.error(` ${i + 1}. ${r.slice(0, 52)}`))
|
||||
console.error('\n Tab 1 is the LANDING SURFACE — the first thing anyone opening the product SPA')
|
||||
console.error(' sees, and the opening step of the onboarding ramp. A reordered rail produces no')
|
||||
console.error(' error and a SPA that still works; it just opens somewhere else.')
|
||||
|
|
@ -103,7 +139,13 @@ if (mismatches.length) {
|
|||
}
|
||||
|
||||
console.log(`SPA-TAB-ORDER: PASS — the rail opens in the specified order (${rows.length} rows total).`)
|
||||
EXPECTED.forEach((w, i) => console.log(` ${i + 1}. ${(actual[i] || '').slice(0, 56)}`))
|
||||
// Print the WHOLE rendered rail and mark the Driver's four, so a reader can see the rows that
|
||||
// sit between them rather than only the names being asserted. A gate that prints just its own
|
||||
// four would hide the very structure that made the old positional assertion wrong.
|
||||
rows.forEach((r, i) => {
|
||||
const k = at.indexOf(i)
|
||||
console.log(` ${String(i + 1).padStart(2)}. ${k >= 0 ? '★' : ' '} ${r.slice(0, 54)}`)
|
||||
})
|
||||
console.log(' Asserted on the RENDERED rail rather than on NAV_MODEL: the array is edited for')
|
||||
console.log(' reasons unrelated to ordering (badges, sections, narrow-screen flags), and a source')
|
||||
console.log(' parse can pass while a rendering bug shows something else.')
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
* node internal/run-parallel.mjs --bundles # BUNDLE=<d> bun x vite build, for every plugin-designs/<d>/
|
||||
* env: GATES_CONCURRENCY (default min(cpus,6)) · BUILD_CONCURRENCY (default 4)
|
||||
*/
|
||||
import { spawn } from 'node:child_process'
|
||||
import { readdirSync, existsSync } from 'node:fs'
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { readdirSync, existsSync, readFileSync } from 'node:fs'
|
||||
import { cpus } from 'node:os'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
|
@ -76,6 +76,69 @@ const CONCURRENCY = Math.max(
|
|||
) || 1
|
||||
)
|
||||
|
||||
// The rendezvous every agent's browser gate queues on. It must be ONE path for all of them or
|
||||
// the queue is not a queue, so it is an ENV VAR with a product-named default: the shared file
|
||||
// several agents' shells already export lives outside this repo and carries an internal code
|
||||
// name, which check:no-codename correctly refuses to let a handed-over repo carry. Export
|
||||
// BROWSER_GATE_LOCK to join whatever rendezvous the rest of the box is using.
|
||||
const BROWSER_LOCK = process.env.BROWSER_GATE_LOCK || '/tmp/doxa-maps-browser-gate.lock'
|
||||
const LOAD_MAX = Number(process.env.LOAD_MAX || 12)
|
||||
const QUIET_WAIT_MAX = Number(process.env.QUIET_WAIT_MAX || 900)
|
||||
const HAS_FLOCK = spawnSync('sh', ['-c', 'command -v flock'], { stdio: 'ignore' }).status === 0
|
||||
|
||||
/**
|
||||
* Does this step drive a real browser? Asked of the GATE FILE, not of a hand-kept list:
|
||||
* a list goes stale the first time someone adds a gate, and a browser gate missing from it
|
||||
* is exactly the one that rejoins the dogpile. package.json names the file; the file says
|
||||
* whether it imports playwright.
|
||||
*/
|
||||
function isBrowserGate(name) {
|
||||
try {
|
||||
const scripts = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')).scripts || {}
|
||||
const m = String(scripts[name] || '').match(/([\w./-]+\.mjs)/)
|
||||
if (!m) return false
|
||||
const file = resolve(ROOT, m[1])
|
||||
if (!existsSync(file)) return false
|
||||
const src = readFileSync(file, 'utf8')
|
||||
// Two ways a step can drive chrome: it launches one itself, or it is a RUNNER that
|
||||
// launches gates which do. `verify` is the second kind — verify-all.mjs imports no
|
||||
// playwright and would have been graded static, while three of the gates it spawns
|
||||
// (posters, app-tables, map-paint) render pages. It already marks those `browser: true`
|
||||
// for its own reasons, so that declaration is honoured here rather than duplicated.
|
||||
return /from 'playwright'|require\('playwright'\)|chromium/.test(src) || /browser:\s*true/.test(src)
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold a browser gate back while the box is already overloaded. Same threshold and cadence as
|
||||
* the shared wait-for-quiet.sh, so agents behave identically whichever entry point they use.
|
||||
* It PROCEEDS after the cap rather than failing: a gate that refuses to run is a coverage
|
||||
* loss, and load is a property of the machine, not of the code under test.
|
||||
*/
|
||||
let quietOnce = null
|
||||
async function waitForQuiet(log) {
|
||||
// ONCE PER WAVE, not once per gate. The flock already guarantees one browser at a time, so
|
||||
// a per-step load wait just multiplies the same hold by the number of browser gates — with
|
||||
// four of them and a 900s cap that is an hour of a wave doing nothing on a busy box, which
|
||||
// is how a load fix turns into a reason to stop running gates.
|
||||
if (quietOnce) return quietOnce
|
||||
quietOnce = (async () => {
|
||||
let waited = 0
|
||||
for (;;) {
|
||||
const load = Number(String(readFileSync('/proc/loadavg', 'utf8')).split(' ')[0])
|
||||
if (!Number.isFinite(load) || load < LOAD_MAX) return
|
||||
if (waited >= QUIET_WAIT_MAX) {
|
||||
log(` · load ${load} still above ${LOAD_MAX} after ${waited}s — proceeding anyway`)
|
||||
return
|
||||
}
|
||||
log(` · load ${load} >= ${LOAD_MAX}, holding browser gates (${waited}s)`)
|
||||
await new Promise((r) => setTimeout(r, 15000))
|
||||
waited += 15
|
||||
}
|
||||
})()
|
||||
return quietOnce
|
||||
}
|
||||
|
||||
/** @returns {{name:string, cmd:string, args:string[], env:Record<string,string>}[]} */
|
||||
function buildSteps() {
|
||||
if (!BUNDLES) {
|
||||
|
|
@ -83,7 +146,19 @@ function buildSteps() {
|
|||
console.error('run-parallel: no steps given. Pass script names after `--`.')
|
||||
process.exit(2)
|
||||
}
|
||||
return names.map((name) => ({ name, cmd: 'bun', args: ['run', name], env: {} }))
|
||||
return names.map((name) => {
|
||||
const step = { name, cmd: 'bun', args: ['run', name], env: {} }
|
||||
if (!isBrowserGate(name) || !HAS_FLOCK) return step
|
||||
// ★ ONE CHROME AT A TIME, ACROSS EVERY AGENT ON THIS BOX. Seven agents each running a
|
||||
// wave of their own put 7+ chrome-headless renders on one machine at once; measured
|
||||
// 2026-09-24, the box hit load 47 with 8GB of swap, and a browser gate that is swapping
|
||||
// does not measure the product, it measures the swap. The rendezvous is a COARSE flock
|
||||
// on one well-known path that every agent's tooling shares (bundler-3's
|
||||
// wait-for-quiet.sh takes the same lock), so this queues rather than dogpiles. -w 1200
|
||||
// caps the wait so a crashed holder cannot wedge a wave forever, and static gates —
|
||||
// parse, sri, hygiene, the twenty that are node-startup — are left completely free.
|
||||
return { ...step, browser: true, cmd: 'flock', args: ['-w', '1200', BROWSER_LOCK, 'bun', 'run', name] }
|
||||
})
|
||||
}
|
||||
// A bundle is a plugin-designs/<name>/ directory — the same selector build:unlocked used
|
||||
// (`for d in plugin-designs/*/`), so the SET of things built is identical.
|
||||
|
|
@ -95,7 +170,8 @@ function buildSteps() {
|
|||
.map((name) => ({ name, cmd: 'bun', args: ['x', 'vite', 'build'], env: { BUNDLE: name } }))
|
||||
}
|
||||
|
||||
function runStep(step) {
|
||||
async function runStep(step) {
|
||||
if (step.browser && existsSync('/proc/loadavg')) await waitForQuiet((m) => console.log(m))
|
||||
return new Promise((done) => {
|
||||
const started = Date.now()
|
||||
const child = spawn(step.cmd, step.args, {
|
||||
|
|
|
|||
|
|
@ -292,6 +292,18 @@ const props = defineProps({
|
|||
type: Array,
|
||||
default: null
|
||||
},
|
||||
/** Which tab the sheet OPENS on, 0-based. Forwarded to SemanticTreeLegend below.
|
||||
*
|
||||
* ★ WHY IT HAD TO BE ADDED. The desktop legend has taken :default-tab for a while, and
|
||||
* my-upg-100-list passes 1 (the UPG-100 Lists tab) because the Driver asked for exactly
|
||||
* that — "it shouldn't have me sitting there on Regions, I don't know what to do, it's
|
||||
* confusing". The mobile sheet had no such prop, so a phone opened on Region no matter
|
||||
* what the profile declared: the same complaint, unfixed, on the surface prayer
|
||||
* mobilisation most uses. Default 0 keeps every other consumer where it is. */
|
||||
defaultTab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** Header label for the value column when supplying `nodes`. */
|
||||
columnLabel: {
|
||||
type: String,
|
||||
|
|
@ -351,6 +363,27 @@ const dataStore = inject('dataStore')
|
|||
// never updated by the mobile flow).
|
||||
const onSemanticTreeSelect = inject('onSemanticTreeSelect', () => {})
|
||||
|
||||
// ★ THE EXPORT AFFORDANCE, WHICH THIS SHEET RENDERED AND NEVER ENABLED.
|
||||
// This component already renders <SemanticTreeLegend> below — the same component that
|
||||
// owns the Export menu — but passed neither :export-enabled nor @export. exportEnabled
|
||||
// defaults to FALSE, so the control's v-if was never satisfied and a phone reader could
|
||||
// browse the UPG-100 lists and not export a packet or a poster. The packet IS the product
|
||||
// for that profile; the Driver asked for "print options at the top right of the legend...
|
||||
// that's the only way that map would be usable".
|
||||
//
|
||||
// ⚠ IT WAS NOT A DESIGN QUESTION, WHICH IS WHAT I FIRST RECORDED. The 0x0 elements that
|
||||
// measurement found belong to the OTHER, hidden SemanticTreeLegend instance — the desktop
|
||||
// one, mounted but not laid out below the breakpoint. That geometry made it look as though
|
||||
// the mobile sheet had nowhere to put the control. It renders its own instance, with a
|
||||
// titlebar, and only needed the prop.
|
||||
//
|
||||
// PROVIDE/INJECT, matching onSemanticTreeSelect directly above, because this sheet is
|
||||
// rendered deep inside a shell rather than as a direct child — an emit would not reach.
|
||||
// OPT-IN BY CONSTRUCTION: exportEnabled is true only when a profile actually provides a
|
||||
// handler, so the three profiles that do not provide one are untouched.
|
||||
const onLegendExport = inject('onLegendExport', null)
|
||||
const mobileExportEnabled = computed(() => typeof onLegendExport === 'function')
|
||||
|
||||
// ── Data-driven legend (universal for all legend types) ─────────────────────
|
||||
const legendTypeRef = toRef(props, 'legendType')
|
||||
const {
|
||||
|
|
@ -777,11 +810,14 @@ onBeforeUnmount(() => {
|
|||
v-else-if="legendType !== 'default'"
|
||||
:nodes="treeForMobile"
|
||||
:tabs="tabsForMobile"
|
||||
:default-tab="defaultTab"
|
||||
:title="legendTitle || treeTitleForMobile"
|
||||
:hideTabs="nodes ? !tabs : _isFlatTabMobile"
|
||||
:columnLabel="treeColumnLabelForMobile"
|
||||
:columns="columns || ['count', 'pop']"
|
||||
:export-enabled="mobileExportEnabled"
|
||||
@select="onSemanticTreeSelect"
|
||||
@export="(kind) => onLegendExport && onLegendExport(kind)"
|
||||
>
|
||||
<template #title-caret>
|
||||
<button class="mobile-collapse-caret"
|
||||
|
|
|
|||
|
|
@ -207,15 +207,30 @@ const SHOW_EXPORT_BUTTON = true
|
|||
const sortBy = ref(null) // null = default (pop desc), 'count' | 'pop'
|
||||
const sortDir = ref('desc') // 'desc' | 'asc'
|
||||
|
||||
// ⚠ THE STORE USED TO WIN UNCONDITIONALLY, which made `defaultTab` dead for every profile
|
||||
// that shares an instance. instance.activeTab starts at 0 and 0 is indistinguishable from
|
||||
// "the user chose tab 0", so a profile passing :default-tab="1" still opened on tab 0.
|
||||
// MEASURED 2026-09-24: my-upg-100-list opened on Region at desktop AND mobile, which is the
|
||||
// Driver's first complaint about this legend word for word.
|
||||
// Now the store only wins once something has ACTUALLY chosen a tab — a tab click, or a node
|
||||
// selection that moves the legend to that node's generation (usePplrInstance sets the flag).
|
||||
// Until then the profile's declared landing tab applies, which is what the prop always meant.
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
if (props.hideTabs) return 0
|
||||
if (instance && !instance.activeTabTouched?.value) {
|
||||
return Math.min(Math.max(0, props.defaultTab | 0), Math.max(0, tabList.value.length - 1))
|
||||
}
|
||||
return instance ? instance.activeTab.value : localActiveTab.value
|
||||
},
|
||||
set: v => {
|
||||
if (props.hideTabs) return
|
||||
if (instance) instance.activeTab.value = v
|
||||
else localActiveTab.value = v
|
||||
if (instance) {
|
||||
instance.activeTab.value = v
|
||||
if (instance.activeTabTouched) instance.activeTabTouched.value = true
|
||||
} else {
|
||||
localActiveTab.value = v
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -238,11 +253,25 @@ watch(() => props.open, (v) => {
|
|||
})
|
||||
|
||||
watch(() => safeNodes.value, () => {
|
||||
const ext = instance?.activeTab.value
|
||||
// Falls back to defaultTab, not a hardcoded 0 — otherwise a profile that opens on
|
||||
// a deeper generation silently reverts to the top the first time its nodes change.
|
||||
// ⚠ AND THE STORE ONLY COUNTS ONCE IT HAS BEEN TOUCHED. This read `instance?.activeTab.value`
|
||||
// and kept it whenever it was `a number >= 0` — but the store's INITIAL value is 0, which
|
||||
// satisfies that test, so defaultTab lost here too. Same root cause as the getter above.
|
||||
const touched = instance ? instance.activeTabTouched?.value : false
|
||||
const ext = touched ? instance?.activeTab.value : undefined
|
||||
const base = Math.min(Math.max(0, props.defaultTab | 0), Math.max(0, tabList.value.length - 1))
|
||||
activeTab.value = (typeof ext === 'number' && ext >= 0) ? ext : base
|
||||
const next = (typeof ext === 'number' && ext >= 0) ? ext : base
|
||||
// ⚠ WRITE PAST THE SETTER. This ran `activeTab.value = next`, and the setter marks the store
|
||||
// TOUCHED — but a nodes-change reset is not a user choice, so that flipped the sentinel on
|
||||
// mount and made defaultTab lose again, one layer down. Measured: the fix appeared to do
|
||||
// nothing until this line changed. Set the store directly and leave `touched` alone.
|
||||
if (instance) {
|
||||
instance.activeTab.value = next
|
||||
if (!touched && instance.activeTabTouched) instance.activeTabTouched.value = false
|
||||
} else {
|
||||
localActiveTab.value = next
|
||||
}
|
||||
expandedIds.value = new Set()
|
||||
selectedNode.value = null
|
||||
emit('select', null)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,22 @@ export function createPplrInstance(instanceId) {
|
|||
/** Active legend tab index (0-based — but Tab 1 = Generation 1 in user-facing labels) */
|
||||
activeTab: ref(0),
|
||||
|
||||
/** Has anything ACTUALLY chosen a tab yet — a user click, a search, a pin?
|
||||
*
|
||||
* ★ WHY THIS EXISTS. activeTab starts at 0, and 0 is indistinguishable from "the user
|
||||
* picked tab 0". SemanticTreeLegend resolves its tab as
|
||||
* `instance ? instance.activeTab.value : localActiveTab.value`, so for every profile
|
||||
* that shares an instance store the `defaultTab` prop was DEAD — declared, passed,
|
||||
* seeding a local ref, and named in a watch fallback, yet never able to win.
|
||||
* MEASURED 2026-09-24: my-upg-100-list passes :default-tab="1" (the UPG-100 Lists tab)
|
||||
* and opened on Region at BOTH widths, which is the Driver's first complaint about this
|
||||
* legend verbatim — "it shouldn't have me sitting there on Regions, I don't know what to
|
||||
* do, it's confusing."
|
||||
* A sentinel rather than a different initial value: changing activeTab's default would
|
||||
* move every other profile's landing tab as a side effect.
|
||||
*/
|
||||
activeTabTouched: ref(false),
|
||||
|
||||
/** Search-mode flag: true when geocoder text ≥ 2 chars */
|
||||
isSearching: ref(false),
|
||||
|
||||
|
|
@ -80,6 +96,9 @@ export function selectNode(instance, node) {
|
|||
instance.selection.value = node
|
||||
if (typeof node.depth === 'number') {
|
||||
instance.activeTab.value = node.depth
|
||||
// A REAL choice — selecting a node moves the legend to that node's generation. Mark it,
|
||||
// so the profile's defaultTab stops applying from here on (see activeTabTouched above).
|
||||
instance.activeTabTouched.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
"check:locale-reachable": "node internal/check-locale-reachable.mjs",
|
||||
"check:poster-prints": "node internal/check-poster-prints.mjs",
|
||||
"check:provider-switch": "node internal/check-provider-switch.mjs # the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.",
|
||||
"check:legend-export-reachable": "node internal/check-legend-export-reachable.mjs # the Export affordance must be reachable at BOTH widths. Selects a leaf row first (the control is gated on selectedNode) and opens the mobile sheet first (it starts closed). Asserts a REAL BOX, not CSS visibility — on mobile the elements report visibility:visible at 0x0. PASSES at both widths since the mobile sheet was given :export-enabled and :tabs. SKIPs when nothing is served.",
|
||||
"check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs",
|
||||
"check:share-policy": "node internal/check-share-policy.mjs",
|
||||
"check:zone-census": "node internal/check-zone-census.mjs # 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.",
|
||||
|
|
@ -145,7 +146,7 @@
|
|||
"check:synced-css": "node internal/check-synced-css-copies.mjs",
|
||||
"check:composite-reg": "node internal/check-composite-registration.mjs",
|
||||
"check:filter-width": "node internal/check-filter-width-stable.mjs",
|
||||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
|
||||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs # the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||||
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
|
||||
"check:mjs-parse": "node internal/check-mjs-parses.mjs",
|
||||
"check:snapshot-guard": "node internal/check-snapshot-guard.mjs # every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
"instances": [
|
||||
{
|
||||
"id": "app",
|
||||
"title": "DX-Maps SPA — Big Picture + every research map (app-shell)",
|
||||
"title": "Research Gallery — Big Picture + every research map (app-shell)",
|
||||
"props": {
|
||||
"profile": "doxa-maps-spa",
|
||||
"provider": "maplibre",
|
||||
"instanceId": "doxa-maps-spa-app",
|
||||
"title": "DX-Maps"
|
||||
"title": "Research Gallery"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@
|
|||
"gospel-resources-map",
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
"label": "DX-Maps SPA (vertical-tab app-shell: Big Picture + every research map)"
|
||||
"label": "Research Gallery (vertical-tab app-shell: Big Picture + every research map)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,6 +308,27 @@ const gapList = computed(() => {
|
|||
// Papua New Guinea has 469 groups with nothing but only 2.2M people, China has 125 groups
|
||||
// and 150.3M. Ranking by either one alone hides the other question.
|
||||
const gapBy = ref('groups')
|
||||
// THE SHARPEST INTERSECTION THIS DATA HOLDS: no gospel resource found in any pool checked,
|
||||
// AND nobody praying. 213 people groups, 4.67M people. Every other view here ends at "this
|
||||
// group has nothing"; this one ends at a page where the reader can do something about it,
|
||||
// which is why the rows are links rather than fly-to buttons.
|
||||
//
|
||||
// "NOBODY PRAYING" MEANS ZERO ON DOXA.LIFE'S COUNTER. It is not a claim that nobody in the
|
||||
// world prays for these people — an unknowable thing — and the sheet says so rather than
|
||||
// letting the number imply it.
|
||||
const gapNoPrayer = computed(() => {
|
||||
if (!showGapList.value) return []
|
||||
const out = []
|
||||
for (let i = 0; i < pinRows.value.length; i++) {
|
||||
const r = pinRows.value[i]
|
||||
if (r[6] !== 0) continue
|
||||
const e = prayIndex.value[prayKey(r[0], r[3])]
|
||||
if (!e || e.praying > 0) continue
|
||||
out.push({ i, name: r[0], iso3: r[3], code: r[4], pop: r[5], url: e.u })
|
||||
}
|
||||
return out.sort((a, b) => b.pop - a.pop).slice(0, 40)
|
||||
})
|
||||
|
||||
// A THIRD AXIS: the RATE. The country tab ranks by how many groups a country holds and
|
||||
// shows population beside it; neither answers "what share of this place is unreached".
|
||||
// Measured, the three disagree completely and each is somebody's question:
|
||||
|
|
@ -346,7 +367,15 @@ const gapCountries = computed(() => {
|
|||
.slice(0, 40)
|
||||
})
|
||||
|
||||
function openGapList() { showGapList.value = true; selected.value = null; selectedGroup.value = null }
|
||||
function openGapList() {
|
||||
// The prayer index is fetched lazily and the "Nobody praying" tab is built from it, so
|
||||
// opening this list without having first clicked a pin left the tab hidden entirely —
|
||||
// the view existed and was unreachable unless you happened to take the long way round.
|
||||
ensurePeopleGroups()
|
||||
showGapList.value = true
|
||||
selected.value = null
|
||||
selectedGroup.value = null
|
||||
}
|
||||
function closeGapList() { showGapList.value = false }
|
||||
// Fly first, THEN open: openPin sets `selected`, which swaps the sheet away from this list,
|
||||
// so doing it the other way round loses the map movement behind a re-render.
|
||||
|
|
@ -429,6 +458,11 @@ const pinsByIso = computed(() => {
|
|||
// and fine in another.
|
||||
// Same normalisation the index was built with. Kept beside groupAt so the two cannot
|
||||
// drift: a key built differently here would silently match nothing.
|
||||
// Their directory names are file-format shorthand; a worker reading the sheet should see
|
||||
// what they can actually open, not the folder name.
|
||||
const SE_KIND = { PDF: 'PDF', audio: 'audio', sab: 'app', study: 'study notes', viewer: 'read online' }
|
||||
const seKinds = (k) => (Array.isArray(k) ? k : []).map((x) => SE_KIND[x] || x).join(', ')
|
||||
|
||||
const prayKey = (name, iso3) =>
|
||||
`${String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()}|${String(iso3 || '').toUpperCase()}`
|
||||
|
||||
|
|
@ -466,6 +500,7 @@ function groupAt(i) {
|
|||
audio: c.au || null,
|
||||
bible_video: c.bv || null,
|
||||
gospel_tract: c.gp || null,
|
||||
scripture_earth: c.se || null,
|
||||
related: c.rel || null,
|
||||
zume_url: c.z || '',
|
||||
zume_approx: c.za || null,
|
||||
|
|
@ -912,7 +947,7 @@ useShadowStyles(
|
|||
'margin-bottom:6px;border-radius:8px;' +
|
||||
'border:1px solid rgba(0,0,0,.10);background:rgba(0,0,0,.02);color:inherit;font:inherit}' +
|
||||
'.is-dark .gm-pg{border-color:rgba(255,255,255,.10);background:rgba(255,255,255,.03)}' +
|
||||
'.gm-pg-btn{cursor:pointer}' +
|
||||
'.gm-pg-btn{cursor:pointer;text-decoration:none;color:inherit}' +
|
||||
// The drill-in control on a resource row: a bare button that inherits the shell's
|
||||
// look, so the row still reads as one tap-target for the name + language line.
|
||||
'.gm-pg-open{display:block;width:100%;text-align:left;padding:0;margin:0;border:0;' +
|
||||
|
|
@ -1073,13 +1108,21 @@ useShadowStyles(
|
|||
<!-- ITEM 0 — the ranked gap: the largest groups with nothing found anywhere. -->
|
||||
<template v-if="showGapList && !selected">
|
||||
<div class="gm-sh-head">
|
||||
<span class="gm-sh-name">{{ gapBy === 'groups' ? 'Largest groups with nothing found' : 'Where the gap concentrates' }}</span>
|
||||
<span class="gm-sh-name">{{ gapBy === 'groups' ? 'Largest groups with nothing found' : gapBy === 'noprayer' ? 'Nothing found, and nobody praying' : 'Where the gap concentrates' }}</span>
|
||||
<button type="button" class="gm-x" @click="closeGapList">Close</button>
|
||||
</div>
|
||||
<p class="gm-note" style="margin-top:0">
|
||||
{{ fmt(pinTally.none) }} people groups have no Scripture, audio, film, Bible video or
|
||||
Door43 resource in the pools checked — {{ fmt(gapPeople) }} people.
|
||||
<template v-if="gapBy === 'regions'">
|
||||
<template v-if="gapBy === 'noprayer'">
|
||||
The groups with nothing found in any pool checked AND nobody praying —
|
||||
{{ fmt(gapNoPrayer.length) }} shown, largest first. Tapping one opens their prayer
|
||||
page on doxa.life.
|
||||
<br><b>"Nobody praying" means zero on doxa.life's counter</b>, not a claim that
|
||||
nobody in the world prays for them. Only the {{ fmt(Object.keys(prayIndex).length) }}
|
||||
groups doxa.life publishes a page for can appear here at all.
|
||||
</template>
|
||||
<template v-else-if="gapBy === 'regions'">
|
||||
The five UN regions, ranked by the SHARE of their groups with nothing — the one
|
||||
question the other two tabs cannot answer. Oceania is half unreached by group
|
||||
and small by population; Asia is the reverse. Rate, count and population each
|
||||
|
|
@ -1110,11 +1153,24 @@ useShadowStyles(
|
|||
@click="gapBy = 'countries'">By country</button>
|
||||
<button type="button" class="gm-gtab" :class="{ on: gapBy === 'regions' }"
|
||||
@click="gapBy = 'regions'">By region</button>
|
||||
<button v-if="gapNoPrayer.length" type="button" class="gm-gtab" :class="{ on: gapBy === 'noprayer' }"
|
||||
@click="gapBy = 'noprayer'">Nobody praying</button>
|
||||
</div>
|
||||
|
||||
<!-- Links, not fly-to buttons: every row here HAS a prayer page, so the useful
|
||||
action is to open it rather than to look at the dot again. -->
|
||||
<template v-if="gapBy === 'noprayer'">
|
||||
<a v-for="g in gapNoPrayer" :key="g.i" class="gm-pg gm-pg-btn" :href="g.url"
|
||||
target="_blank" rel="noopener noreferrer">
|
||||
<span class="gm-pg-n">{{ g.name }}</span>
|
||||
<span class="gm-pg-l">{{ fmt(g.pop) }} people · {{ g.iso3 }}<code class="gm-code">{{ g.code || 'no code' }}</code></span>
|
||||
<span class="gm-pg-rel">nothing found, and nobody praying — open their prayer page ↗</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- Not buttons: a region is not a place this map can fly to, and a control that
|
||||
looks clickable and does nothing is worse than a plain row. -->
|
||||
<template v-if="gapBy === 'regions'">
|
||||
<template v-else-if="gapBy === 'regions'">
|
||||
<div v-for="r in gapRegions" :key="r.region" class="gm-pg">
|
||||
<span class="gm-pg-n">{{ r.region }}</span>
|
||||
<span class="gm-pg-l"><b>{{ r.pct }}%</b> of its {{ fmt(r.groups) }} groups have nothing
|
||||
|
|
@ -1231,6 +1287,17 @@ useShadowStyles(
|
|||
Gospel presentation · {{ selectedGroup.gospel_tract.n }}
|
||||
</a>
|
||||
|
||||
<!-- SCRIPTURE EARTH — SIL and its translation partners, reaching small languages
|
||||
the big catalogues skip. The chip names WHAT IS THERE rather than saying just
|
||||
"Scripture Earth", because the kinds differ per language and a worker needs to
|
||||
know whether they are being sent to a PDF or to recorded audio. Codes carrying
|
||||
only Jesus Film metadata stubs never reach this field. -->
|
||||
<a v-if="selectedGroup.scripture_earth" class="gm-chip ok wrap"
|
||||
:href="selectedGroup.scripture_earth.u" target="_blank" rel="noopener noreferrer"
|
||||
title="Scripture Earth (SIL International and its translation partners) publishes Scripture in this language. The listing shows every file they hold for it.">
|
||||
Scripture Earth · {{ seKinds(selectedGroup.scripture_earth.k) }}
|
||||
</a>
|
||||
|
||||
<!-- Zume is DISCIPLESHIP TRAINING, not Scripture and not film, so it sits here
|
||||
as its own thing and moves no tier. The absent case names the denominator
|
||||
rather than saying "none found": Zume is one platform in 54 languages, not
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const VERIFY = resolve(REPO, 'data/tables/gospel-link-verification.json')
|
|||
const FCBH = resolve(REPO, 'data/tables/fcbh-index.json')
|
||||
const MACRO = resolve(REPO, 'data/tables/iso-macrolanguages.json')
|
||||
const GODTOOLS = resolve(REPO, 'data/tables/godtools-index.json')
|
||||
const SCRIPTUREEARTH = resolve(REPO, 'data/tables/scriptureearth-index.json')
|
||||
|
||||
// Two entries with the same subject rendered as "Open Bible Stories Open Bible Stories"
|
||||
// — two links a reader cannot tell apart. 40 languages are in that state, because a
|
||||
|
|
@ -150,6 +151,11 @@ try {
|
|||
let godtools = {}
|
||||
try { godtools = JSON.parse(readFileSync(GODTOOLS, 'utf8')).codes || {} } catch { godtools = {} }
|
||||
|
||||
// Scripture Earth (scripts/fetch-scriptureearth-index.mjs). Optional like the rest: if the
|
||||
// snapshot is absent the build still runs and simply offers less.
|
||||
let scriptureEarth = {}
|
||||
try { scriptureEarth = JSON.parse(readFileSync(SCRIPTUREEARTH, 'utf8')).byIso || {} } catch { scriptureEarth = {} }
|
||||
|
||||
let zume = {}
|
||||
let zumeApprox = {}
|
||||
try {
|
||||
|
|
@ -316,7 +322,7 @@ for (const [c, v] of Object.entries(codes)) {
|
|||
// credentials this build does not have, and they are the ONLY things the unchecked list
|
||||
// should ever name. A reader deciding where to work needs "we looked and it is not there"
|
||||
// to be distinguishable from "nobody looked".
|
||||
const SCANNED = 'ebible, arclight, door43, fcbh'
|
||||
const SCANNED = 'ebible, arclight, door43, fcbh, scriptureearth'
|
||||
const NOT_SCANNED = 'grn, api_bible, dbl, progress_bible'
|
||||
// SIGN LANGUAGES GET THEIR OWN PROVENANCE, because the generic list is actively
|
||||
// misleading for them. A Deaf group is not served by eBible (text), Door43 (text and
|
||||
|
|
@ -397,6 +403,24 @@ for (const [c, g] of Object.entries(godtools)) {
|
|||
gospelCodes++
|
||||
}
|
||||
|
||||
// SCRIPTURE EARTH. SIL and its translation partners publish Scripture for small languages
|
||||
// the big catalogues skip, which is why this pool reaches groups the others cannot. It DOES
|
||||
// lift a group off tier 0, on the same footing as Door43 and audio: a person here is offered
|
||||
// Scripture in their own language — PDF, recorded audio, a Scripture App Builder app, or the
|
||||
// SFM text — so "nothing found" is simply untrue for them. It never reaches tier 2, which
|
||||
// stays reserved for text Scripture together with the JESUS Film.
|
||||
//
|
||||
// The fetcher already threw away two kinds of false positive that would have shipped here:
|
||||
// empty directories (most of the index) and directories holding only JESUSFilm-*.txt
|
||||
// metadata stubs. What arrives in this file is content that was measured to exist.
|
||||
let seCodes = 0
|
||||
let seNew = 0
|
||||
for (const [c, e] of Object.entries(scriptureEarth)) {
|
||||
if (!codes[c]) { codes[c] = { l: '', ck: SCANNED, un: NOT_SCANNED }; seNew++ }
|
||||
codes[c].se = { u: e.u, k: e.k }
|
||||
seCodes++
|
||||
}
|
||||
|
||||
let zumeApproxCodes = 0
|
||||
for (const [c, a] of Object.entries(zumeApprox)) {
|
||||
// Never overwrite an exact join with an approximate one, and never create a code that
|
||||
|
|
@ -422,6 +446,7 @@ if (dead.size) {
|
|||
if (v.au && dead.has(v.au.u)) { delete v.au; droppedDead++ }
|
||||
if (v.bv && dead.has(v.bv.u)) { delete v.bv; droppedDead++ }
|
||||
if (v.gp && dead.has(v.gp.u)) { delete v.gp; droppedDead++ }
|
||||
if (v.se && dead.has(v.se.u)) { delete v.se; droppedDead++ }
|
||||
if (v.d) {
|
||||
const before = v.d.length
|
||||
v.d = v.d.filter((x) => !dead.has(x.u))
|
||||
|
|
@ -467,7 +492,7 @@ for (const [c, v] of Object.entries(codes)) {
|
|||
|
||||
const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
|
||||
const pins = []
|
||||
let joined = 0, withUrl = 0, noCoord = 0, liftedByD43 = 0, liftedByAudio = 0
|
||||
let joined = 0, withUrl = 0, noCoord = 0, liftedByD43 = 0, liftedByAudio = 0, liftedBySE = 0
|
||||
for (const p of slim) {
|
||||
const lat = num(p.Lat), lng = num(p.Long)
|
||||
if (lat === null || lng === null) { noCoord++; continue }
|
||||
|
|
@ -478,13 +503,15 @@ for (const p of slim) {
|
|||
const hasD = !!(c && c.d && c.d.length)
|
||||
const hasA = !!(c && c.au)
|
||||
const hasBV = !!(c && c.bv)
|
||||
if (hasS || hasF || hasD || hasA || hasBV) withUrl++
|
||||
const hasSE = !!(c && c.se)
|
||||
if (hasS || hasF || hasD || hasA || hasBV || hasSE) withUrl++
|
||||
if (!hasS && !hasF && hasD) liftedByD43++
|
||||
if (!hasS && !hasF && !hasD && (hasA || hasBV)) liftedByAudio++
|
||||
if (!hasS && !hasF && !hasD && !hasA && !hasBV && hasSE) liftedBySE++
|
||||
// tier 2 = Scripture AND film · 1 = SOMETHING found · 0 = nothing in any pool checked.
|
||||
// A Door43 resource lifts a group out of tier 0 because "nothing found" is then simply
|
||||
// untrue for them — but it never reaches tier 2, which is reserved for Scripture + film.
|
||||
const tier = hasS && hasF ? 2 : (hasS || hasF || hasD || hasA || hasBV) ? 1 : 0
|
||||
const tier = hasS && hasF ? 2 : (hasS || hasF || hasD || hasA || hasBV || hasSE) ? 1 : 0
|
||||
pins.push([
|
||||
p.Name || '',
|
||||
Math.round(lat * 100) / 100,
|
||||
|
|
@ -525,6 +552,7 @@ console.log(`arclight codes ${Object.keys(arc).length} (${arcOnly} new, ${upgr
|
|||
console.log(`share urls ${Object.keys(share).length}`)
|
||||
console.log(`fcbh codes ${audioCodes} (${audioNew} new codes)`)
|
||||
console.log(`gospel tracts ${gospelCodes} (Cru/GodTools presentations; depth only, tier UNCHANGED)`)
|
||||
console.log(`scripture earth ${seCodes} (${seNew} new codes; SIL partners — Scripture in their own language)`)
|
||||
console.log(`related variety ${relatedCodes} (nothing of their own; an ISO macrolanguage sibling has something — tier UNCHANGED)`)
|
||||
console.log(`fcbh bible video ${videoCodes} (a separate page; never counted as film) + ${videoMerged} merged into the Scripture link`)
|
||||
console.log(`dead links dropped ${droppedDead} (verification proved them 404; ${dead.size} known broken)`)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const SOURCE = (u) =>
|
|||
: /zume\.training/i.test(u) ? 'Zume Training'
|
||||
: /bible\.is/i.test(u) ? 'Faith Comes By Hearing'
|
||||
: /knowgod\.com/i.test(u) ? 'Cru (GodTools)'
|
||||
: /scriptureearth\.org/i.test(u) ? 'Scripture Earth (SIL)'
|
||||
: 'other'
|
||||
|
||||
const out = []
|
||||
|
|
@ -115,6 +116,15 @@ for (const [code, c] of Object.entries(codes)) {
|
|||
// Its own type. A tract is not Scripture and not a course — it is the thing you open a
|
||||
// conversation with, and a reader choosing a link needs to know which of those they get.
|
||||
if (c.gp) add(c.gp.n || 'Gospel presentation', 'Gospel presentation', c.gp.u, 'Read in this language')
|
||||
// Scripture Earth is typed as Scripture because that is what it is — SIL and its
|
||||
// translation partners publishing the text in this language. The scope column names the
|
||||
// FORMATS actually held, since a worker needs to know whether they are being sent to a
|
||||
// PDF, to recorded audio, or to an app before they set out.
|
||||
if (c.se) {
|
||||
const SE_KIND = { PDF: 'PDF', audio: 'audio', sab: 'app', study: 'study notes', viewer: 'read online' }
|
||||
const kinds = (c.se.k || []).map((k) => SE_KIND[k] || k).join(', ')
|
||||
add('Scripture Earth', 'Scripture', c.se.u, kinds)
|
||||
}
|
||||
add('Zume Training', 'Discipleship training', c.z, 'Course in this language')
|
||||
// The approximate match ships too, with the qualification IN THE SCOPE COLUMN. Leaving
|
||||
// it out would make the table disagree with the map for these languages, and a reader
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* fetch-scriptureearth-index.mjs — Scripture Earth, joined on ISO 639-3.
|
||||
*
|
||||
* WHY THIS SOURCE. Scripture Earth (SIL International and its translation partners)
|
||||
* publishes Scripture for minority languages that the big catalogues miss. eBible,
|
||||
* Arclight, Door43 and FCBH between them still leave 2,901 of our people groups with
|
||||
* nothing at all, and those groups are small-language groups almost by definition —
|
||||
* exactly the population this portal serves. Its content is straight Scripture
|
||||
* translation, which passes the doctrinal filter on the same footing as eBible.
|
||||
*
|
||||
* WHAT IS AND IS NOT EVIDENCE HERE — this is the whole point of the file.
|
||||
* The portal exposes an open Apache index at /data/, listing 3,477 ISO-639-3
|
||||
* directories. THE DIRECTORY EXISTING MEANS NOTHING. Measured: of the 622 directories
|
||||
* matching a tier-0 code, 582 are empty stubs — a bare parent link, 646 bytes. Trusting
|
||||
* the listing alone would have shipped 716 people groups a link to an empty folder.
|
||||
* So a code counts only when its directory actually holds an artefact directory.
|
||||
*
|
||||
* NOR IS EVERY ARTEFACT A RESOURCE. A `video/` directory here contains only
|
||||
* JESUSFilm-<iso>.<lang>.txt metadata stubs, not playable film — 17 of the 40 non-empty
|
||||
* tier-0 codes had nothing but those. A person sent there finds a text file. So `video`
|
||||
* alone never qualifies a code; only PDF, audio, sab (Scripture App Builder), study and
|
||||
* viewer (SFM scripture text) do. Verified by hand before this was written:
|
||||
* data/bkc/PDF/Baka-AT-01.pdf is 14,366,830 bytes of application/pdf, and a nonexistent
|
||||
* sibling returns 404 — so a 200 here is real.
|
||||
*
|
||||
* We do not touch their search endpoints. LSearch.php and livesearch.php answer "Hack!"
|
||||
* to a query string; the open directory index is the part of the site meant to be read,
|
||||
* and robots.txt disallows nothing.
|
||||
*/
|
||||
import { writeFileSync, readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { writeSnapshot } from './_snapshot-guard.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const TABLES = resolve(HERE, '../../../data/tables')
|
||||
const OUT = resolve(TABLES, 'scriptureearth-index.json')
|
||||
const PINS = resolve(TABLES, 'people-group-pins.json')
|
||||
const ORIGIN = 'https://scriptureearth.org'
|
||||
|
||||
/** Their host rejects a bare client; this is the same UA a reader would arrive with. */
|
||||
const UA =
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125 Safari/537.36'
|
||||
|
||||
/** An empty language directory is 646 bytes of Apache chrome. Anything real is larger. */
|
||||
const EMPTY_DIR_BYTES = 700
|
||||
|
||||
/**
|
||||
* Directories that represent something a person can actually open. `video` is deliberately
|
||||
* absent: see the header — it holds Jesus Film metadata stubs, not film.
|
||||
*/
|
||||
const REAL_KINDS = new Set(['PDF', 'audio', 'sab', 'study', 'viewer'])
|
||||
|
||||
const CONCURRENCY = Number(process.env.SE_CONCURRENCY || 4)
|
||||
const LIMIT = Number(process.env.SE_LIMIT || 0)
|
||||
|
||||
async function get(url, timeoutMs = 20000) {
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
headers: { 'user-agent': UA },
|
||||
redirect: 'follow',
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
})
|
||||
if (!r.ok) return null
|
||||
return await r.text()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Child directory names in an Apache index, ignoring the sort links and the parent. */
|
||||
function childDirs(html) {
|
||||
const out = []
|
||||
for (const m of html.matchAll(/href="([^"?/][^"]*)\/"/g)) out.push(decodeURIComponent(m[1]))
|
||||
return out
|
||||
}
|
||||
|
||||
const root = await get(`${ORIGIN}/data/`, 60000)
|
||||
if (!root) {
|
||||
console.error('SCRIPTUREEARTH: /data/ did not answer — leaving the previous snapshot alone.')
|
||||
process.exit(1)
|
||||
}
|
||||
const listed = childDirs(root).filter((d) => /^[a-z]{3}$/.test(d))
|
||||
console.log(`SCRIPTUREEARTH: ${listed.length} ISO-639-3 directories listed.`)
|
||||
|
||||
/* Only codes our own map can join to. Scanning the other ~200 would be traffic for
|
||||
nothing — we could not attach the result to any people group. */
|
||||
const pins = JSON.parse(readFileSync(PINS, 'utf8'))
|
||||
const ours = new Set(Object.keys(pins.codes || {}).map((s) => s.toLowerCase()))
|
||||
let targets = listed.filter((d) => ours.has(d))
|
||||
if (LIMIT) targets = targets.slice(0, LIMIT)
|
||||
console.log(`SCRIPTUREEARTH: ${targets.length} of them are codes this map uses.`)
|
||||
|
||||
const byIso = {}
|
||||
let scanned = 0
|
||||
let empty = 0
|
||||
let videoOnly = 0
|
||||
|
||||
async function worker(queue) {
|
||||
for (;;) {
|
||||
const iso = queue.shift()
|
||||
if (!iso) return
|
||||
const html = await get(`${ORIGIN}/data/${iso}/`)
|
||||
scanned++
|
||||
if (scanned % 250 === 0) console.log(` ${scanned}/${targets.length}`)
|
||||
if (!html || html.length < EMPTY_DIR_BYTES) {
|
||||
empty++
|
||||
continue
|
||||
}
|
||||
const kinds = childDirs(html)
|
||||
const real = kinds.filter((k) => REAL_KINDS.has(k))
|
||||
if (!real.length) {
|
||||
videoOnly++
|
||||
continue
|
||||
}
|
||||
byIso[iso] = { u: `${ORIGIN}/data/${iso}/`, k: real.sort() }
|
||||
}
|
||||
}
|
||||
|
||||
const queue = [...targets]
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker(queue)))
|
||||
|
||||
console.log(
|
||||
`SCRIPTUREEARTH: ${Object.keys(byIso).length} code(s) hold a real artefact; ` +
|
||||
`${empty} empty stub(s), ${videoOnly} with only Jesus Film metadata.`,
|
||||
)
|
||||
|
||||
if (LIMIT) {
|
||||
console.log('SCRIPTUREEARTH: SE_LIMIT set — smoke run, writing nothing.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
writeSnapshot(
|
||||
OUT,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
source: `${ORIGIN}/data/ (open Apache index, robots.txt disallows nothing)`,
|
||||
fetched: new Date().toISOString().slice(0, 10),
|
||||
_note:
|
||||
'A listed directory is NOT a resource: most are empty stubs, and a video/ directory ' +
|
||||
'holds JESUSFilm-*.txt metadata rather than film. Only codes with PDF, audio, sab, ' +
|
||||
'study or viewer content are kept. Publisher is SIL International and its translation ' +
|
||||
'partners — straight Scripture, same doctrinal footing as eBible.',
|
||||
listed: listed.length,
|
||||
scanned: targets.length,
|
||||
emptyStubs: empty,
|
||||
metadataOnly: videoOnly,
|
||||
codes: Object.keys(byIso).length,
|
||||
byIso,
|
||||
},
|
||||
null,
|
||||
1,
|
||||
)}\n`,
|
||||
(j) => Object.keys(j.byIso || {}).length,
|
||||
'Scripture Earth code(s)',
|
||||
)
|
||||
console.log(`wrote ${OUT}`)
|
||||
|
|
@ -333,6 +333,7 @@ for (const v of Object.values(pins.codes)) {
|
|||
if (v.au && v.au.u) urls.add(v.au.u)
|
||||
if (v.bv && v.bv.u) urls.add(v.bv.u)
|
||||
if (v.gp && v.gp.u) urls.add(v.gp.u)
|
||||
if (v.se && v.se.u) urls.add(v.se.u)
|
||||
for (const d of v.d || []) if (d.u) urls.add(d.u)
|
||||
}
|
||||
// Carry forward everything a previous run proved dead, so dropping a link cannot erase the
|
||||
|
|
|
|||
|
|
@ -1097,6 +1097,13 @@ function watchLegendRows() {
|
|||
// here). Providing it wires the mobile sheet to the same handler the desktop
|
||||
// legend's @select uses — without this, mobile rows render but tap dead.
|
||||
provide('onSemanticTreeSelect', onLegendSelect)
|
||||
// The SAME handler the desktop legend gets via @export. Provided rather than passed as a
|
||||
// prop because <LegendMobile> renders several levels down inside the shell, which is also
|
||||
// why onSemanticTreeSelect above is provided. Without this the mobile sheet rendered its
|
||||
// SemanticTreeLegend with exportEnabled defaulting to FALSE, so a phone reader could browse
|
||||
// the lists and had no way to print a packet or a poster — the half of this profile the
|
||||
// Driver called "the only way that map would be usable".
|
||||
provide('onLegendExport', onLegendExport)
|
||||
|
||||
// ── LEGEND EXPORT (top-right of the legend titlebar) ────────────────────────
|
||||
// The shared legend emits 'export' with 'poster' | 'packet' for the currently
|
||||
|
|
@ -2004,6 +2011,8 @@ onBeforeUnmount(() => {
|
|||
v-if="legendNodes.length"
|
||||
legend-type="upg-100"
|
||||
:nodes="legendNodes"
|
||||
:tabs="LEGEND_TABS"
|
||||
:default-tab="LEGEND_DEFAULT_TAB"
|
||||
:columns="['count', 'pop']"
|
||||
column-label="UPG 100"
|
||||
:title="LEGEND_TITLE"
|
||||
|
|
|
|||
2
public/assets/tables/gospel-gaps.json
vendored
2
public/assets/tables/gospel-gaps.json
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/assets/tables/people-group-pins.json
vendored
2
public/assets/tables/people-group-pins.json
vendored
File diff suppressed because one or more lines are too long
340
public/doxa-maps/assets/tables/country-gap.json
generated
340
public/doxa-maps/assets/tables/country-gap.json
generated
|
|
@ -70,8 +70,8 @@
|
|||
"title": "The gap by country",
|
||||
"grain": "one row per country that has at least one people group on this map",
|
||||
"generated": "2026-09-24",
|
||||
"measured": "242 countries · 2,901 groups with nothing found · 287,644,779 people",
|
||||
"by_region": "Oceania 51.2% (679 of 1325) · Africa 30.0% (923 of 3074) · Asia 19.3% (978 of 5073) · Americas 11.4% (241 of 2117) · Europe 10.1% (80 of 790)",
|
||||
"measured": "242 countries · 2,873 groups with nothing found · 287,305,229 people",
|
||||
"by_region": "Oceania 51.2% (678 of 1325) · Africa 29.9% (919 of 3074) · Asia 19.2% (974 of 5073) · Americas 10.5% (222 of 2117) · Europe 10.1% (80 of 790)",
|
||||
"three_questions": "Groups with nothing, people with nothing, and the share of the country each represents. They rank differently on purpose — Papua New Guinea has the most groups and few people, China the reverse, and Oceania has the worst share of any region while being the smallest by population.",
|
||||
"source": "data/tables/gospel-by-country.json, which is itself refreshed from data/tables/people-group-pins.json — the same index the map draws, so this table and the map cannot disagree.",
|
||||
"curated_columns": "growing_unreached and deaths_day come from the curated country analysis and are null where it does not cover the country. Null, never zero: zero is a claim this build cannot make."
|
||||
|
|
@ -82,9 +82,9 @@
|
|||
"iso3": "PNG",
|
||||
"region": "Oceania",
|
||||
"people_groups": 831,
|
||||
"groups_with_nothing": 469,
|
||||
"pct_with_nothing": 56.4,
|
||||
"people_with_nothing": 2187150,
|
||||
"groups_with_nothing": 468,
|
||||
"pct_with_nothing": 56.3,
|
||||
"people_with_nothing": 2179750,
|
||||
"groups_with_scripture": 332,
|
||||
"groups_with_film": 143,
|
||||
"band": "high",
|
||||
|
|
@ -112,9 +112,9 @@
|
|||
"iso3": "NGA",
|
||||
"region": "Africa",
|
||||
"people_groups": 433,
|
||||
"groups_with_nothing": 188,
|
||||
"pct_with_nothing": 43.4,
|
||||
"people_with_nothing": 9036825,
|
||||
"groups_with_nothing": 187,
|
||||
"pct_with_nothing": 43.2,
|
||||
"people_with_nothing": 8978825,
|
||||
"groups_with_scripture": 105,
|
||||
"groups_with_film": 188,
|
||||
"band": "high",
|
||||
|
|
@ -127,9 +127,9 @@
|
|||
"iso3": "CMR",
|
||||
"region": "Africa",
|
||||
"people_groups": 260,
|
||||
"groups_with_nothing": 130,
|
||||
"pct_with_nothing": 50,
|
||||
"people_with_nothing": 3722425,
|
||||
"groups_with_nothing": 129,
|
||||
"pct_with_nothing": 49.6,
|
||||
"people_with_nothing": 3656425,
|
||||
"groups_with_scripture": 72,
|
||||
"groups_with_film": 84,
|
||||
"band": "high",
|
||||
|
|
@ -262,9 +262,9 @@
|
|||
"iso3": "BRA",
|
||||
"region": "Americas",
|
||||
"people_groups": 270,
|
||||
"groups_with_nothing": 62,
|
||||
"pct_with_nothing": 23,
|
||||
"people_with_nothing": 73130,
|
||||
"groups_with_nothing": 58,
|
||||
"pct_with_nothing": 21.5,
|
||||
"people_with_nothing": 65680,
|
||||
"groups_with_scripture": 186,
|
||||
"groups_with_film": 163,
|
||||
"band": "partial",
|
||||
|
|
@ -292,9 +292,9 @@
|
|||
"iso3": "USA",
|
||||
"region": "Americas",
|
||||
"people_groups": 623,
|
||||
"groups_with_nothing": 44,
|
||||
"pct_with_nothing": 7.1,
|
||||
"people_with_nothing": 473415,
|
||||
"groups_with_nothing": 41,
|
||||
"pct_with_nothing": 6.6,
|
||||
"people_with_nothing": 465640,
|
||||
"groups_with_scripture": 517,
|
||||
"groups_with_film": 549,
|
||||
"band": "partial",
|
||||
|
|
@ -332,21 +332,6 @@
|
|||
"deaths_day": 0.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Vietnam",
|
||||
"iso3": "VNM",
|
||||
"region": "Asia",
|
||||
"people_groups": 74,
|
||||
"groups_with_nothing": 30,
|
||||
"pct_with_nothing": 40.5,
|
||||
"people_with_nothing": 1902675,
|
||||
"groups_with_scripture": 28,
|
||||
"groups_with_film": 38,
|
||||
"band": "high",
|
||||
"growing_unreached": 11,
|
||||
"deaths_day": 5.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Republic of the Congo",
|
||||
"iso3": "COG",
|
||||
|
|
@ -362,6 +347,21 @@
|
|||
"deaths_day": 1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Vietnam",
|
||||
"iso3": "VNM",
|
||||
"region": "Asia",
|
||||
"people_groups": 74,
|
||||
"groups_with_nothing": 29,
|
||||
"pct_with_nothing": 39.2,
|
||||
"people_with_nothing": 1860175,
|
||||
"groups_with_scripture": 28,
|
||||
"groups_with_film": 38,
|
||||
"band": "high",
|
||||
"growing_unreached": 11,
|
||||
"deaths_day": 5.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "New Caledonia",
|
||||
"iso3": "NCL",
|
||||
|
|
@ -397,9 +397,9 @@
|
|||
"iso3": "MMR",
|
||||
"region": "Asia",
|
||||
"people_groups": 110,
|
||||
"groups_with_nothing": 26,
|
||||
"pct_with_nothing": 23.6,
|
||||
"people_with_nothing": 1278900,
|
||||
"groups_with_nothing": 25,
|
||||
"pct_with_nothing": 22.7,
|
||||
"people_with_nothing": 1274400,
|
||||
"groups_with_scripture": 63,
|
||||
"groups_with_film": 64,
|
||||
"band": "partial",
|
||||
|
|
@ -467,36 +467,6 @@
|
|||
"deaths_day": 13.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bhutan",
|
||||
"iso3": "BTN",
|
||||
"region": "Asia",
|
||||
"people_groups": 36,
|
||||
"groups_with_nothing": 20,
|
||||
"pct_with_nothing": 55.6,
|
||||
"people_with_nothing": 240825,
|
||||
"groups_with_scripture": 11,
|
||||
"groups_with_film": 16,
|
||||
"band": "high",
|
||||
"growing_unreached": 13,
|
||||
"deaths_day": 2.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Canada",
|
||||
"iso3": "CAN",
|
||||
"region": "Americas",
|
||||
"people_groups": 304,
|
||||
"groups_with_nothing": 20,
|
||||
"pct_with_nothing": 6.6,
|
||||
"people_with_nothing": 187600,
|
||||
"groups_with_scripture": 256,
|
||||
"groups_with_film": 270,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Afghanistan",
|
||||
"iso3": "AFG",
|
||||
|
|
@ -512,6 +482,21 @@
|
|||
"deaths_day": 17.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bhutan",
|
||||
"iso3": "BTN",
|
||||
"region": "Asia",
|
||||
"people_groups": 36,
|
||||
"groups_with_nothing": 19,
|
||||
"pct_with_nothing": 52.8,
|
||||
"people_with_nothing": 238225,
|
||||
"groups_with_scripture": 11,
|
||||
"groups_with_film": 16,
|
||||
"band": "high",
|
||||
"growing_unreached": 13,
|
||||
"deaths_day": 2.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Ethiopia",
|
||||
"iso3": "ETH",
|
||||
|
|
@ -527,6 +512,21 @@
|
|||
"deaths_day": 130,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Canada",
|
||||
"iso3": "CAN",
|
||||
"region": "Americas",
|
||||
"people_groups": 304,
|
||||
"groups_with_nothing": 18,
|
||||
"pct_with_nothing": 5.9,
|
||||
"people_with_nothing": 183000,
|
||||
"groups_with_scripture": 256,
|
||||
"groups_with_film": 270,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Russian Federation",
|
||||
"iso3": "RUS",
|
||||
|
|
@ -572,21 +572,6 @@
|
|||
"deaths_day": 12.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Peru",
|
||||
"iso3": "PER",
|
||||
"region": "Americas",
|
||||
"people_groups": 90,
|
||||
"groups_with_nothing": 16,
|
||||
"pct_with_nothing": 17.8,
|
||||
"people_with_nothing": 1194475,
|
||||
"groups_with_scripture": 73,
|
||||
"groups_with_film": 57,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 5.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Ghana",
|
||||
"iso3": "GHA",
|
||||
|
|
@ -647,6 +632,21 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Peru",
|
||||
"iso3": "PER",
|
||||
"region": "Americas",
|
||||
"people_groups": 90,
|
||||
"groups_with_nothing": 12,
|
||||
"pct_with_nothing": 13.3,
|
||||
"people_with_nothing": 1160875,
|
||||
"groups_with_scripture": 73,
|
||||
"groups_with_film": 57,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 5.4,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Oman",
|
||||
"iso3": "OMN",
|
||||
|
|
@ -722,21 +722,6 @@
|
|||
"deaths_day": 0.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Gabon",
|
||||
"iso3": "GAB",
|
||||
"region": "Africa",
|
||||
"people_groups": 45,
|
||||
"groups_with_nothing": 9,
|
||||
"pct_with_nothing": 20,
|
||||
"people_with_nothing": 99600,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 24,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.3,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "French Polynesia",
|
||||
"iso3": "PYF",
|
||||
|
|
@ -767,6 +752,21 @@
|
|||
"deaths_day": 4.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Gabon",
|
||||
"iso3": "GAB",
|
||||
"region": "Africa",
|
||||
"people_groups": 45,
|
||||
"groups_with_nothing": 8,
|
||||
"pct_with_nothing": 17.8,
|
||||
"people_with_nothing": 92800,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 24,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.3,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Namibia",
|
||||
"iso3": "NAM",
|
||||
|
|
@ -797,21 +797,6 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Venezuela",
|
||||
"iso3": "VEN",
|
||||
"region": "Americas",
|
||||
"people_groups": 51,
|
||||
"groups_with_nothing": 7,
|
||||
"pct_with_nothing": 13.7,
|
||||
"people_with_nothing": 311800,
|
||||
"groups_with_scripture": 42,
|
||||
"groups_with_film": 39,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.2,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Cambodia",
|
||||
"iso3": "KHM",
|
||||
|
|
@ -1022,21 +1007,6 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Benin",
|
||||
"iso3": "BEN",
|
||||
"region": "Africa",
|
||||
"people_groups": 57,
|
||||
"groups_with_nothing": 5,
|
||||
"pct_with_nothing": 8.8,
|
||||
"people_with_nothing": 554500,
|
||||
"groups_with_scripture": 29,
|
||||
"groups_with_film": 38,
|
||||
"band": "partial",
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 21.9,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Brunei Darussalam",
|
||||
"iso3": "BRN",
|
||||
|
|
@ -1052,6 +1022,21 @@
|
|||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Venezuela",
|
||||
"iso3": "VEN",
|
||||
"region": "Americas",
|
||||
"people_groups": 51,
|
||||
"groups_with_nothing": 5,
|
||||
"pct_with_nothing": 9.8,
|
||||
"people_with_nothing": 307700,
|
||||
"groups_with_scripture": 42,
|
||||
"groups_with_film": 39,
|
||||
"band": "partial",
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.2,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Taiwan",
|
||||
"iso3": "TWN",
|
||||
|
|
@ -1172,21 +1157,6 @@
|
|||
"deaths_day": 1.3,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Mexico",
|
||||
"iso3": "MEX",
|
||||
"region": "Americas",
|
||||
"people_groups": 205,
|
||||
"groups_with_nothing": 5,
|
||||
"pct_with_nothing": 2.4,
|
||||
"people_with_nothing": 27700,
|
||||
"groups_with_scripture": 193,
|
||||
"groups_with_film": 175,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Egypt",
|
||||
"iso3": "EGY",
|
||||
|
|
@ -1217,6 +1187,21 @@
|
|||
"deaths_day": 3.6,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Benin",
|
||||
"iso3": "BEN",
|
||||
"region": "Africa",
|
||||
"people_groups": 57,
|
||||
"groups_with_nothing": 4,
|
||||
"pct_with_nothing": 7,
|
||||
"people_with_nothing": 500000,
|
||||
"groups_with_scripture": 29,
|
||||
"groups_with_film": 38,
|
||||
"band": "partial",
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 21.9,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Comoros",
|
||||
"iso3": "COM",
|
||||
|
|
@ -1322,21 +1307,6 @@
|
|||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bolivia",
|
||||
"iso3": "BOL",
|
||||
"region": "Americas",
|
||||
"people_groups": 39,
|
||||
"groups_with_nothing": 4,
|
||||
"pct_with_nothing": 10.3,
|
||||
"people_with_nothing": 134875,
|
||||
"groups_with_scripture": 34,
|
||||
"groups_with_film": 28,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "French Guiana",
|
||||
"iso3": "GUF",
|
||||
|
|
@ -1562,6 +1532,21 @@
|
|||
"deaths_day": 1.1,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Mexico",
|
||||
"iso3": "MEX",
|
||||
"region": "Americas",
|
||||
"people_groups": 205,
|
||||
"groups_with_nothing": 3,
|
||||
"pct_with_nothing": 1.5,
|
||||
"people_with_nothing": 18600,
|
||||
"groups_with_scripture": 193,
|
||||
"groups_with_film": 175,
|
||||
"band": "partial",
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Wallis and Futuna",
|
||||
"iso3": "WLF",
|
||||
|
|
@ -1727,6 +1712,21 @@
|
|||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Bolivia",
|
||||
"iso3": "BOL",
|
||||
"region": "Americas",
|
||||
"people_groups": 39,
|
||||
"groups_with_nothing": 2,
|
||||
"pct_with_nothing": 5.1,
|
||||
"people_with_nothing": 120250,
|
||||
"groups_with_scripture": 34,
|
||||
"groups_with_film": 28,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Honduras",
|
||||
"iso3": "HND",
|
||||
|
|
@ -2687,21 +2687,6 @@
|
|||
"deaths_day": null,
|
||||
"basis": "pins only — no curated country analysis for this country"
|
||||
},
|
||||
{
|
||||
"country": "Syrian Arab Republic",
|
||||
"iso3": "SYR",
|
||||
"region": "Asia",
|
||||
"people_groups": 20,
|
||||
"groups_with_nothing": 1,
|
||||
"pct_with_nothing": 5,
|
||||
"people_with_nothing": 16000,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 19,
|
||||
"band": "partial",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 25.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Lithuania",
|
||||
"iso3": "LTU",
|
||||
|
|
@ -3392,6 +3377,21 @@
|
|||
"deaths_day": 39.8,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Syrian Arab Republic",
|
||||
"iso3": "SYR",
|
||||
"region": "Asia",
|
||||
"people_groups": 20,
|
||||
"groups_with_nothing": 0,
|
||||
"pct_with_nothing": 0,
|
||||
"people_with_nothing": 0,
|
||||
"groups_with_scripture": 13,
|
||||
"groups_with_film": 19,
|
||||
"band": "covered",
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 25.7,
|
||||
"basis": "pins, with curated country analysis"
|
||||
},
|
||||
{
|
||||
"country": "Jordan",
|
||||
"iso3": "JOR",
|
||||
|
|
|
|||
|
|
@ -59,14 +59,14 @@
|
|||
"country": "Brazil",
|
||||
"region": "Americas",
|
||||
"groups": 270,
|
||||
"no_resource": 62,
|
||||
"pop_no_resource": 73130,
|
||||
"no_resource": 58,
|
||||
"pop_no_resource": 65680,
|
||||
"scripture": 186,
|
||||
"film": 163,
|
||||
"growing_unreached": 38,
|
||||
"deaths_day": 1.2,
|
||||
"growth": 0.41,
|
||||
"pct_no_resource": 23,
|
||||
"pct_no_resource": 21.5,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -104,14 +104,14 @@
|
|||
"country": "Nigeria",
|
||||
"region": "Africa",
|
||||
"groups": 433,
|
||||
"no_resource": 188,
|
||||
"pop_no_resource": 9036825,
|
||||
"no_resource": 187,
|
||||
"pop_no_resource": 8978825,
|
||||
"scripture": 105,
|
||||
"film": 188,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 44,
|
||||
"growth": null,
|
||||
"pct_no_resource": 43.4,
|
||||
"pct_no_resource": 43.2,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -164,14 +164,14 @@
|
|||
"country": "Bhutan",
|
||||
"region": "Asia",
|
||||
"groups": 36,
|
||||
"no_resource": 20,
|
||||
"pop_no_resource": 240825,
|
||||
"no_resource": 19,
|
||||
"pop_no_resource": 238225,
|
||||
"scripture": 11,
|
||||
"film": 16,
|
||||
"growing_unreached": 13,
|
||||
"deaths_day": 2.1,
|
||||
"growth": 0.65,
|
||||
"pct_no_resource": 55.6,
|
||||
"pct_no_resource": 52.8,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -194,14 +194,14 @@
|
|||
"country": "Vietnam",
|
||||
"region": "Asia",
|
||||
"groups": 74,
|
||||
"no_resource": 30,
|
||||
"pop_no_resource": 1902675,
|
||||
"no_resource": 29,
|
||||
"pop_no_resource": 1860175,
|
||||
"scripture": 28,
|
||||
"film": 38,
|
||||
"growing_unreached": 11,
|
||||
"deaths_day": 5.7,
|
||||
"growth": 0.63,
|
||||
"pct_no_resource": 40.5,
|
||||
"pct_no_resource": 39.2,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -284,14 +284,14 @@
|
|||
"country": "Cameroon",
|
||||
"region": "Africa",
|
||||
"groups": 260,
|
||||
"no_resource": 130,
|
||||
"pop_no_resource": 3722425,
|
||||
"no_resource": 129,
|
||||
"pop_no_resource": 3656425,
|
||||
"scripture": 72,
|
||||
"film": 84,
|
||||
"growing_unreached": 8,
|
||||
"deaths_day": 3.4,
|
||||
"growth": 2.61,
|
||||
"pct_no_resource": 50,
|
||||
"pct_no_resource": 49.6,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -494,14 +494,14 @@
|
|||
"country": "Myanmar",
|
||||
"region": "Asia",
|
||||
"groups": 110,
|
||||
"no_resource": 26,
|
||||
"pop_no_resource": 1278900,
|
||||
"no_resource": 25,
|
||||
"pop_no_resource": 1274400,
|
||||
"scripture": 63,
|
||||
"film": 64,
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 25.8,
|
||||
"growth": 0.67,
|
||||
"pct_no_resource": 23.6,
|
||||
"pct_no_resource": 22.7,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -509,14 +509,14 @@
|
|||
"country": "Benin",
|
||||
"region": "Africa",
|
||||
"groups": 57,
|
||||
"no_resource": 5,
|
||||
"pop_no_resource": 554500,
|
||||
"no_resource": 4,
|
||||
"pop_no_resource": 500000,
|
||||
"scripture": 29,
|
||||
"film": 38,
|
||||
"growing_unreached": 4,
|
||||
"deaths_day": 21.9,
|
||||
"growth": 2.46,
|
||||
"pct_no_resource": 8.8,
|
||||
"pct_no_resource": 7,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -674,14 +674,14 @@
|
|||
"country": "Peru",
|
||||
"region": "Americas",
|
||||
"groups": 90,
|
||||
"no_resource": 16,
|
||||
"pop_no_resource": 1194475,
|
||||
"no_resource": 12,
|
||||
"pop_no_resource": 1160875,
|
||||
"scripture": 73,
|
||||
"film": 57,
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 5.4,
|
||||
"growth": 1.09,
|
||||
"pct_no_resource": 17.8,
|
||||
"pct_no_resource": 13.3,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -809,14 +809,14 @@
|
|||
"country": "Gabon",
|
||||
"region": "Africa",
|
||||
"groups": 45,
|
||||
"no_resource": 9,
|
||||
"pop_no_resource": 99600,
|
||||
"no_resource": 8,
|
||||
"pop_no_resource": 92800,
|
||||
"scripture": 13,
|
||||
"film": 24,
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.3,
|
||||
"growth": 2.16,
|
||||
"pct_no_resource": 20,
|
||||
"pct_no_resource": 17.8,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -824,14 +824,14 @@
|
|||
"country": "Venezuela",
|
||||
"region": "Americas",
|
||||
"groups": 51,
|
||||
"no_resource": 7,
|
||||
"pop_no_resource": 311800,
|
||||
"no_resource": 5,
|
||||
"pop_no_resource": 307700,
|
||||
"scripture": 42,
|
||||
"film": 39,
|
||||
"growing_unreached": 2,
|
||||
"deaths_day": 0.2,
|
||||
"growth": 0.37,
|
||||
"pct_no_resource": 13.7,
|
||||
"pct_no_resource": 9.8,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -1049,14 +1049,14 @@
|
|||
"country": "Papua New Guinea",
|
||||
"region": "Oceania",
|
||||
"groups": 831,
|
||||
"no_resource": 469,
|
||||
"pop_no_resource": 2187150,
|
||||
"no_resource": 468,
|
||||
"pop_no_resource": 2179750,
|
||||
"scripture": 332,
|
||||
"film": 143,
|
||||
"growing_unreached": 1,
|
||||
"deaths_day": 0.9,
|
||||
"growth": 1.78,
|
||||
"pct_no_resource": 56.4,
|
||||
"pct_no_resource": 56.3,
|
||||
"band": "high"
|
||||
},
|
||||
{
|
||||
|
|
@ -1889,15 +1889,15 @@
|
|||
"country": "Syrian Arab Republic",
|
||||
"region": "Asia",
|
||||
"groups": 20,
|
||||
"no_resource": 1,
|
||||
"pop_no_resource": 16000,
|
||||
"no_resource": 0,
|
||||
"pop_no_resource": 0,
|
||||
"scripture": 13,
|
||||
"film": 19,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 25.7,
|
||||
"growth": 4.47,
|
||||
"pct_no_resource": 5,
|
||||
"band": "partial"
|
||||
"pct_no_resource": 0,
|
||||
"band": "covered"
|
||||
},
|
||||
{
|
||||
"iso3": "JOR",
|
||||
|
|
@ -2264,14 +2264,14 @@
|
|||
"country": "United States of America",
|
||||
"region": "Americas",
|
||||
"groups": 623,
|
||||
"no_resource": 44,
|
||||
"pop_no_resource": 473415,
|
||||
"no_resource": 41,
|
||||
"pop_no_resource": 465640,
|
||||
"scripture": 517,
|
||||
"film": 549,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0.1,
|
||||
"growth": 0.96,
|
||||
"pct_no_resource": 7.1,
|
||||
"pct_no_resource": 6.6,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -2384,14 +2384,14 @@
|
|||
"country": "Bolivia",
|
||||
"region": "Americas",
|
||||
"groups": 39,
|
||||
"no_resource": 4,
|
||||
"pop_no_resource": 134875,
|
||||
"no_resource": 2,
|
||||
"pop_no_resource": 120250,
|
||||
"scripture": 34,
|
||||
"film": 28,
|
||||
"growing_unreached": 0,
|
||||
"deaths_day": 0,
|
||||
"growth": 1.37,
|
||||
"pct_no_resource": 10.3,
|
||||
"pct_no_resource": 5.1,
|
||||
"band": "partial"
|
||||
},
|
||||
{
|
||||
|
|
@ -2676,14 +2676,14 @@
|
|||
"country": "Mexico",
|
||||
"region": "Americas",
|
||||
"groups": 205,
|
||||
"no_resource": 5,
|
||||
"pop_no_resource": 27700,
|
||||
"no_resource": 3,
|
||||
"pop_no_resource": 18600,
|
||||
"scripture": 193,
|
||||
"film": 175,
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"growth": null,
|
||||
"pct_no_resource": 2.4,
|
||||
"pct_no_resource": 1.5,
|
||||
"band": "partial",
|
||||
"from_pins_only": true
|
||||
},
|
||||
|
|
@ -2868,14 +2868,14 @@
|
|||
"country": "Canada",
|
||||
"region": "Americas",
|
||||
"groups": 304,
|
||||
"no_resource": 20,
|
||||
"pop_no_resource": 187600,
|
||||
"no_resource": 18,
|
||||
"pop_no_resource": 183000,
|
||||
"scripture": 256,
|
||||
"film": 270,
|
||||
"growing_unreached": null,
|
||||
"deaths_day": null,
|
||||
"growth": null,
|
||||
"pct_no_resource": 6.6,
|
||||
"pct_no_resource": 5.9,
|
||||
"band": "partial",
|
||||
"from_pins_only": true
|
||||
},
|
||||
|
|
|
|||
2
public/doxa-maps/assets/tables/gospel-gaps.json
generated
2
public/doxa-maps/assets/tables/gospel-gaps.json
generated
File diff suppressed because one or more lines are too long
4616
public/doxa-maps/assets/tables/gospel-link-verification.json
generated
4616
public/doxa-maps/assets/tables/gospel-link-verification.json
generated
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7242
public/doxa-maps/assets/tables/scriptureearth-index.json
generated
Normal file
7242
public/doxa-maps/assets/tables/scriptureearth-index.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DX-Maps SPA — Big Picture + every research map (app-shell) · doxa-maps-spa</title>
|
||||
<title>Research Gallery — Big Picture + every research map (app-shell) · doxa-maps-spa</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
|
||||
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"doxa-maps-spa","provider":"maplibre","instanceId":"doxa-maps-spa-app","title":"DX-Maps"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"doxa-maps-spa","provider":"maplibre","instanceId":"doxa-maps-spa-app","title":"Research Gallery"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
var TOKEN_FREE = true;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
24
public/doxa-maps/manifest.json
generated
24
public/doxa-maps/manifest.json
generated
|
|
@ -327,13 +327,13 @@
|
|||
"instances": [
|
||||
{
|
||||
"id": "app",
|
||||
"title": "DX-Maps SPA — Big Picture + every research map (app-shell)",
|
||||
"title": "Research Gallery — Big Picture + every research map (app-shell)",
|
||||
"apiBaseUrl": "",
|
||||
"props": {
|
||||
"profile": "doxa-maps-spa",
|
||||
"provider": "maplibre",
|
||||
"instanceId": "doxa-maps-spa-app",
|
||||
"title": "DX-Maps"
|
||||
"title": "Research Gallery"
|
||||
},
|
||||
"element": "doxa-maps-spa",
|
||||
"iframeUrl": "./doxa-maps-spa/doxa-maps-spa/app/index.html",
|
||||
|
|
@ -591,7 +591,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "doxa-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ",
|
||||
"integrity": "sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -611,7 +611,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/charts/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -625,7 +625,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -639,7 +639,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -653,7 +653,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -667,7 +667,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-map/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=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-TiRZbat0Kaxnl3yc5jZGVBaEkfQau4jxgRhdne31vkEYSldmSpOKiNX4WjwRudUQ\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-DLpCR/4TKWn/tY/3M4MYKfBcueIKSL1WhSTGSWs49t6raFYF5vJit++4TpmCdNJ2\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -676,7 +676,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p",
|
||||
"integrity": "sha384-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -696,7 +696,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-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" 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-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -710,7 +710,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh\" 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>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -724,7 +724,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh\" 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
146
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
146
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-KF3XG9q4VdSvuLQEeK0P1gsKg1cpNOvkLYK5MAIMCUwSsO07z5dI4ffGuc4U6Y+p" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-jGD6GdGzjAWNQ/APgKvUZDcXvryVdhvFXGLWNVENRfjZMurTwcMGmOUXDGF3wvnh" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue