fix(idaa): resolve ~1-year 'no meetings found' bug on recovery meetings page
Root cause: stale IDB records from prior deploys persisted indefinitely. Fast path returned 0 (account_id mismatch), API errored silently, and the error state showed the same message as a genuinely empty result — making the failure indistinguishable from real data. Fix is layered defense: - Bump IDB_CONTENT_VERSIONS.events.event to 2 (one-time force-clear for all users) - Add check_and_clear_idb_table() helper to store_versions.ts; wire it in (idaa)/+layout.svelte to catch future version mismatches on session start - One silent auto-retry (3s) on API failure before surfacing error UI - Distinct error state (Unable to load meetings) separate from empty state - Escape-hatch cache-reset button after 8s when zero results + no active filters - Document root cause and fix in README.md and BOOTSTRAP__AI_Agent_Quickstart.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,78 @@ let event_id_li: Array<string> = $state([]);
|
||||
let search_debounce_timer: any = null;
|
||||
let last_search_id = 0;
|
||||
let last_executed_key = '';
|
||||
// Auto-retry state: one silent background retry is attempted before showing the error
|
||||
// state to the user. Manual "Try Again" button resets this so users can always retry.
|
||||
// WHY: Mobile connections drop briefly. A single retry resolves most transient failures
|
||||
// without the user ever seeing an error. If both attempts fail, surface the error clearly
|
||||
// with a visible retry button — NOT the same "No meetings found" message a real empty
|
||||
// result would show. (Conflating the two was a key reason the bug went unsolved so long.)
|
||||
let auto_retry_count = 0;
|
||||
|
||||
// ── Escape-hatch: cache-reset button after suspicious zero-result delay ──────────────
|
||||
//
|
||||
// WHY: The auto-retry + error state covers API failures (caught exceptions). But there
|
||||
// is a separate failure mode where the API succeeds and returns 0 results — due to
|
||||
// stale IDB data, a missed IDB_CONTENT_VERSIONS bump, or an undetected filtering issue.
|
||||
// In that path qry__status lands on 'done', not 'error', so no retry or error UI fires.
|
||||
// With ~140 active meetings in the database, zero results with no active filters is
|
||||
// ALWAYS a symptom of something wrong. After 8 seconds in that state we surface a
|
||||
// visible escape hatch. Using no-filters as the trigger prevents false positives when
|
||||
// a member genuinely filters to a category that has no results (e.g. "Type = Workshop").
|
||||
//
|
||||
// The reset clears db_events.event directly (bypasses the version check, which would be
|
||||
// a no-op here since the version already matches after the initial layout-mount clear).
|
||||
// The SWR search then re-fetches from the API and repopulates the table.
|
||||
|
||||
// True only when: search is done, result list is empty, AND no filter dimensions are set.
|
||||
let no_results_no_filters = $derived(
|
||||
$idaa_sess.recovery_meetings.qry__status === 'done' &&
|
||||
event_id_li.length === 0 &&
|
||||
!$idaa_loc.recovery_meetings.qry__physical &&
|
||||
!$idaa_loc.recovery_meetings.qry__virtual &&
|
||||
!$idaa_loc.recovery_meetings.qry__type &&
|
||||
!($idaa_loc.recovery_meetings.qry__fulltext_str?.trim())
|
||||
);
|
||||
|
||||
let show_cache_reset_btn = $state(false);
|
||||
let cache_reset_timer: any = null;
|
||||
|
||||
// Starts/cancels the 8-second countdown based on no_results_no_filters.
|
||||
// $effect only re-runs when the derived VALUE changes (true↔false), not on every
|
||||
// store write — so the timer counts down cleanly without spurious resets.
|
||||
$effect(() => {
|
||||
if (cache_reset_timer) {
|
||||
clearTimeout(cache_reset_timer);
|
||||
cache_reset_timer = null;
|
||||
}
|
||||
|
||||
if (no_results_no_filters) {
|
||||
show_cache_reset_btn = false;
|
||||
cache_reset_timer = setTimeout(() => {
|
||||
show_cache_reset_btn = true;
|
||||
}, 8000);
|
||||
} else {
|
||||
show_cache_reset_btn = false;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (cache_reset_timer) clearTimeout(cache_reset_timer);
|
||||
};
|
||||
});
|
||||
|
||||
async function handle_cache_reset() {
|
||||
show_cache_reset_btn = false;
|
||||
if (cache_reset_timer) { clearTimeout(cache_reset_timer); cache_reset_timer = null; }
|
||||
auto_retry_count = 0;
|
||||
try {
|
||||
await db_events.event.clear();
|
||||
console.log('[recovery_meetings] Escape-hatch reset: db_events.event cleared');
|
||||
} catch (e) {
|
||||
console.warn('[recovery_meetings] Escape-hatch reset: failed to clear IDB', e);
|
||||
}
|
||||
$idaa_sess.recovery_meetings.search_version++;
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Standardized Reactive Search Pattern (Aether UI V3)
|
||||
// This effect manages the orchestration between UI state and data fetching.
|
||||
@@ -294,12 +366,26 @@ async function handle_search_refresh(qry_key: string) {
|
||||
} catch (error) {
|
||||
if (current_search_id === last_search_id) {
|
||||
console.error('Revalidation failed:', error);
|
||||
$idaa_sess.recovery_meetings.qry__status = 'error';
|
||||
|
||||
// If the API failed (e.g. 400/500), we should NOT preserve results from a
|
||||
// previous successful search as it's misleading.
|
||||
// We clear the list so the 'No recovery meetings found' / Error message shows.
|
||||
event_id_li = [];
|
||||
if (auto_retry_count < 1) {
|
||||
// First failure — schedule one silent retry before surfacing the error.
|
||||
// Incrementing search_version triggers the $effect, which rebuilds qry_key
|
||||
// with the new version and calls handle_search_refresh again.
|
||||
auto_retry_count++;
|
||||
console.log(`[Search] API failed — auto-retry ${auto_retry_count}/1 in 3s...`);
|
||||
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
||||
setTimeout(() => {
|
||||
$idaa_sess.recovery_meetings.search_version++;
|
||||
}, 3000);
|
||||
} else {
|
||||
// Both attempts failed — surface the error distinctly.
|
||||
// IMPORTANT: do NOT show "No meetings found" here. A network/API failure
|
||||
// is not the same as a genuinely empty result. Conflating the two was why
|
||||
// the "no meetings found" bug went undiagnosed for ~1 year — staff and users
|
||||
// saw what looked like an empty list and assumed no data, not a failure.
|
||||
$idaa_sess.recovery_meetings.qry__status = 'error';
|
||||
event_id_li = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,11 +425,41 @@ if (browser) {
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
Searching...
|
||||
</div>
|
||||
{:else if $idaa_sess.recovery_meetings.qry__status === 'error'}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p>Unable to load meetings. Please try again.</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-primary m-auto"
|
||||
onclick={() => {
|
||||
auto_retry_count = 0;
|
||||
$idaa_sess.recovery_meetings.search_version++;
|
||||
}}>
|
||||
<span class="fas fa-redo m-1"></span>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
||||
No recovery meetings found matching your criteria.
|
||||
</div>
|
||||
{#if show_cache_reset_btn}
|
||||
<!-- Escape hatch: surfaces after 8s when zero results + no active filters.
|
||||
With ~140 active meetings, zero unfiltered results always indicates
|
||||
stale IDB data. Clears the event cache and triggers a fresh API fetch. -->
|
||||
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p class="text-sm opacity-75">Still not seeing meetings? Your local cache may be out of date.</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-warning m-auto"
|
||||
onclick={handle_cache_reset}>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Refresh Meeting Cache
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user