Its toast has always read "Tap a coloured dot — someone still needs prayer". Tapping one showed Scripture, film, audio, video, stories, tracts and training, and no way to pray. doxa.life publishes a page per people group and sibling plugins in this repo already link to it; this map simply never did. 1,723 groups now carry a "Pray for <group>" action with the number already praying beside it. THIS IS THE ONE JOIN HERE THAT IS NOT BY LANGUAGE, and it needed justifying rather than assuming. Every other source keys on ISO 639-3 because a translation serves everyone who speaks the language; prayer does not work that way — you pray for a people, in a place. So this keys on (group name, country), which is a NAME join, the thing this map refuses everywhere else because names collide. Allowed only because the key was tested first: on the API side (name, country) is perfectly unique — 2,107 entries, 2,107 distinct keys, 0 collisions — and on the pin side 12,379 pins give 12,369 distinct keys. The ambiguous ones are DROPPED, not guessed. One entry was lost that way, and 383 more have no pin here at all. pray.doxa.life is the fourth host in this bundle that answers 200 with identical bytes for any URL: /abai-sungai and /zzz-not-a-real-group are both 3,903 bytes. Rendered it does discriminate — a real slug titles the page "Abai Sungai - App" with 1,863 characters, a bogus one titles it "Doxa.Life" with zero — but existence comes from the API listing, which is the publisher's own statement and costs one request rather than 1,723 renders. The lookup is COMPUTED, not baked into groupAt. groupAt runs the instant a pin is clicked, so a value read there is whatever had arrived by then and never updates when the lazy fetch lands — the same shape as the link-verification state that used to hang off openPin's country branch. Verified with a negative control: a group in the index renders an anchor whose href matches the expected slug URL exactly, and German, which is not in it, renders nothing. My probe reported FAIL twice against correct code by waiting a guessed 900ms; it now waits for the data. Screenshot read — the Lati, 2,900 people with nothing found in any pool, can now be prayed for.
103 lines
5.2 KiB
JavaScript
103 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* fetch-pray-index.mjs — the prayer link for a people group, from doxa.life's own list.
|
|
*
|
|
* WHY. This map's toast says "Tap a coloured dot — someone still needs prayer", and until
|
|
* now tapping one offered no way to pray. Sibling plugins in this repo already link to
|
|
* pray.doxa.life per group (see library/components/PeopleGroupDetail.vue), so the house
|
|
* pattern and the URL shape already exist:
|
|
* https://pray.doxa.life/<slug>?source=doxalife&utm_source=agwm
|
|
*
|
|
* ═══ THIS IS THE ONE JOIN THAT IS NOT BY LANGUAGE ═══════════════════════════════════════
|
|
* Every other source here keys on ISO 639-3, because a translation serves everyone who
|
|
* speaks the language. Prayer does not work that way: you pray for a PEOPLE, in a place.
|
|
* So this keys on (people-group name, country) — and that is a NAME join, which this map
|
|
* refuses everywhere else because language names collide.
|
|
*
|
|
* It is allowed here only because the key was tested first, not assumed:
|
|
* - on the API side (name, country) is PERFECTLY unique — 2,107 entries, 2,107 distinct
|
|
* keys, 0 collisions;
|
|
* - on the pin side 12,379 pins give 12,369 distinct keys, so 10 pins are ambiguous.
|
|
* Those 10 are SKIPPED rather than guessed. An ambiguous key would attach one group's
|
|
* prayer page to another group with the same name in the same country, which is exactly
|
|
* the failure the no-name-matching rule exists to prevent.
|
|
*
|
|
* ═══ A 200 FROM pray.doxa.life PROVES NOTHING ═══════════════════════════════════════════
|
|
* It is the fourth host in this bundle that answers 200 with identical bytes for any URL:
|
|
* /abai-sungai -> 200, 3,903 bytes
|
|
* /zzz-not-a-real-group -> 200, 3,903 bytes
|
|
* Rendered it does discriminate — a real slug titles the page "Abai Sungai - App" with
|
|
* 1,863 characters of content, a bogus one titles it "Doxa.Life" with zero — but the
|
|
* existence proof used here is the API listing itself: a slug exists exactly when
|
|
* doxa.life publishes it. That is the publisher's own statement, and it costs one request
|
|
* rather than 1,725 renders.
|
|
*/
|
|
import { writeSnapshot } from './_snapshot-guard.mjs'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const REPO = resolve(HERE, '../../..')
|
|
const OUT = resolve(REPO, 'data/tables/pray-index.json')
|
|
const API = 'https://pray.doxa.life/api/people-groups/list'
|
|
const SITE = 'https://pray.doxa.life'
|
|
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
|
|
|
|
const key = (name, iso3) => `${String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()}|${String(iso3 || '').toUpperCase()}`
|
|
|
|
const res = await fetch(API, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(90000) })
|
|
if (!res.ok) throw new Error(`pray list: HTTP ${res.status}`)
|
|
const posts = (await res.json()).posts || []
|
|
if (posts.length < 500) {
|
|
console.error(`PRAY: ABORT — the list returned ${posts.length} entries; refusing to run on a truncated payload.`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// The API key must be unique, or the join is not a join. Checked, never assumed.
|
|
const seen = new Map()
|
|
let apiDup = 0
|
|
for (const p of posts) {
|
|
const k = key(p.name, p.country_code && p.country_code.value)
|
|
if (seen.has(k)) { apiDup++; continue }
|
|
seen.set(k, p)
|
|
}
|
|
if (apiDup) console.warn(`PRAY: ${apiDup} duplicate (name, country) key(s) on the API side — those entries are skipped.`)
|
|
|
|
// Pin-side ambiguity: any key held by more than one pin is dropped entirely.
|
|
const pins = JSON.parse(readFileSync(resolve(REPO, 'data/tables/people-group-pins.json'), 'utf8'))
|
|
const pinCount = {}
|
|
for (const [name, , , iso3] of pins.pins) {
|
|
const k = key(name, iso3)
|
|
pinCount[k] = (pinCount[k] || 0) + 1
|
|
}
|
|
|
|
const byKey = {}
|
|
let matched = 0, ambiguous = 0, unmatched = 0
|
|
for (const [k, p] of seen) {
|
|
if (!pinCount[k]) { unmatched++; continue }
|
|
if (pinCount[k] > 1) { ambiguous++; continue }
|
|
byKey[k] = {
|
|
u: `${SITE}/${encodeURIComponent(p.slug)}?source=doxalife&utm_source=agwm`,
|
|
n: p.name,
|
|
praying: Number(p.people_praying) || 0,
|
|
committed: Number(p.people_committed) || 0,
|
|
}
|
|
matched++
|
|
}
|
|
|
|
writeSnapshot(
|
|
OUT,
|
|
`${JSON.stringify({
|
|
_note: 'Prayer pages from doxa.life, keyed by (people-group name, country) — the ONE join here that is not by language, because you pray for a people in a place, not for a language. Ambiguous keys are dropped, never guessed. pray.doxa.life answers 200 with identical bytes for any slug, so existence comes from this API listing and never from the page.',
|
|
source: API,
|
|
site: SITE,
|
|
generated: new Date().toISOString().slice(0, 10),
|
|
counts: { api: posts.length, apiDuplicateKeys: apiDup, matched, ambiguousPinKeysDropped: ambiguous, noPinForEntry: unmatched },
|
|
byKey,
|
|
}, null, 1)}\n`,
|
|
(j) => Object.keys(j.byKey || {}).length,
|
|
'prayer links',
|
|
)
|
|
console.log(`PRAY: ${posts.length} entries -> ${matched} joined to a unique pin`)
|
|
console.log(` skipped: ${ambiguous} ambiguous pin key(s), ${unmatched} entr(ies) with no pin here`)
|