You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For organisations with showDueChecklistOnDashboard enabled, the Due checklist section is missing from My Dashboard on 18.0. On 17.x it showed a real count.
It fails silently — no error and no empty card, the row simply is not rendered — which is why it went unnoticed for months.
Raised during code review of #2022. It is not caused by that card's fix; it is an independent #1865 regression that the #2022 work made fully visible. Filed separately because #2022's acceptance criteria do not cover it (see Why this is not #2022) and because it needs a different fix.
What must be true when this is done
An org with showDueChecklistOnDashboard enabled sees the Due checklist section on My Dashboard with a correct count, on 18.0, for the same data 17.x counted.
Tapping it opens the checklist listing with the matching subjects.
An org without the setting still sees no Due checklist section, and pays nothing for it.
The count honours the same filters as the other cards — subject type, location, program, and a My Dashboard custom filter.
Verified on Realm and on SQLite.
The card never displays a count it cannot open a listing for.
The regression
80085652a (#1865 | Lazy-load MyDashboard entity lists on card tap, 1 Apr 2026) made the entity lists lazy to remove a 40s white screen, and gave the six visit/registration cards real count queries (countScheduledVisits and friends — SELECT COUNT on SQLite, .length on Realm).
The due checklist got no query. It got a placeholder:
dueChecklist: 0,
and MyDashboardView.renderableVisits (MyDashboardView.js:110) drops any row whose count is 0:
Before #1865 every card took its number the same way — _.mapValues(queryResult, v => v && v.length || 0) — so the due checklist number was simply the length of the list dueChecklistForDefaultDashboard returned.
Measured, not inferred
Driving MyDashboardActions.onLoad against a stubbed IndividualService whose dueChecklistForDefaultDashboard returns three due subjects, across first load / re-entry / manual refresh:
The card was already gone before the #2022 fix landed, in the configuration #2022 specifies for testing — auto-refresh on, the default, and the setting its reproduction used (anjali@jss_uat, recorded as disableAutoRefresh: false). The #2022 fix changed only the auto-refresh-off column, normalising an undefined count to 0, so a row that rendered with a blank number became a hidden row: a change between two already-broken states, not the cause.
#2022's acceptance criteria concern the six cards in its recorded run — Scheduled, Overdue, Registrations, Enrolments, Visits, Total. Its test org (nupoork@ntest) has no checklists, so Due checklist appears in none of its columns.
Its AC 7 reads "The same steps on a 17.1 build and an 18.0 build produce the same numbers", and "the same steps" is anchored by that observed-run table. Reading it to cover a card absent from those steps stretches it past what was measured on either build.
Why this cannot be a count-only query
The obvious fix — give the due checklist a countDueChecklist alongside the other six — is not available. Two independent reasons:
1. "Due" is not stored; it is computed.dueChecklists runs a rule per checklist item:
Nothing in the database records that an item is due — it is derived at read time from the item's definition and the subject's data. There is no SELECT COUNT to write, and obtaining the count means loading the enrolments and evaluating every item. Counting costs exactly what building the list costs.
2. This card does not rebuild its list on tap. Every other card dispatches ON_LIST_LOAD. This one does not — StatusCountRow.js:36 hands the already-built list straight to ChecklistListingView:
so the hydrated list has to be in reducer state or the drill-down opens on nothing.
Together these rule the approach out: a count-only query would cost the same as the list and produce a tappable card leading to an empty listing — worse than the card being hidden. Hence AC 7 above.
A constraint worth knowing, not addressed here
dueChecklists hardcodes the program name:
.filtered('voided = false AND individual.voided = false AND program.name = $0','Child')
The card only ever considers enrolments in a program literally named Child. This is long-standing — it predates #1865 and is untouched by this card. It has two consequences: it is what bounds the scan, and an org whose checklist programme is named anything else gets nothing from this card. Changing it would change what the card counts, which is a product decision rather than a regression fix, so it belongs in its own card if it matters.
The fix
9d5e00eb9 on 18.0.
The due-checklist list is loaded alongside the other counts on a refetching load, and the number is derived from it.
Situation
Behaviour
Org without the setting
dueChecklistForDefaultDashboard returns empty before it queries → count 0 → row hidden → no cost
Org with the setting
Scan Child enrolments, evaluate the rule, keep the list in state, count distinct subjects
Non-refetching load (plain re-entry)
Count derived from the list in state, never from the cached card
Custom filter applied
dueChecklistFilter: 'individual.uuid' narrows it like every other card, chunked at 500
The number never comes from the cached card. If it did, a restart with auto-refresh off would show a cached non-zero count with no list behind it, and the card would open an empty listing (AC 7). Deriving it from the list in hand means no list ⇒ count 0 ⇒ row hidden.
The cost #1865 removed is reintroduced only for orgs that have the feature on, bounded to Child enrolments, and only on refetching loads — app start, sync, date toggle, filter change, manual refresh. Plain re-entry still serves the cache, so #2022's fix is untouched.
Tests (MyDashboardOnLoadTest.js): the count is people not rows and the list is carried for the drill-down; the row stays hidden for an org without checklists; the number survives re-entry; a due count is never shown without a list behind it; the custom filter restricts it.
Still to verify on a device: AC 1, 2, 3 and 6, against an org that actually uses checklists, on Realm and on SQLite. The tests stub IndividualService, so they prove the wiring, not the query.
onListLoad's mis-call.MyDashboardActions.js:263 calls dueChecklistForDefaultDashboard with seven arguments against its two-parameter signature (date, queryAdditions), landing [] in queryAdditions and dropping the filters. That branch is unreachable while StatusCountRow bypasses ON_LIST_LOAD, so it is left alone rather than fixed blind.
Summary
For organisations with
showDueChecklistOnDashboardenabled, the Due checklist section is missing from My Dashboard on 18.0. On 17.x it showed a real count.It fails silently — no error and no empty card, the row simply is not rendered — which is why it went unnoticed for months.
Raised during code review of #2022. It is not caused by that card's fix; it is an independent #1865 regression that the #2022 work made fully visible. Filed separately because #2022's acceptance criteria do not cover it (see Why this is not #2022) and because it needs a different fix.
What must be true when this is done
showDueChecklistOnDashboardenabled sees the Due checklist section on My Dashboard with a correct count, on 18.0, for the same data 17.x counted.The regression
80085652a(#1865 | Lazy-load MyDashboard entity lists on card tap, 1 Apr 2026) made the entity lists lazy to remove a 40s white screen, and gave the six visit/registration cards real count queries (countScheduledVisitsand friends —SELECT COUNTon SQLite,.lengthon Realm).The due checklist got no query. It got a placeholder:
and
MyDashboardView.renderableVisits(MyDashboardView.js:110) drops any row whose count is0:so the section stops rendering entirely.
Before #1865 every card took its number the same way —
_.mapValues(queryResult, v => v && v.length || 0)— so the due checklist number was simply the length of the listdueChecklistForDefaultDashboardreturned.Measured, not inferred
Driving
MyDashboardActions.onLoadagainst a stubbedIndividualServicewhosedueChecklistForDefaultDashboardreturns three due subjects, across first load / re-entry / manual refresh:beb37c350~1, 17.x)33dd201d5ee)0— row hiddenundefined— row rendered with a blank number;0after a manual refresh35825dbeb)0— row hidden0— row hiddenThe card was already gone before the #2022 fix landed, in the configuration #2022 specifies for testing — auto-refresh on, the default, and the setting its reproduction used (
anjali@jss_uat, recorded asdisableAutoRefresh: false). The #2022 fix changed only the auto-refresh-off column, normalising an undefined count to0, so a row that rendered with a blank number became a hidden row: a change between two already-broken states, not the cause.Why this is not #2022
#2022's acceptance criteria concern the six cards in its recorded run — Scheduled, Overdue, Registrations, Enrolments, Visits, Total. Its test org (
nupoork@ntest) has no checklists, so Due checklist appears in none of its columns.Its AC 7 reads "The same steps on a 17.1 build and an 18.0 build produce the same numbers", and "the same steps" is anchored by that observed-run table. Reading it to cover a card absent from those steps stretches it past what was measured on either build.
Why this cannot be a count-only query
The obvious fix — give the due checklist a
countDueChecklistalongside the other six — is not available. Two independent reasons:1. "Due" is not stored; it is computed.
dueChecklistsruns a rule per checklist item:Nothing in the database records that an item is due — it is derived at read time from the item's definition and the subject's data. There is no
SELECT COUNTto write, and obtaining the count means loading the enrolments and evaluating every item. Counting costs exactly what building the list costs.2. This card does not rebuild its list on tap. Every other card dispatches
ON_LIST_LOAD. This one does not —StatusCountRow.js:36hands the already-built list straight toChecklistListingView:so the hydrated list has to be in reducer state or the drill-down opens on nothing.
Together these rule the approach out: a count-only query would cost the same as the list and produce a tappable card leading to an empty listing — worse than the card being hidden. Hence AC 7 above.
A constraint worth knowing, not addressed here
dueChecklistshardcodes the program name:The card only ever considers enrolments in a program literally named
Child. This is long-standing — it predates #1865 and is untouched by this card. It has two consequences: it is what bounds the scan, and an org whose checklist programme is named anything else gets nothing from this card. Changing it would change what the card counts, which is a product decision rather than a regression fix, so it belongs in its own card if it matters.The fix
9d5e00eb9on18.0.The due-checklist list is loaded alongside the other counts on a refetching load, and the number is derived from it.
dueChecklistForDefaultDashboardreturns empty before it queries → count0→ row hidden → no costdueChecklistFilter: 'individual.uuid'narrows it like every other card, chunked at 500Two deliberate choices:
0⇒ row hidden.The cost #1865 removed is reintroduced only for orgs that have the feature on, bounded to Child enrolments, and only on refetching loads — app start, sync, date toggle, filter change, manual refresh. Plain re-entry still serves the cache, so #2022's fix is untouched.
Tests (
MyDashboardOnLoadTest.js): the count is people not rows and the list is carried for the drill-down; the row stays hidden for an org without checklists; the number survives re-entry; a due count is never shown without a list behind it; the custom filter restricts it.Still to verify on a device: AC 1, 2, 3 and 6, against an org that actually uses checklists, on Realm and on SQLite. The tests stub
IndividualService, so they prove the wiring, not the query.Out of scope
program.name = 'Child'hardcoding, per above.onListLoad's mis-call.MyDashboardActions.js:263callsdueChecklistForDefaultDashboardwith seven arguments against its two-parameter signature(date, queryAdditions), landing[]inqueryAdditionsand dropping the filters. That branch is unreachable whileStatusCountRowbypassesON_LIST_LOAD, so it is left alone rather than fixed blind.