DRV1650-LAZY-LOCALE-IMPORT-RETRY. net::ERR_NETWORK_CHANGED killed an embedded UPG-100's load, and the card asks for the lazy locale/module loads to retry like retryableAsyncComponent does. ★ THOSE URLs DO NOT EXIST IN PRODUCTION. The console named module URLs — locale JSON with ?import, useOptionalT.js, useShadowStyles.js, useMapTheme.js. The build is IIFE with inlineDynamicImports (vite.config.js:216-220), so every one of them is inside the single bundle and is never fetched separately. They are dev-server module requests. There is no loader to wrap and nothing a reader could hit, so the card cannot be done as written, and saying so is most of its value. ★ THE FAILURE CLASS IS REAL ONE LAYER DOWN, and nobody had covered it. A built bundle still makes runtime requests — the boundaries geojson (twice, two composables), the people-group list, prayer statistics — and NOT ONE of them retried. The same wifi switch that killed the Driver's dev load takes out whichever of those is in flight, and the feature is simply ABSENT: an empty map, a missing country outline, a statistic that never arrives, with nothing a reader can act on. That is the same defect he reported, at the layer that ships. retryFetch is the fetch-shaped sibling of retryableAsyncComponent and deliberately shares its policy — 3 attempts, growing backoff — because it survives the same event, and two answers to "how long is a blip" would be worse than one. It retries TRANSPORT failures only. A 404 or a 500 RESOLVES; retrying those would hide real breakage and hammer a struggling server, so the response is handed back untouched. An abort is a decision, not a blip, and is never retried. check-retry-fetch asserts the behaviour AND that the shared data paths use it. Its "persistently dead endpoint still fails" arm originally only checked that it threw — which an UNBOUNDED retry loop also does, eventually. Caught by this gate's own negative test; it now counts the attempts, because the property is bounded give-up and the bound is the number. Negative-tested: retrying an abort, changing the default attempt count, and reverting a data path to bare fetch each turn it red. NOT wired: usePosterPrints, useCsvExport, PeopleGroupDetail and the token service also fetch at runtime. They belong to other surfaces or have their own failure handling, and I am not editing them blind — the list is in the report.
91 lines
4.5 KiB
JavaScript
91 lines
4.5 KiB
JavaScript
/**
|
|
* usePrayerStatistics.js — the ONE authoritative prayer-coverage source of truth.
|
|
*
|
|
* The prayer-tab legend used to COUNT loaded pins client-side (peoplePraying > 0),
|
|
* which produced 435 — but the parent doxa-life site shows 601. They diverged for
|
|
* two reasons: (1) a different field — the parent counts `people_committed > 0`, the
|
|
* client scan counted `people_praying > 0`; and (2) a different record set — the map
|
|
* only counts the pins it has loaded. The server `/api/people-groups/statistics`
|
|
* aggregate is computed over the FULL dataset and is what the parent trusts, so every
|
|
* surface (research-map, simple-map, dashboard, parent site) must read its numbers
|
|
* from HERE instead of recomputing locally.
|
|
*
|
|
* The upstream endpoint returns BOTH metrics; the parent treats the *committed* one as
|
|
* canonical (its server route renames `total_with_prayer_committed` → `total_with_prayer`
|
|
* and `total_with_100_committed` → `total_with_full_prayer`). We mirror that exactly:
|
|
*
|
|
* withPrayer = total_with_prayer_committed (601 — "1+ committed to pray")
|
|
* withFullPrayer = total_with_100_committed (0 — full coverage, >= 100 committed)
|
|
* total = total (2106)
|
|
*
|
|
* → none = total - withPrayer (1505), partial = withPrayer - withFullPrayer (601),
|
|
* full = withFullPrayer (0). These are the 601 / 1505 / 0 the parent shows.
|
|
*
|
|
* Shared-layer contract: the fetch is a MODULE-LEVEL singleton, so all profiles/legends
|
|
* on a page share ONE request and ONE reactive result — the single source of truth.
|
|
* Uses the same shared data config as the rest of the app (sources.json activeSource
|
|
* endpoint + getApiBaseUrl); no DOM, shadow-DOM safe.
|
|
*/
|
|
import { retryFetch } from './retryFetch.js'
|
|
import { ref } from 'vue'
|
|
import { getApiBaseUrl } from '../api/apiBaseUrl.js'
|
|
import sourcesConfig from '../api/sources.json'
|
|
|
|
// ── Module-level singleton state (shared across every usePrayerStatistics() call) ──
|
|
const total = ref(0)
|
|
const withPrayer = ref(0) // total_with_prayer_committed (partial-or-full, committed)
|
|
const withFullPrayer = ref(0) // total_with_100_committed
|
|
const loaded = ref(false)
|
|
const loading = ref(false)
|
|
const error = ref(null)
|
|
let inflight = null // dedupe concurrent loads → one network request
|
|
|
|
async function load() {
|
|
if (loaded.value) return // already have the authoritative numbers
|
|
if (inflight) return inflight // a load is already in progress — await it
|
|
loading.value = true
|
|
error.value = null
|
|
inflight = (async () => {
|
|
try {
|
|
const src = sourcesConfig?.sources?.[sourcesConfig.activeSource] || {}
|
|
const base = getApiBaseUrl()
|
|
const endpoint = src.endpoints?.statistics || '/api/people-groups/statistics'
|
|
const res = await retryFetch(`${base}${endpoint}`, { cache: 'no-store' })
|
|
if (!res.ok) throw new Error(`statistics HTTP ${res.status}`)
|
|
const d = await res.json()
|
|
total.value = Number(d.total || 0)
|
|
// Committed is canonical (matches the parent site). Fall back to the
|
|
// prayer-based fields only if the committed ones are absent, so an older
|
|
// upstream shape still yields a number rather than 0.
|
|
withPrayer.value = Number(d.total_with_prayer_committed ?? d.total_with_prayer ?? 0)
|
|
withFullPrayer.value = Number(d.total_with_100_committed ?? d.total_with_full_prayer ?? 0)
|
|
loaded.value = true
|
|
} catch (e) {
|
|
error.value = (e && e.message) ? e.message : String(e)
|
|
} finally {
|
|
loading.value = false
|
|
inflight = null
|
|
}
|
|
})()
|
|
return inflight
|
|
}
|
|
|
|
/**
|
|
* @returns reactive prayer-coverage aggregate + derived per-bucket counts.
|
|
* total, withPrayer, withFullPrayer — raw authoritative numbers
|
|
* noPrayerCount / hasPrayerCount / fullPrayerCount — the 1505 / 601 / 0 buckets
|
|
* loaded — false until the fetch resolves (consumers can fall back meanwhile)
|
|
* load() — idempotent; safe to call from every consumer (deduped)
|
|
*/
|
|
export function usePrayerStatistics() {
|
|
return {
|
|
total, withPrayer, withFullPrayer, loaded, loading, error, load,
|
|
// Derived, EXCLUSIVE buckets that sum to `total` (partial excludes full),
|
|
// mirroring the parent's "601 partial / 0 full / 1505 none".
|
|
noPrayerCount: () => Math.max(0, total.value - withPrayer.value),
|
|
hasPrayerCount: () => Math.max(0, withPrayer.value - withFullPrayer.value),
|
|
fullPrayerCount: () => withFullPrayer.value,
|
|
}
|
|
}
|
|
|
|
export default usePrayerStatistics
|