build_tmp_sort() encodes priority=true as '0' for ascending sort. JS comparators were using b.localeCompare(a) (descending), inverting the encoding so priority=false items sorted first. Fixed to a.localeCompare(b) in ae_journals_search_helpers.ts (3 sites in recovery_meetings +page.svelte and wrapper component). Also fixes a Dexie anti-pattern in bb/[post_id]: .reverse() before .sortBy() is a no-op in Dexie; moved array .reverse() to after the await. Documents the encoding rule and legacy inverted-encoding modules in GUIDE__SvelteKit2_Svelte5_DexieJS.md and adds mistake #15 to BOOTSTRAP quickstart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
564 lines
25 KiB
Svelte
564 lines
25 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from 'svelte';
|
|
interface Props {
|
|
/** @type {import('./$types').PageData} */
|
|
data: any;
|
|
}
|
|
let { data }: Props = $props();
|
|
|
|
let log_lvl: number = $state(0);
|
|
|
|
// *** Import Svelte specific
|
|
// import { page } from '$app/state';
|
|
import { browser } from '$app/environment';
|
|
|
|
// *** Import other supporting libraries
|
|
import { db_events } from '$lib/ae_events/db_events';
|
|
import {
|
|
idaa_loc,
|
|
idaa_sess,
|
|
idaa_slct,
|
|
idaa_trig
|
|
} from '$lib/stores/ae_idaa_stores';
|
|
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
|
import { events_func } from '$lib/ae_events/ae_events_functions';
|
|
|
|
import Element_data_store from '$lib/elements/element_data_store.svelte';
|
|
import Comp__event_obj_qry from './ae_idaa_comp__event_obj_qry.svelte';
|
|
import Comp__event_obj_li_wrapper from './ae_idaa_comp__event_obj_li_wrapper.svelte';
|
|
|
|
if (browser) {
|
|
$idaa_slct.event_id = null;
|
|
window.parent.postMessage({ event_id: null }, '*');
|
|
|
|
// Use a session-scoped trigger so the persisted IDAA profile is not rewritten
|
|
// on every page mount. Recovery Meetings only needs this to kick the initial search.
|
|
$idaa_sess.recovery_meetings.search_version++;
|
|
}
|
|
|
|
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;
|
|
// Tracks the category of the last search error for a more informative user message.
|
|
// ERR_NETWORK_CHANGED and other fetch failures surface as TypeError in JS.
|
|
let qry_error_detail: 'network' | 'server' | null = $state(null);
|
|
|
|
// ── 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())
|
|
);
|
|
|
|
// True when any filter dimension is active — drives the guided empty state.
|
|
let has_active_filters = $derived(
|
|
!!$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 7-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;
|
|
}, 5000);
|
|
} 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++;
|
|
}
|
|
function clear_filters() {
|
|
$idaa_loc.recovery_meetings.qry__physical = null;
|
|
$idaa_loc.recovery_meetings.qry__virtual = null;
|
|
$idaa_loc.recovery_meetings.qry__type = null;
|
|
$idaa_loc.recovery_meetings.qry__fulltext_str = null;
|
|
$idaa_sess.recovery_meetings.search_version++;
|
|
}
|
|
// ─────────────────────────────────────────────────────────────────────────────────────
|
|
|
|
// Standardized Reactive Search Pattern (Aether UI V3)
|
|
// This effect manages the orchestration between UI state and data fetching.
|
|
$effect(() => {
|
|
// 1. Reactive Dependencies
|
|
const account_id = $ae_loc.account_id;
|
|
if (!account_id) return; // Wait for account context
|
|
|
|
// Auth gate: do not fetch IDAA events for unauthenticated users.
|
|
// WHY $effect and not +layout.ts: layout load functions fire on SvelteKit link prefetch,
|
|
// causing private data to be written to IDB before Novi auth runs.
|
|
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
|
|
|
|
// Track filters and the search version (trigger)
|
|
const qry_params = {
|
|
v: $idaa_sess.recovery_meetings.search_version,
|
|
str: $idaa_loc.recovery_meetings.qry__fulltext_str,
|
|
phys: $idaa_loc.recovery_meetings.qry__physical,
|
|
virt: $idaa_loc.recovery_meetings.qry__virtual,
|
|
type: $idaa_loc.recovery_meetings.qry__type,
|
|
limit: $idaa_loc.recovery_meetings.qry__limit,
|
|
order: $idaa_loc.recovery_meetings.qry__order_by,
|
|
remote: $idaa_loc.recovery_meetings.qry__remote_first
|
|
};
|
|
const qry_key = JSON.stringify(qry_params);
|
|
|
|
// 2. Debounce Logic
|
|
if (search_debounce_timer) clearTimeout(search_debounce_timer);
|
|
search_debounce_timer = setTimeout(() => {
|
|
// 3. Execution (Untracked to prevent loops)
|
|
untrack(() => {
|
|
handle_search_refresh(qry_key);
|
|
});
|
|
}, 250);
|
|
|
|
return () => {
|
|
if (search_debounce_timer) clearTimeout(search_debounce_timer);
|
|
};
|
|
});
|
|
|
|
/**
|
|
* SWR (Stale-While-Revalidate) Search Orchestrator
|
|
*
|
|
* GOAL: Render matching meetings in < 50ms, then update with perfect server data.
|
|
*/
|
|
async function handle_search_refresh(qry_key: string) {
|
|
if (qry_key === last_executed_key) return;
|
|
last_executed_key = qry_key;
|
|
|
|
const current_search_id = ++last_search_id;
|
|
const account_id = $ae_loc.account_id;
|
|
const remote_first = $idaa_loc.recovery_meetings.qry__remote_first;
|
|
|
|
if (!account_id) return;
|
|
|
|
if (log_lvl)
|
|
console.log(
|
|
`[Search #${current_search_id}] Refreshing recovery meetings (remote_first=${remote_first}) for account: ${account_id}...`
|
|
);
|
|
|
|
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
|
qry_error_detail = null;
|
|
|
|
// If 'Remote First' is toggled (Admin only), we clear results immediately to show fresh state.
|
|
if (remote_first) {
|
|
event_id_li = [];
|
|
}
|
|
|
|
// Snapshot parameters to ensure the Fast Path and Revalidation are using the same criteria.
|
|
const qry_str = ($idaa_loc.recovery_meetings.qry__fulltext_str ?? '')
|
|
.toLowerCase()
|
|
.trim();
|
|
const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
|
|
const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
|
|
const qry_type = $idaa_loc.recovery_meetings.qry__type;
|
|
|
|
let local_ids: string[] = [];
|
|
|
|
// 1. FAST PATH: Local IDB Search
|
|
// We query Dexie first to show results from the 499-item cache pool instantly.
|
|
if (!remote_first) {
|
|
try {
|
|
let local_results = await db_events.event
|
|
.filter((ev) => {
|
|
// Resilient account check
|
|
const acct_match = ev.account_id === account_id;
|
|
if (!acct_match) return false;
|
|
|
|
if (qry_type && ev.type !== qry_type) return false;
|
|
if (qry_physical || qry_virtual) {
|
|
let match = false;
|
|
// Loose equality to handle 1, '1', true from DB
|
|
if (qry_physical && ev.physical == true) match = true;
|
|
if (qry_virtual && ev.virtual == true) match = true;
|
|
if (!match) return false;
|
|
}
|
|
if (qry_str) {
|
|
const name = (ev.name ?? '').toLowerCase();
|
|
const desc = (ev.description ?? '').toLowerCase();
|
|
const loc = (ev.location_text ?? '').toLowerCase();
|
|
const dqs = (ev.default_qry_str ?? '').toLowerCase();
|
|
return (
|
|
name.includes(qry_str) ||
|
|
desc.includes(qry_str) ||
|
|
loc.includes(qry_str) ||
|
|
dqs.includes(qry_str)
|
|
);
|
|
}
|
|
return true;
|
|
})
|
|
.toArray();
|
|
|
|
// Sort local results matching UI selection (Refactored 2026-02-16)
|
|
const sort_mode = $idaa_loc.recovery_meetings.qry__order_by;
|
|
if (sort_mode === 'name_asc' || sort_mode === 'name') {
|
|
local_results.sort((a, b) =>
|
|
(a.name ?? '').localeCompare(b.name ?? '')
|
|
);
|
|
} else if (sort_mode === 'name_desc') {
|
|
local_results.sort((a, b) =>
|
|
(b.name ?? '').localeCompare(a.name ?? '')
|
|
);
|
|
} else {
|
|
// Robust Chronological Sort using pre-computed tmp_sort_1
|
|
// Handles Priority, Manual Sort, and the updated_on/created_on fallback
|
|
// tmp_sort_1 built by build_tmp_sort(): priority=true→'0', so ASC puts priority first.
|
|
local_results.sort((a, b) =>
|
|
(a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? '')
|
|
);
|
|
}
|
|
|
|
local_ids = local_results.map((e) => String(e.id)).filter(Boolean);
|
|
|
|
// Update UI immediately. This eliminates the "white page" during searching.
|
|
if (current_search_id === last_search_id) {
|
|
if (log_lvl)
|
|
console.log(
|
|
`[Search #${current_search_id}] Fast Path complete. Found ${local_ids.length} items locally.`
|
|
);
|
|
event_id_li = local_ids;
|
|
if (local_ids.length > 0) {
|
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (log_lvl)
|
|
console.warn('Fast Path failed, waiting for API...', e);
|
|
}
|
|
}
|
|
|
|
// 2. REVALIDATE: Server API Search
|
|
// This hits the full database, bypassing the "cache pool" limitation.
|
|
try {
|
|
const results = await events_func.search__event({
|
|
api_cfg: $ae_api,
|
|
for_obj_type: 'account',
|
|
for_obj_id: account_id,
|
|
qry_conference: false,
|
|
qry_physical: qry_physical,
|
|
qry_virtual: qry_virtual,
|
|
qry_type: qry_type,
|
|
qry_str: qry_str || null,
|
|
enabled: $idaa_loc.recovery_meetings.qry__enabled,
|
|
hidden: $idaa_loc.recovery_meetings.qry__hidden,
|
|
limit: $idaa_loc.recovery_meetings.qry__limit,
|
|
order_by_li: $idaa_loc.recovery_meetings.qry__order_by_li,
|
|
log_lvl: 0
|
|
});
|
|
|
|
if (current_search_id === last_search_id) {
|
|
let api_results = results || [];
|
|
|
|
// SECONDARY FILTER: Re-apply structured filters the API may handle loosely.
|
|
// WHY type/physical/virtual: the backend uses AND logic for body filters, so
|
|
// physical+virtual together would incorrectly exclude either-only meetings —
|
|
// we pass only one at a time and handle OR logic here.
|
|
// WHY NOT qry_str: the API already applied exact LIKE search on default_qry_str
|
|
// (a backend-generated combined index that includes contact name/email). Re-running
|
|
// text filtering client-side against the response fields silently drops meetings
|
|
// that matched only via default_qry_str (e.g., by contact name) because that
|
|
// field may not be present in the response body or may not duplicate the match.
|
|
api_results = api_results.filter((ev) => {
|
|
if (qry_type && ev.type !== qry_type) return false;
|
|
if (qry_physical || qry_virtual) {
|
|
let match = false;
|
|
if (qry_physical && ev.physical == true) match = true;
|
|
if (qry_virtual && ev.virtual == true) match = true;
|
|
if (!match) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// RE-SORT: Match the fast-path IDB sort so stale/NULL API ordering is corrected.
|
|
// WHY: The RE-SORT previously only checked 'name' (a legacy mode that no longer
|
|
// exists in sort_modes). 'name_asc' and 'name_desc' fell through to the else
|
|
// branch and were silently re-sorted chronologically, ignoring the user's selection.
|
|
const sort_order = $idaa_loc.recovery_meetings.qry__order_by;
|
|
if (sort_order === 'name_asc' || sort_order === 'name') {
|
|
api_results.sort((a, b) =>
|
|
(a.name ?? '').localeCompare(b.name ?? '')
|
|
);
|
|
} else if (sort_order === 'name_desc') {
|
|
api_results.sort((a, b) =>
|
|
(b.name ?? '').localeCompare(a.name ?? '')
|
|
);
|
|
} else {
|
|
// tmp_sort_1 built by build_tmp_sort(): priority=true→'0', so ASC puts priority first.
|
|
api_results.sort((a, b) =>
|
|
(a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? '')
|
|
);
|
|
}
|
|
|
|
const api_ids = api_results
|
|
.map((e: any) => String(e.id))
|
|
.filter(Boolean);
|
|
|
|
// UI PROTECTION: Preserve results if API returns nothing (0 results)
|
|
// BUT only if it was a successful empty response, not a failure.
|
|
// ALSO: Only protect if qry_str was the primary change.
|
|
// If Type or Location changed, we want to see the 0.
|
|
const filter_changed =
|
|
qry_type !==
|
|
$idaa_sess.recovery_meetings.status_qry__last_type ||
|
|
qry_physical !==
|
|
$idaa_sess.recovery_meetings.status_qry__last_phys ||
|
|
qry_virtual !==
|
|
$idaa_sess.recovery_meetings.status_qry__last_virt;
|
|
|
|
if (
|
|
api_ids.length === 0 &&
|
|
local_ids.length > 0 &&
|
|
!remote_first &&
|
|
!filter_changed
|
|
) {
|
|
if (log_lvl)
|
|
console.warn(
|
|
`[Search #${current_search_id}] API returned 0 matching results. Preserving local cache view.`
|
|
);
|
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
|
return;
|
|
}
|
|
|
|
$idaa_slct.event_obj_li = api_results;
|
|
event_id_li = api_ids;
|
|
|
|
// Snapshot last used filters for future protection checks
|
|
$idaa_sess.recovery_meetings.status_qry__last_request_str = qry_str;
|
|
$idaa_sess.recovery_meetings.status_qry__last_type = qry_type;
|
|
$idaa_sess.recovery_meetings.status_qry__last_phys = qry_physical;
|
|
$idaa_sess.recovery_meetings.status_qry__last_virt = qry_virtual;
|
|
|
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
|
if (log_lvl)
|
|
console.log(
|
|
`[Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
if (current_search_id === last_search_id) {
|
|
// TypeError = network-level failure (ERR_NETWORK_CHANGED, offline, DNS failure).
|
|
// These are transient by nature; show a different message than a server error.
|
|
const is_network_err = error instanceof TypeError;
|
|
console.error('Revalidation failed:', is_network_err ? `Network error: ${error}` : error);
|
|
|
|
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.
|
|
qry_error_detail = is_network_err ? 'network' : 'server';
|
|
$idaa_sess.recovery_meetings.qry__status = 'error';
|
|
event_id_li = [];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (browser) {
|
|
console.log('Browser environment ready.');
|
|
window.parent.postMessage({ event_id: $idaa_slct?.event_id ?? null }, '*');
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>
|
|
IDAA Recovery Meetings - Novi - {$ae_loc?.title}
|
|
</title>
|
|
</svelte:head>
|
|
|
|
<Comp__event_obj_qry />
|
|
|
|
<div class="w-full max-w-xl mx-auto space-y-1">
|
|
<button
|
|
type="button"
|
|
onclick={() => {
|
|
$idaa_loc.recovery_meetings.ds_info_collapsed =
|
|
!($idaa_loc.recovery_meetings.ds_info_collapsed ?? false);
|
|
}}
|
|
class="novi_btn btn btn-sm w-full flex items-center justify-between
|
|
rounded-lg preset-outlined-surface-200-800 hover:preset-tonal-surface
|
|
opacity-60 hover:opacity-100 transition-all px-3 py-1"
|
|
title={$idaa_loc.recovery_meetings.ds_info_collapsed
|
|
? 'Show meeting info'
|
|
: 'Collapse meeting info'}>
|
|
<span class="text-sm">
|
|
<span class="fas fa-info-circle mr-1 text-xs"></span>Meeting Info
|
|
</span>
|
|
<span class="fas text-xs opacity-60
|
|
{$idaa_loc.recovery_meetings.ds_info_collapsed ? 'fa-chevron-down' : 'fa-chevron-up'}">
|
|
</span>
|
|
</button>
|
|
{#if !($idaa_loc.recovery_meetings.ds_info_collapsed ?? false)}
|
|
<Element_data_store
|
|
ds_code="recovery_meetings_info"
|
|
ds_type="html"
|
|
class_li="rounded-lg preset-outlined-surface-200-800 m-auto p-2 space-y-2 w-full"
|
|
show_edit_btn={true} />
|
|
{/if}
|
|
</div>
|
|
|
|
{#if Array.isArray(event_id_li) && event_id_li.length}
|
|
<Comp__event_obj_li_wrapper
|
|
{event_id_li}
|
|
link_to_type={'account'}
|
|
link_to_id={$ae_loc.account_id}
|
|
limit={$idaa_loc.recovery_meetings.qry__limit}
|
|
{log_lvl} />
|
|
{:else}
|
|
<div class="space-y-2">
|
|
{#if $idaa_sess.recovery_meetings.qry__status === 'loading'}
|
|
<div
|
|
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
|
<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>
|
|
{#if qry_error_detail === 'network'}
|
|
Network connection interrupted — please check your connection and try again.
|
|
{:else}
|
|
Unable to load meetings — server error. Please try again.
|
|
{/if}
|
|
</p>
|
|
<p class="text-xs opacity-60">
|
|
If "Try Again" keeps failing, use "Clear Cache & Reload" to reset your local data.
|
|
</p>
|
|
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm preset-tonal-primary"
|
|
onclick={() => {
|
|
auto_retry_count = 0;
|
|
$idaa_sess.recovery_meetings.search_version++;
|
|
}}>
|
|
<span class="fas fa-redo m-1"></span>
|
|
Try Again
|
|
</button>
|
|
<!-- Escape hatch for persistent server errors caused by stale auth state in
|
|
localStorage (stale account_id, api_secret_key, or site config). "Try Again"
|
|
reuses the same bad state and loops indefinitely — this clears it.
|
|
Mirrors the "Clear Cache & Reload" button in the IDAA layout auth error state. -->
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm preset-tonal-surface preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all"
|
|
onclick={async () => {
|
|
localStorage.removeItem('ae_loc');
|
|
localStorage.removeItem('ae_idaa_loc');
|
|
try { await db_events.event.clear(); } catch { /* ignore */ }
|
|
try {
|
|
const saved_url = sessionStorage.getItem('idaa_iframe_reload_url');
|
|
if (saved_url) { location.href = saved_url; return; }
|
|
} catch { /* ignore */ }
|
|
location.reload();
|
|
}}>
|
|
<span class="fas fa-sync-alt m-1"></span>
|
|
Clear Cache & Reload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{#if has_active_filters}
|
|
<!-- Guided empty state: filters are active but returned no results.
|
|
Distinct from the zero-unfiltered-results path (which indicates a data problem).
|
|
Here the member may simply have narrowed too far — offer a one-click escape. -->
|
|
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
|
<p>No meetings found for these filters.</p>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm preset-tonal-primary m-auto"
|
|
onclick={clear_filters}>
|
|
<span class="fas fa-times m-1"></span>
|
|
Clear all filters
|
|
</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 List Cache
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|