From ce03cdb7c116bf76e444149a1a12ce1644bb3b3d Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Wed, 29 Jul 2026 22:34:26 -0700 Subject: [PATCH 1/6] Dashboard: stop inflating the Overdue count, and say when reminders are dropped Two problems found while tracing "why am I only seeing 3 reminders". The Overdue section counted reminders it did not show. When maxItemsToShowInSection left fewer slots than there were overdue reminders, the extras were sliced off the displayed list but still added to totalOverdue, so the header claimed more items than the section contained and the difference was unreachable. Count what was actually assigned, and warn when some do not fit. More seriously, a reminder whose bucket has no visible host section is discarded with no trace. In a perspective with Yesterday, Tomorrow and Overdue all off, 6 of 9 reminders vanished and only the 3 undated ones appeared in REM -- indistinguishable, from the outside, from having only 3 reminders. getSomeSectionsData now names them: 1 yesterday (Yesterday section off, and Overdue off so the spill has nowhere to land); 3 overdue (Overdue section off); 2 tomorrow (Tomorrow section off) reminder(s) have no visible section and will not be shown anywhere Note the yesterday case has to check that Overdue is actually visible, not just that the spill happened: yesterday spills into Overdue, so treating the spill as a home hid the loss whenever Overdue was itself off. Also log each reminder placement with its target section, which is what made the routing traceable in the first place. Verified against a live NotePlan across three perspectives: 11 fetched, 2 dated beyond tomorrow and dropped by design, 9 bucketed, and 9 displayed in z_ALL_ON (1 yesterday + 2 tomorrow + 3 overdue + 3 undated) with no duplicates. Dashboard tests 217/217. Co-Authored-By: Claude Opus 5 (1M context) --- jgclark.Dashboard/src/dataGeneration.js | 19 ++++++++++++++++++- .../src/dataGenerationOverdue.js | 10 +++++++++- .../src/dataGenerationReminders.js | 3 +++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/jgclark.Dashboard/src/dataGeneration.js b/jgclark.Dashboard/src/dataGeneration.js index 82fc71bee..6889a8035 100644 --- a/jgclark.Dashboard/src/dataGeneration.js +++ b/jgclark.Dashboard/src/dataGeneration.js @@ -25,7 +25,7 @@ import { getTaggedSectionData } from './dataGenerationTags' import { getLastWeekSectionData, getThisWeekSectionData } from './dataGenerationWeeks' import { getTagSectionDetails } from './react/components/Section/sectionHelpers' import { getNestedValue, setNestedValue } from '@helpers/dataManipulation' -import { logDebug, logError } from '@helpers/dev' +import { logDebug, logError, logWarn } from '@helpers/dev' import { getLiveWindowRect, getStoredWindowRect, rectToString } from '@helpers/NPWindows' //----------------------------------------------------------------- @@ -119,6 +119,23 @@ export async function getSomeSectionsData( const overdueReminderItems = undatedOverdueRemindersEnabled ? remindersData.overdueItems.concat(yesterdaySpillToOverdue) : [] + // A reminder only reaches the UI if the section that hosts its bucket is on. + // When every candidate host is off it is dropped with no trace, which reads as + // "my reminders are missing" -- so say so out loud. Yesterday spills to Overdue, + // but if Overdue is off too there is nowhere left for it to go. + // Spilling to Overdue only counts as a home if Overdue is actually on screen. + const overdueSectionVisible = Boolean(config.showOverdueSection) + const yesterdayHomeless = + remindersData.yesterdayItems.length > 0 && yesterdayForDaySection.length === 0 && !(yesterdaySpillToOverdue.length > 0 && overdueSectionVisible) + const overdueHomeless = remindersData.overdueItems.length > 0 && !overdueSectionVisible + const tomorrowHomeless = remindersData.tomorrowItems.length > 0 && !config.showTomorrowSection + if (yesterdayHomeless || overdueHomeless || tomorrowHomeless) { + const parts = [] + if (yesterdayHomeless) parts.push(`${String(remindersData.yesterdayItems.length)} yesterday (Yesterday section off${overdueSectionVisible ? '' : ', and Overdue off so the spill has nowhere to land'})`) + if (overdueHomeless) parts.push(`${String(remindersData.overdueItems.length)} overdue (Overdue section off)`) + if (tomorrowHomeless) parts.push(`${String(remindersData.tomorrowItems.length)} tomorrow (Tomorrow section off)`) + logWarn('getSomeSectionsData', `- ${parts.join('; ')} reminder(s) have no visible section and will not be shown anywhere`) + } // DT and TB sections are now generated separately but share paragraph data fetching if (sectionCodesToGet.includes('DT')) { diff --git a/jgclark.Dashboard/src/dataGenerationOverdue.js b/jgclark.Dashboard/src/dataGenerationOverdue.js index 723348abd..706e5bbb0 100644 --- a/jgclark.Dashboard/src/dataGenerationOverdue.js +++ b/jgclark.Dashboard/src/dataGenerationOverdue.js @@ -146,7 +146,15 @@ export async function getOverdueSectionData( const assigned = assignReminderItemsToSection(remindersToAdd, thisSectionCode, thisSectionCode, itemCount) items.push(...assigned) itemCount += assigned.length - totalOverdue += overdueReminderItems.length + // Count only what was actually added. Adding the full incoming length + // inflated the header count whenever maxItemsToShowInSection left fewer + // slots than there were reminders, so the section claimed more items + // than it showed and the extras were silently unreachable. + totalOverdue += assigned.length + const droppedForSlots = overdueReminderItems.length - remindersToAdd.length + if (droppedForSlots > 0) { + logWarn('getOverdueSectionData', `- ${String(droppedForSlots)} overdue reminder(s) did not fit in maxItemsToShowInSection=${String(maxInSection ?? 24)} and are not shown anywhere`) + } logDebug('getOverdueSectionData', `- added ${String(assigned.length)} of ${String(overdueReminderItems.length)} overdue reminder(s)`) } } diff --git a/jgclark.Dashboard/src/dataGenerationReminders.js b/jgclark.Dashboard/src/dataGenerationReminders.js index e42f92e1a..fcc2abdc9 100644 --- a/jgclark.Dashboard/src/dataGenerationReminders.js +++ b/jgclark.Dashboard/src/dataGenerationReminders.js @@ -346,6 +346,9 @@ export function assignReminderItemsToSection( idPrefix: string, startIndex: number = 0, ): Array { + if (reminderItems.length > 0) { + logDebug('assignReminderItemsToSection', `- placing ${String(reminderItems.length)} reminder(s) into section ${String(sectionCode)}`) + } return reminderItems.map((item, i) => ({ ...item, ID: `${idPrefix}-${startIndex + i}`, From 5416542292d76fbec7db4dec56f5c36218769c24 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Wed, 29 Jul 2026 22:40:00 -0700 Subject: [PATCH 2/6] Dashboard: fall back to the Undated/Overdue Reminders section rather than dropping reminders A dated reminder whose host section was switched off was discarded. With Yesterday, Tomorrow and Overdue all off, 6 of 9 reminders disappeared and only the 3 undated ones showed, which is indistinguishable from having 3 reminders. Yesterday and overdue reminders now fall back to the REM section, whose setting is literally "Show Undated/Overdue Reminders": an overdue reminder belongs there by name, and a yesterday one is overdue in every sense that matters once there is no Yesterday section to hold it. Routing order is own section -> Overdue -> REM, so nothing is adopted while a more specific section is visible. Tomorrow is deliberately excluded. A future reminder is neither undated nor overdue, and switching off the Tomorrow section is a reasonable way of saying you do not want to see it yet. Those are still reported by the warning, which is now narrowed to reminders the fallback genuinely cannot rescue. Verified live across three perspectives: - Today-All (all three off): REM adopts 4 (3 overdue + 1 yesterday), so 7 reminders show instead of 3; only the 2 tomorrow ones remain hidden, and are warned about. 7 + 2 = 9 bucketed. - z_ALL_ON: no adoption at all, DY gets 1 and DO gets 2 as before. - z_OVERDUE Only: no adoption; Overdue takes 4 (3 overdue + the yesterday spill). Dashboard tests 220/220, and no new Flow errors (4 in these files before and after). Co-Authored-By: Claude Opus 5 (1M context) --- jgclark.Dashboard/src/dataGeneration.js | 21 ++++++++--------- .../src/dataGenerationReminders.js | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/jgclark.Dashboard/src/dataGeneration.js b/jgclark.Dashboard/src/dataGeneration.js index 6889a8035..7c4020443 100644 --- a/jgclark.Dashboard/src/dataGeneration.js +++ b/jgclark.Dashboard/src/dataGeneration.js @@ -119,21 +119,20 @@ export async function getSomeSectionsData( const overdueReminderItems = undatedOverdueRemindersEnabled ? remindersData.overdueItems.concat(yesterdaySpillToOverdue) : [] - // A reminder only reaches the UI if the section that hosts its bucket is on. - // When every candidate host is off it is dropped with no trace, which reads as - // "my reminders are missing" -- so say so out loud. Yesterday spills to Overdue, - // but if Overdue is off too there is nowhere left for it to go. - // Spilling to Overdue only counts as a home if Overdue is actually on screen. - const overdueSectionVisible = Boolean(config.showOverdueSection) + // A reminder only reaches the UI if some section hosts it. Yesterday and overdue + // reminders now fall back to the REM ("Undated/Overdue Reminders") section when + // their own section is off, so they are only truly lost if REM is off too. + // Tomorrow has no fallback by design, so it is lost whenever Tomorrow is off. + const remCanHost = undatedOverdueRemindersEnabled const yesterdayHomeless = - remindersData.yesterdayItems.length > 0 && yesterdayForDaySection.length === 0 && !(yesterdaySpillToOverdue.length > 0 && overdueSectionVisible) - const overdueHomeless = remindersData.overdueItems.length > 0 && !overdueSectionVisible + remindersData.yesterdayItems.length > 0 && yesterdayForDaySection.length === 0 && !Boolean(config.showOverdueSection) && !remCanHost + const overdueHomeless = remindersData.overdueItems.length > 0 && !Boolean(config.showOverdueSection) && !remCanHost const tomorrowHomeless = remindersData.tomorrowItems.length > 0 && !config.showTomorrowSection if (yesterdayHomeless || overdueHomeless || tomorrowHomeless) { const parts = [] - if (yesterdayHomeless) parts.push(`${String(remindersData.yesterdayItems.length)} yesterday (Yesterday section off${overdueSectionVisible ? '' : ', and Overdue off so the spill has nowhere to land'})`) - if (overdueHomeless) parts.push(`${String(remindersData.overdueItems.length)} overdue (Overdue section off)`) - if (tomorrowHomeless) parts.push(`${String(remindersData.tomorrowItems.length)} tomorrow (Tomorrow section off)`) + if (yesterdayHomeless) parts.push(`${String(remindersData.yesterdayItems.length)} yesterday`) + if (overdueHomeless) parts.push(`${String(remindersData.overdueItems.length)} overdue`) + if (tomorrowHomeless) parts.push(`${String(remindersData.tomorrowItems.length)} tomorrow (no fallback: Tomorrow section off)`) logWarn('getSomeSectionsData', `- ${parts.join('; ')} reminder(s) have no visible section and will not be shown anywhere`) } diff --git a/jgclark.Dashboard/src/dataGenerationReminders.js b/jgclark.Dashboard/src/dataGenerationReminders.js index fcc2abdc9..700e20214 100644 --- a/jgclark.Dashboard/src/dataGenerationReminders.js +++ b/jgclark.Dashboard/src/dataGenerationReminders.js @@ -549,6 +549,29 @@ export async function getRemindersGeneratedData( const overdueItems = undatedOverdueRemindersEnabled ? buckets.overdueItems : [] let undatedItems = undatedOverdueRemindersEnabled ? buckets.undatedItems : [] + // Fallback chain, so a dated reminder is never silently discarded just because + // the section that would have hosted it is switched off. This section's setting + // is "Show Undated/Overdue Reminders", so an overdue reminder belongs here by + // name, and a yesterday one is overdue in every sense that matters once there is + // no Yesterday section to put it in. Order: own section -> Overdue -> here. + // Tomorrow is deliberately excluded: a future reminder is neither undated nor + // overdue, and hiding the Tomorrow section is a reasonable way to say you don't + // want to see it yet. + if (undatedOverdueRemindersEnabled) { + const overdueSectionVisible = Boolean(config.showOverdueSection) + const fallbackItems: Array = [] + if (!overdueSectionVisible && buckets.overdueItems.length > 0) { + fallbackItems.push(...buckets.overdueItems) + } + if (!config.showYesterdaySection && !overdueSectionVisible && buckets.yesterdayItems.length > 0) { + fallbackItems.push(...buckets.yesterdayItems) + } + if (fallbackItems.length > 0) { + logDebug('getRemindersGeneratedData', `- REM fallback: adopting ${String(fallbackItems.length)} reminder(s) whose own section is off (overdueVisible=${String(overdueSectionVisible)} yesterdayVisible=${String(Boolean(config.showYesterdaySection))})`) + undatedItems = sortReminderSectionItems(undatedItems.concat(fallbackItems)) + } + } + const maxInSection = config.maxItemsToShowInSection ?? 24 const totalUndatedCount = undatedItems.length if (totalUndatedCount > maxInSection) { From d1453167fc03d3e2cd106c35f109ef9e43be79dc Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Wed, 29 Jul 2026 22:55:44 -0700 Subject: [PATCH 3/6] Dashboard: don't let the REM fallback resurrect reminders hidden by Show Current Reminders The fallback added in the previous commit adopted yesterday's reminders into the REM section whenever the Yesterday and Overdue sections were both off -- including when "Show Current Reminders" was itself off. That toggle's description is "Show or hide reminders due today, yesterday, or tomorrow", so adopting a yesterday reminder there put back an item the user had explicitly hidden. Yesterday is a current reminder, so its fallback is now gated on that toggle. Overdue is unaffected: it belongs to "Show Undated/Overdue Reminders", which is the setting the REM section is named for. Verified in a perspective with Show Current Reminders off, Yesterday off and Overdue off: adoption drops from 4 to 3, i.e. the 3 overdue reminders are still rescued while the yesterday one stays hidden as requested. Dashboard tests 220/220. Co-Authored-By: Claude Opus 5 (1M context) --- jgclark.Dashboard/src/dataGenerationReminders.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jgclark.Dashboard/src/dataGenerationReminders.js b/jgclark.Dashboard/src/dataGenerationReminders.js index 700e20214..422a1e660 100644 --- a/jgclark.Dashboard/src/dataGenerationReminders.js +++ b/jgclark.Dashboard/src/dataGenerationReminders.js @@ -563,7 +563,10 @@ export async function getRemindersGeneratedData( if (!overdueSectionVisible && buckets.overdueItems.length > 0) { fallbackItems.push(...buckets.overdueItems) } - if (!config.showYesterdaySection && !overdueSectionVisible && buckets.yesterdayItems.length > 0) { + // Yesterday is a *current* reminder, so it stays subject to "Show Current + // Reminders" -- adopting it here when that toggle is off would resurrect an + // item the user explicitly hid ("reminders due today, yesterday, or tomorrow"). + if (currentRemindersEnabled && !config.showYesterdaySection && !overdueSectionVisible && buckets.yesterdayItems.length > 0) { fallbackItems.push(...buckets.yesterdayItems) } if (fallbackItems.length > 0) { From 24ac1f5484e0b2d0a60fcf73d85619ff3a9f517f Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Wed, 29 Jul 2026 23:27:41 -0700 Subject: [PATCH 4/6] Dashboard: fall back to private notes when no configured teamspace is reachable A user's Dashboard showed all 9 reminders and zero tasks. Cause: includedTeamspaces held 6 teamspace IDs and not 'private'. Private notes are only read when 'private' appears in that list, so every note was filtered out before a single task could be read -- reminders were unaffected because they come from the Reminders app, not from notes. The visible symptom was "8 backlinked paras found" immediately followed by "0 refs". The list was also unfixable from the UI: the user is signed out of Spaces, so "Spaces to Include" correctly reports "You are not a member of any Spaces" and offers nothing to click. The six IDs were stale, pointing at spaces that can no longer be resolved. The two existing guards both miss this. `?? ['private']` only applies when the setting is absent, and the `length === 0` check only when it is empty; a non-empty list of unreachable IDs sails through both and silently matches nothing. resolveAllowedTeamspaceIDs() now discards IDs that no longer exist, and when nothing survives falls back to ['private'] with a warning naming the setting and the likely cause. Deliberate configurations are untouched: absent still means private-only, an empty list still means don't filter, and a list of genuinely reachable teamspaces without 'private' still excludes private notes, because that is a real thing to ask for. All four call sites now share the resolver instead of repeating the `?? ['private']` idiom. Verified live on the affected setup: the warning fires, the teamspace filter goes from 0 of 1 notes to 1 of 1, and Today returns 4 open items again -- without editing the stored settings, so existing configs heal themselves. Dashboard tests 220/220. Co-Authored-By: Claude Opus 5 (1M context) --- jgclark.Dashboard/src/dashboardHelpers.js | 47 +++++++++++++++++++-- jgclark.Dashboard/src/dataGenerationTags.js | 4 +- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/jgclark.Dashboard/src/dashboardHelpers.js b/jgclark.Dashboard/src/dashboardHelpers.js index 301a5136e..b3260019f 100644 --- a/jgclark.Dashboard/src/dashboardHelpers.js +++ b/jgclark.Dashboard/src/dashboardHelpers.js @@ -707,7 +707,7 @@ export function getOpenItemParasForTimePeriod( const { matchingNotes, possTimePeriodNote } = getMatchingCalendarNotes(NPCalendarFilenameStr) // Filter notes by allowed teamspaces - const allowedTeamspaceIDs = dashboardSettings.includedTeamspaces ?? ['private'] + const allowedTeamspaceIDs = resolveAllowedTeamspaceIDs(dashboardSettings) const filteredMatchingNotes = matchingNotes.filter((note) => isNoteFromAllowedTeamspace(note, allowedTeamspaceIDs)) logDebug('getOpenItemParasForTimePeriod', `- after teamspace filter: ${filteredMatchingNotes.length} of ${matchingNotes.length} notes`) @@ -1119,6 +1119,47 @@ export function isLineDisallowedByIgnoreTerms(lineContent: string, ignoreItemsWi return matchFound } +/** + * Resolve which teamspaces this config should read notes from, discarding IDs that + * no longer exist. + * + * Why this exists: `includedTeamspaces` is a plain list of IDs, and 'private' has to + * appear in it for your own notes to be read at all. So a list holding only teamspace + * IDs silently hides every private note -- which for most users is every note they + * have. That is survivable while the IDs are real, because it is what you asked for. + * It is not survivable when the IDs are stale: signing out of Spaces (or leaving them) + * leaves a list that matches nothing, the Dashboard shows no tasks at all, and the + * settings UI reports "You are not a member of any Spaces" so there is nothing to + * click to undo it. Seen in the wild: 6 unreachable IDs, no 'private', zero tasks. + * + * When nothing in the list is reachable, the only sensible reading is private notes. + * @param {TDashboardSettings} dashboardSettings + * @returns {Array} teamspace IDs to allow, possibly healed to ['private'] + */ +export function resolveAllowedTeamspaceIDs(dashboardSettings: TDashboardSettings): Array { + const configured = dashboardSettings.includedTeamspaces + // Absent means "private only"; an explicitly empty list means "don't filter". + // Both are long-standing behaviour, so leave them alone. + if (!configured) return ['private'] + if (configured.length === 0) return configured + + let existingIDs: Array = [] + try { + existingIDs = getAllTeamspaceIDsAndTitles().map((t) => t.id) + } catch (err) { + // No teamspace API / not signed in: treat every configured ID as unreachable + existingIDs = [] + } + const reachable = configured.filter((id) => id === 'private' || existingIDs.includes(id)) + if (reachable.length > 0) return reachable + + logWarn( + 'resolveAllowedTeamspaceIDs', + `includedTeamspaces lists ${String(configured.length)} teamspace(s) but none are reachable and 'private' is not among them, so no note could ever match. Falling back to private notes. Check the "Spaces to Include" setting; you may be signed out of Spaces.`, + ) + return ['private'] +} + /** * Check if a note is from an allowed teamspace based on dashboard settings. * If no teamspaces specified, allow all (backward compatibility). @@ -1151,7 +1192,7 @@ export function filterNotesByAllowedTeamspaces( notes: Array, dashboardSettings: TDashboardSettings ): Array { - const allowedTeamspaceIDs = dashboardSettings.includedTeamspaces ?? ['private'] + const allowedTeamspaceIDs = resolveAllowedTeamspaceIDs(dashboardSettings) return notes.filter((note) => isNoteFromAllowedTeamspace(note, allowedTeamspaceIDs)) } @@ -1191,7 +1232,7 @@ export function filterParasByAllowedTeamspaces( startTime: Date, functionName: string ): Array { - const allowedTeamspaceIDs = dashboardSettings.includedTeamspaces ?? ['private'] + const allowedTeamspaceIDs = resolveAllowedTeamspaceIDs(dashboardSettings) const filteredParas = paras.filter((p) => { const note = getNoteFromPara(p) if (!note) { diff --git a/jgclark.Dashboard/src/dataGenerationTags.js b/jgclark.Dashboard/src/dataGenerationTags.js index 4669fea35..dd12a074f 100644 --- a/jgclark.Dashboard/src/dataGenerationTags.js +++ b/jgclark.Dashboard/src/dataGenerationTags.js @@ -6,7 +6,7 @@ import moment from 'moment/min/moment-with-locales' import type { TDashboardSettings, TSection, TSectionItem, TSectionDetails } from './types' -import { createSectionItemObject, isLineDisallowedByIgnoreTerms, isNoteFromAllowedTeamspace, makeDashboardParas } from './dashboardHelpers' +import { createSectionItemObject, isLineDisallowedByIgnoreTerms, isNoteFromAllowedTeamspace, makeDashboardParas, resolveAllowedTeamspaceIDs } from './dashboardHelpers' import { tagParasFromNote } from './demoData' import { isTagCacheEnabled } from './dashboardSettingsClean' import { @@ -131,7 +131,7 @@ export async function getTaggedSectionData( const allowedFolders = getFoldersMatching(includedFolders,false, excludedFolders) // Get allowed teamspaces - const allowedTeamspaceIDs = config.includedTeamspaces ?? ['private'] + const allowedTeamspaceIDs = resolveAllowedTeamspaceIDs(config) for (const n of notesWithTag) { // logTimer('getTaggedSectionData', thisStartTime, `- start of processing for note "${n.filename}"`) From fa8b2e3c84a808ef72b215d553f1d5380fc129ad Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Thu, 30 Jul 2026 00:15:18 -0700 Subject: [PATCH 5/6] Dashboard: catch untimed-today reminders in REM, and document the not-yet-due rule Sweeping five perspectives against the reminder buckets showed every one of them losing items, and separated two causes. Real gap, now fixed: an untimed reminder due today had exactly one home, the Today section. With that off it went nowhere, even with the Reminders section on. It now falls back to REM alongside overdue and yesterday, under the same "Show Current Reminders" gate so an explicitly hidden item is not resurrected. Not a gap: a timed reminder due later today is shown nowhere until its time arrives. That is a deliberate decision, but nothing in the code said so, and from the outside it is indistinguishable from the bug above -- it cost a good while to tell the two apart. There is now a DESIGN DECISION note at the filter in getTimeBlockSectionData recording the intent, naming the two places that would have to change to show them early, and warning about the double-display that would result if only one were changed. The REM fallback comment says why this bucket alone is skipped, and the "no visible section" warning explicitly does not fire for it. Measured before and after on the same data (buckets timedToday=1 untimedToday=1 yesterday=0 tomorrow=0 overdue=4 undated=3, sum 9): perspective before after z_ALL_ON 8 of 9 8 shown + 1 not-yet-due = 9 Reminders Only - ALL 7 of 9 8 shown + 1 not-yet-due = 9 z_OVERDUE Only 7 of 9 8 shown + 1 not-yet-due = 9 Every reminder is now either displayed or intentionally withheld, with no silent losses left in those paths. Dashboard tests 220/220. Diagnostic logging is deliberately left in for continued testing: one line per reminder (list, title, raw EventKit date vs derived date/time), one per bucket assignment, the REM section size, and the names of reminder lists disabled in NotePlan -- that last one is how we found two "missing" reminders were simply in lists NotePlan has switched off. Co-Authored-By: Claude Opus 5 (1M context) --- jgclark.Dashboard/src/dataGeneration.js | 7 ++- jgclark.Dashboard/src/dataGenerationDays.js | 12 ++++- .../src/dataGenerationReminders.js | 46 +++++++++++++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/jgclark.Dashboard/src/dataGeneration.js b/jgclark.Dashboard/src/dataGeneration.js index 7c4020443..b9d59f723 100644 --- a/jgclark.Dashboard/src/dataGeneration.js +++ b/jgclark.Dashboard/src/dataGeneration.js @@ -128,11 +128,16 @@ export async function getSomeSectionsData( remindersData.yesterdayItems.length > 0 && yesterdayForDaySection.length === 0 && !Boolean(config.showOverdueSection) && !remCanHost const overdueHomeless = remindersData.overdueItems.length > 0 && !Boolean(config.showOverdueSection) && !remCanHost const tomorrowHomeless = remindersData.tomorrowItems.length > 0 && !config.showTomorrowSection - if (yesterdayHomeless || overdueHomeless || tomorrowHomeless) { + // Untimed today falls back to REM, so it is only lost when REM cannot host either. + // Timed today reminders that are not yet due are intentionally shown nowhere + // (see the DESIGN DECISION note in dataGenerationDays.js), so they are not warned about. + const untimedTodayHomeless = remindersData.untimedTodayItems.length > 0 && !config.showTodaySection && !remCanHost + if (yesterdayHomeless || overdueHomeless || tomorrowHomeless || untimedTodayHomeless) { const parts = [] if (yesterdayHomeless) parts.push(`${String(remindersData.yesterdayItems.length)} yesterday`) if (overdueHomeless) parts.push(`${String(remindersData.overdueItems.length)} overdue`) if (tomorrowHomeless) parts.push(`${String(remindersData.tomorrowItems.length)} tomorrow (no fallback: Tomorrow section off)`) + if (untimedTodayHomeless) parts.push(`${String(remindersData.untimedTodayItems.length)} untimed today`) logWarn('getSomeSectionsData', `- ${parts.join('; ')} reminder(s) have no visible section and will not be shown anywhere`) } diff --git a/jgclark.Dashboard/src/dataGenerationDays.js b/jgclark.Dashboard/src/dataGenerationDays.js index ae2fd687f..48af8f12d 100644 --- a/jgclark.Dashboard/src/dataGenerationDays.js +++ b/jgclark.Dashboard/src/dataGenerationDays.js @@ -321,11 +321,21 @@ export function getTimeBlockSectionData( let dueNowReminderCount = 0 // Append today's timed reminders whose due time has been reached (when Reminders section is enabled) + // + // DESIGN DECISION (@jgclark): a timed reminder due later today is shown NOWHERE + // until its time arrives. It is excluded here, and the Today section only ever + // receives *untimed* today reminders, so there is no other section that could + // pick it up -- this is deliberate, not an oversight, and it is why the REM + // fallback in dataGenerationReminders.js skips this bucket while catching the + // others. If you want "due later today" visible ahead of time, the change is to + // route the skipped items below into the Today section (or REM when Today is off) + // rather than dropping them, and to drop them from here once their time passes so + // they do not appear twice. if (remindersSectionEnabled && timedTodayReminderItems.length > 0) { const dueNowReminders = filterRemindersWhoseTimeHasBeenReached(timedTodayReminderItems) const skippedFutureCount = timedTodayReminderItems.length - dueNowReminders.length if (skippedFutureCount > 0) { - logDebug('getTimeBlockSectionData', `- skipped ${String(skippedFutureCount)} timed reminder(s) whose time has not been reached yet`) + logDebug('getTimeBlockSectionData', `- skipped ${String(skippedFutureCount)} timed reminder(s) whose time has not been reached yet (by design: not shown anywhere until due)`) } if (dueNowReminders.length > 0) { dueNowReminderCount = dueNowReminders.length diff --git a/jgclark.Dashboard/src/dataGenerationReminders.js b/jgclark.Dashboard/src/dataGenerationReminders.js index 422a1e660..a53119a04 100644 --- a/jgclark.Dashboard/src/dataGenerationReminders.js +++ b/jgclark.Dashboard/src/dataGenerationReminders.js @@ -122,6 +122,18 @@ export function getEnabledReminderLists(): TReminderListsResult { const enabledLists = Calendar.availableReminderLists({ enabledOnly: true }) const result = titlesAndColorsFromReminderListObjects(enabledLists) logDebug('getEnabledReminderLists', `- ${String(result.titles.length)} enabled reminder list(s): ${result.titles.join(', ') || '(none)'}`) + // Name the lists that exist but are switched off in NotePlan. A reminder living + // in one of them is simply absent from the Dashboard, with nothing anywhere to + // say why -- which reads as "the Dashboard lost my reminder". + try { + const allTitles = titlesAndColorsFromReminderListObjects(Calendar.availableReminderLists()).titles + const ignored = allTitles.filter((t) => !result.titles.includes(t)) + if (ignored.length > 0) { + logDebug('getEnabledReminderLists', `- ignoring ${String(ignored.length)} list(s) disabled in NotePlan: ${ignored.join(', ')}`) + } + } catch (err) { + logDebug('getEnabledReminderLists', `- could not enumerate all lists to report ignored ones: ${err.message}`) + } return result } // Older NotePlan: cannot read enabled/disabled or colors; use all accessible list titles @@ -287,6 +299,10 @@ export function mapCalendarItemToReminderForDashboard( clof(calendarItem, "CalendarItem (for Reminder): ", ['title', 'date', 'occurences', 'isAllDay', 'isCompleted', 'priority']) clo(reminder, " => Reminder: ") } + // One compact line per reminder: the raw EventKit fields next to what we derived. + // Strictly better than the older `if (title.match(/test/i))` dump, which only + // covered reminders that happened to have "test" in the name. + logDebug('reminderFromCalendarItem', `- list="${String(listname ?? "?")}" "${String(calendarItem.title).slice(0, 34)}" rawDate=${String(calendarItem.date)} isAllDay=${String(calendarItem.isAllDay)} occurences=${String(calendarItem.occurences ? calendarItem.occurences.length : 'none')} -> date=${String(reminder.date ?? 'UNDATED')} time=${String(reminder.time ?? '-')}`) return reminder } @@ -468,6 +484,14 @@ export function bucketReminderItems(allItems: Array): { if (skippedFutureCount > 0) { logDebug('bucketReminderItems', `- skipped ${String(skippedFutureCount)} reminder(s) dated after ${tomorrowISO}`) } + // Which bucket each reminder landed in, so a mis-bucketed one is visible. + const bucketOf = (arr, name) => arr.forEach((it) => logDebug('bucketReminderItems', `- bucket=${name} "${String(it.reminder?.title ?? '?').slice(0, 34)}" date=${String(it.reminder?.date ?? 'UNDATED')} time=${String(it.reminder?.time ?? '-')}`)) + bucketOf(timedTodayItems, 'timedToday') + bucketOf(untimedTodayItems, 'untimedToday') + bucketOf(yesterdayItems, 'yesterday') + bucketOf(tomorrowItems, 'tomorrow') + bucketOf(overdueItems, 'overdue') + bucketOf(undatedItems, 'undated') return { timedTodayItems: sortReminderSectionItems(timedTodayItems), @@ -552,11 +576,16 @@ export async function getRemindersGeneratedData( // Fallback chain, so a dated reminder is never silently discarded just because // the section that would have hosted it is switched off. This section's setting // is "Show Undated/Overdue Reminders", so an overdue reminder belongs here by - // name, and a yesterday one is overdue in every sense that matters once there is - // no Yesterday section to put it in. Order: own section -> Overdue -> here. - // Tomorrow is deliberately excluded: a future reminder is neither undated nor - // overdue, and hiding the Tomorrow section is a reasonable way to say you don't - // want to see it yet. + // name; a yesterday one is overdue in every sense that matters once there is no + // Yesterday section to put it in; and an untimed reminder due today would + // otherwise vanish entirely when the Today section is off. Order in each case is + // own section -> Overdue (where applicable) -> here. + // + // Two buckets deliberately have no fallback: + // - tomorrow: a future reminder is neither undated nor overdue, and hiding the + // Tomorrow section is a reasonable way to say you don't want to see it yet. + // - today's TIMED reminders whose time has not been reached: see the note in + // getTimeBlockSectionData -- those are meant to stay hidden until they are due. if (undatedOverdueRemindersEnabled) { const overdueSectionVisible = Boolean(config.showOverdueSection) const fallbackItems: Array = [] @@ -569,6 +598,12 @@ export async function getRemindersGeneratedData( if (currentRemindersEnabled && !config.showYesterdaySection && !overdueSectionVisible && buckets.yesterdayItems.length > 0) { fallbackItems.push(...buckets.yesterdayItems) } + // Untimed reminders due today have only ever had one home, the Today section, + // so with that off they had nowhere to go at all. Same "Show Current Reminders" + // gate as yesterday, for the same reason. + if (currentRemindersEnabled && !config.showTodaySection && buckets.untimedTodayItems.length > 0) { + fallbackItems.push(...buckets.untimedTodayItems) + } if (fallbackItems.length > 0) { logDebug('getRemindersGeneratedData', `- REM fallback: adopting ${String(fallbackItems.length)} reminder(s) whose own section is off (overdueVisible=${String(overdueSectionVisible)} yesterdayVisible=${String(Boolean(config.showYesterdaySection))})`) undatedItems = sortReminderSectionItems(undatedItems.concat(fallbackItems)) @@ -577,6 +612,7 @@ export async function getRemindersGeneratedData( const maxInSection = config.maxItemsToShowInSection ?? 24 const totalUndatedCount = undatedItems.length + logDebug('getRemindersGeneratedData', `- REM section will hold ${String(undatedItems.length)} reminder(s)`) if (totalUndatedCount > maxInSection) { undatedItems = undatedItems.slice(0, maxInSection) } From 459a60ab69f52141c55f8d2a34b61f014af3dbda Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Thu, 30 Jul 2026 00:21:34 -0700 Subject: [PATCH 6/6] Dashboard v2.4.0.b58: version bump + changelog for reminder routing and teamspace fixes Covers the five fixes already on main (84bddf54, 8eec7ba0, c4e88d80, 703dd712, 16b08e91), all found while tracing "why am I only seeing 3 reminders" against a live install. Co-Authored-By: Claude Opus 5 (1M context) --- jgclark.Dashboard/CHANGELOG.md | 8 ++++++++ jgclark.Dashboard/plugin.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/jgclark.Dashboard/CHANGELOG.md b/jgclark.Dashboard/CHANGELOG.md index 924560b7b..38a92dc0b 100644 --- a/jgclark.Dashboard/CHANGELOG.md +++ b/jgclark.Dashboard/CHANGELOG.md @@ -8,6 +8,14 @@ For more details see the [plugin's documentation](https://github.com/NotePlan/pl - TODO: fix long-standing layout bug where some tooltips were getting clipped - TODO: fix isNoteFromAllowedFolder() for teamspace or possibly 2025-W21.md --> +## [2.4.0.b58] 2026-07-30 +- Fix: reminders whose own section was switched off were discarded silently. Overdue, yesterday and **untimed today** reminders now fall back to the **Reminders** section (own section → Overdue → Reminders), so nothing disappears just because a section is hidden. Both fallbacks stay subject to **Show Current Reminders** / **Show Undated/Overdue Reminders**, so an item you deliberately hid is not resurrected. +- Fix: the **Overdue** section counted reminders it did not display. When `maxItemsToShowInSection` left fewer slots than there were reminders, the surplus was sliced off the list but still added to the total, so the header claimed more items than the section held. Now counts only what was added, and warns when some do not fit. +- Fix: **Spaces to Include** holding only unreachable teamspace IDs hid every task. Private notes are read only when `private` is in that list, so a list of stale IDs (e.g. after signing out of Spaces) filtered out every note before any task could be read - reminders still appeared, which made it look like tasks had vanished. Unreachable IDs are now discarded, falling back to private notes with a warning. Deliberate configurations are untouched. +- Doc: a timed reminder due later today is intentionally shown nowhere until its time arrives. This was undocumented and indistinguishable from the bug above; there is now a DESIGN DECISION note at the filter in `dataGenerationDays.js` naming what to change to show them early, and the pitfall of them then appearing twice. +- New: log when reminders or notes are dropped - reminder buckets with no visible host section, reminder lists disabled in NotePlan (a reminder there is invisible with no other clue), and overdue reminders that did not fit the section limit. +- dev: temporary diagnostic logging retained for ongoing testing - one line per reminder (list, title, raw EventKit date vs derived date/time), one per bucket assignment, and the REM section size. + ## [2.4.0.b57] 2026-07-29 - New: sticky **Filter settings** search at the top of Dashboard Settings - matches label/description (from 3+ characters). - Reduce opacity of 'chips' showing Reminder list name, and note names. diff --git a/jgclark.Dashboard/plugin.json b/jgclark.Dashboard/plugin.json index 6abdc50e2..d31cfd69f 100644 --- a/jgclark.Dashboard/plugin.json +++ b/jgclark.Dashboard/plugin.json @@ -8,7 +8,7 @@ "plugin.description": "A Dashboard for NotePlan, that in one place shows:\n- a compact list of open tasks and checklists from today's note\n- scheduled open tasks and checklists from other notes.\n- similarly for yesterday's note, tomorrow's note, and the weekly, monthly and quarterly notes too (if used)\n- all overdue tasks\n- all open tasks and checklists that contain particular @tags or #mentions of your choosing\n- the next notes ready to review (if you use the 'Projects and Reviews' plugin).\nIt includes many other ways of speeding up managing your tasks: see the website for more details.", "plugin.author": "@jgclark", "plugin.comment": "TODO: On full release, change minAppVersion down to 3.7?", - "plugin.version": "2.4.0.b57", + "plugin.version": "2.4.0.b58", "plugin.releaseStatus": "beta", "plugin.hidden": false, "plugin.lastUpdateInfo": "2.4.0: new 'Active Projects' Section. New 'Spaces to Include' setting which controls which (Team)Spaces you wish to include, plus whether or not to include the Private 'Space' (all notes not in a Space)\n2.3.3: new 'Year' section available.\n2.3.2: fix display when there are no priority items shown.\n2.3.1: fix for possible loss of settings error when upgrading.\n2.3.0: Support for NotePlan (Team)Spaces. Can re-order display of Sections.New '/backupSettings' command. Added 'noteTags' feature. Speeded up Tag/Mention sections. Layout improvements. Lots of other small fixes and improvements.\n2.2.1: Add new sorting option for Tag and Overdue sections.\n2.2.0: Add 'Search' section. New keyboard shortcuts. Plus many small improvements, bug fixes and performance improvements. See documentation for details.\n2.1.10: More move-under-heading options. Bug fixes and performance improvements.\n2.1.9: performance improvements and better UI for iPhone users.\n2.1.8: various fixes and small improvements.\n2.1.7: various fixes and small improvements.\n2.1.6: allow all current timeblocks to be shown, not just the first. Add new @repeat()s if using the extended syntax from the Repeat Extensions plugin. Bug fixes.\n2.1.5: fixes to time blocks and scheduling items.\n2.1.4: fix to Interactive Processing, and Edit All Perspectives dialog now shows unsaved changes.",