Let the deepest two tree tiers reach the grid
Some checks are pending
build-gate / build (push) Waiting to run

Cluster and People Group changed the dashboard correctly and could never move the grid,
because the fields behind them were not in the grid's fetch. They are real fields, unlike
the two family axes, which are derived and have no row field to filter against at all —
those stay unmapped rather than being aimed at a lookalike and quietly showing the wrong
rows.

Verified against the live data before mapping, since a wrong mapping is worse than no
mapping: both fields are present on 2,106 of 2,107 rows in the same value-and-label shape
as the axes already wired, with 194 and 1,736 distinct values, no collisions once
normalised, and no case where one entry's label matches another's code. Codes and names
never overlap, so either vocabulary can be accepted safely.

Measured end to end: selecting the Telugu cluster narrows the grid to 93, which is
exactly the number of rows the API holds for it.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 16:22:21 +00:00
parent 6189966a89
commit 6b29e146df
2 changed files with 37 additions and 3 deletions

View file

@ -127,12 +127,20 @@ const FACET_TO_PROP = {
country_code: 'countryCode',
primary_language: 'primaryLanguage',
engagement_status: 'engagementStatus',
// The two DEEPEST tree tiers. Their row fields are imb_reg_of_people_2 / _25, which the grid did
// not fetch — so these changed the dashboard correctly and could never move the grid. Verified
// live before mapping (2,107 rows): both present on 2106, both {value,label}, 194 and 1736
// distinct values, ZERO collisions after normalisation and ZERO label-vs-value cross-collisions.
// Unlike language_family / religion_family, which are DERIVED and stay unmapped, these are real
// fields that only needed asking for.
cluster: 'cluster',
people_group: 'peopleGroup',
}
// Empty string (not null) is the grid's documented "no filter" value for every axis, so a
// cleared facet must RESET its prop rather than leave the previous value stuck.
const filter = reactive({ query: '', wagfRegion: '', wagfBlock: '', rop1: '', religion: '', countryCode: '',
primaryLanguage: '', engagementStatus: '' })
primaryLanguage: '', engagementStatus: '', cluster: '', peopleGroup: '' })
function onDashboardFilter({ filterSet = {}, searchTerm = '' } = {}) {
filter.query = searchTerm || ''
@ -205,6 +213,8 @@ const sections = [
:country-code="filter.countryCode"
:primary-language="filter.primaryLanguage"
:engagement-status="filter.engagementStatus"
:cluster="filter.cluster"
:people-group="filter.peopleGroup"
:show-search="false" :show-filters="false" :show-total="false" />
</template>
</StackLayout>

View file

@ -137,6 +137,24 @@ const props = defineProps({
// admits Yombe rows. Country could claim a measured ZERO collisions; language cannot, and saying
// so is the point. One over-match in 863 beats an axis that cannot filter at all.
primaryLanguage: { type: String, default: '' },
// ★ THE LAST TWO REACHABLE TREE AXES. The semantic tree's deepest tiers — Cluster and People Group
// — changed the dashboard correctly but could not move the grid, because the fields they read were
// simply not in the grid's fetch. Unlike language_family and religion_family (which are DERIVED and
// have no matching row field at all, so they stay unmapped), these are real fields and only needed
// asking for.
//
// VERIFIED AGAINST THE LIVE API BEFORE MAPPING — the standing rule is never to guess a mapping, and
// the religion_family worked example in dashboard-plus-grid.vue shows why: a plausible-looking
// mapping filters confidently to the WRONG rows, which is worse than not narrowing. Measured over
// all 2,107 rows:
// imb_reg_of_people_2 (cluster) present 2106/2107 {value,label} 194 distinct
// imb_reg_of_people_25 (people group) present 2106/2107 {value,label} 1736 distinct
// collisions after norm(): 0 and 0 label-vs-value cross-collisions: 0 and 0
// Codes and labels never overlap (C0059/"Borneo-Kalimantan", 300005/"Abai Sungai"), so valueMatches
// can accept either vocabulary without over-matching — the same property that made country_code safe.
cluster: { type: String, default: '' },
peopleGroup: { type: String, default: '' },
engagementStatus: { type: String, default: '' },
// ── presentation knobs copied from UupgsList (defaults preserve today's behaviour) ──
@ -201,6 +219,12 @@ const propFilters = computed(() => {
// every row and filter the grid to zero.
if (props.primaryLanguage) f.primary_language = { value: props.primaryLanguage, label: lbl(props.primaryLanguage) }
if (props.engagementStatus) f.engagement_status = { value: props.engagementStatus, label: lbl(props.engagementStatus) }
// Same API-field-name rule again, and here the names give no hint of it: the tree calls these
// "Cluster" and "People Group", but the row fields are imb_reg_of_people_2 and
// imb_reg_of_people_25. Keying on the friendly name would read an undefined field on every row and
// filter the grid to zero — the exact failure the two comments above describe.
if (props.cluster) f.imb_reg_of_people_2 = { value: props.cluster, label: lbl(props.cluster) }
if (props.peopleGroup) f.imb_reg_of_people_25 = { value: props.peopleGroup, label: lbl(props.peopleGroup) }
return f
})
const activeFilters = computed(() => (useFilters.value ? localFilters.value : propFilters.value))
@ -516,7 +540,7 @@ async function getUUPGs() {
// primaryLanguage prop match nothing and silently filter the grid to zero rows. Verified live
// (2026-09-22): adding it keeps the response at 2,107 rows and the field is populated on 100%
// of them. Any future filter axis must be added HERE as well as to propFilters.
const url = `${prayBaseUrl.value}/api/people-groups/list?fields=name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,primary_language&lang=${languageCode.value}`
const url = `${prayBaseUrl.value}/api/people-groups/list?fields=name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,primary_language,imb_reg_of_people_2,imb_reg_of_people_25&lang=${languageCode.value}`
loading.value = true
try {
const response = await fetch(url)
@ -533,7 +557,7 @@ async function getUUPGs() {
// external prop-driven filters should re-filter the grid (when controls are hidden).
watch(() => [props.query, props.wagfRegion, props.wagfBlock, props.rop1, props.religion, props.countryCode,
props.primaryLanguage, props.engagementStatus, useSearch.value, useFilters.value], () => {
props.primaryLanguage, props.engagementStatus, props.cluster, props.peopleGroup, useSearch.value, useFilters.value], () => {
page.value = 1
filterUUPGs()
})