fix(idaa): stabilize Recovery Meetings search and broaden visibility logic
- Resolved 400 Bad Request by whitelisting 'physical', 'virtual', and 'external_person_id' fields in backend searchable_fields. - Broadened server-side search by removing restrictive AND filters for location types, moving the inclusive OR logic to the client-side filter layer. - Hardened handle_search_refresh error handling to clear results on category filter changes (Type, Physical, Virtual) while maintaining flicker protection strictly for text search typing. - Restored permissive visibility for Trusted users, allowing them to see hidden/disabled items regardless of Edit Mode. - Optimized plural object loading in Archives and Events modules using concurrent Promise.all. - Standardized ID handling across the search flow to prevent Dexie lookup failures. - Finalized well-commented code across IDAA modules to document search strategies and workarounds.
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -22,7 +22,7 @@ This is a list of tasks to be completed before the next event/show/conference.
|
|||||||
## Urgent Tasks (Feb 4, 2026)
|
## Urgent Tasks (Feb 4, 2026)
|
||||||
|
|
||||||
1. **IDAA Module Verification:**
|
1. **IDAA Module Verification:**
|
||||||
- [ ] **Archives:** Verify everything loads/plays; ensure Archives and Archive Content are editable; check linked file management.
|
- [x] **Archives:** Verify everything loads/plays; ensure Archives and Archive Content are editable; check linked file management. (Completed 2026-02-05)
|
||||||
- [ ] **Bulletin Board (BB):** Verify Post/Comment create/edit; test email notifications for staff and original posters; verify anonymous toggle; test file attachments on Posts; optimize inline image display (collapse/expand).
|
- [ ] **Bulletin Board (BB):** Verify Post/Comment create/edit; test email notifications for staff and original posters; verify anonymous toggle; test file attachments on Posts; optimize inline image display (collapse/expand).
|
||||||
- [ ] **Recovery Meetings:** Verify all search filters; audit full editing workflow; verify special code/copy buttons for Zoom/Jitsi.
|
- [ ] **Recovery Meetings:** Verify all search filters; audit full editing workflow; verify special code/copy buttons for Zoom/Jitsi.
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export async function load_ae_obj_li__event({
|
|||||||
hidden = 'not_hidden',
|
hidden = 'not_hidden',
|
||||||
view = 'default',
|
view = 'default',
|
||||||
inc_session_li = false,
|
inc_session_li = false,
|
||||||
limit = 99,
|
limit = 9,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
order_by_li = { start_datetime: 'DESC' } as const,
|
order_by_li = { start_datetime: 'DESC' } as const,
|
||||||
try_cache = true,
|
try_cache = true,
|
||||||
@@ -307,15 +307,16 @@ export async function load_ae_obj_li__event({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inc_session_li && ae_promises.load__event_obj_li) {
|
if (inc_session_li && ae_promises.load__event_obj_li) {
|
||||||
for (const event_obj of ae_promises.load__event_obj_li) {
|
const session_tasks = ae_promises.load__event_obj_li.map((event_obj: any) => {
|
||||||
const current_event_id = event_obj.id || event_obj.event_id;
|
const current_event_id = event_obj.id || event_obj.event_id;
|
||||||
event_obj.event_session_obj_li = await load_ae_obj_li__event_session({
|
return load_ae_obj_li__event_session({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
for_obj_type: 'event',
|
for_obj_type: 'event',
|
||||||
for_obj_id: current_event_id,
|
for_obj_id: current_event_id,
|
||||||
log_lvl
|
log_lvl
|
||||||
});
|
}).then((res) => (event_obj.event_session_obj_li = res));
|
||||||
}
|
});
|
||||||
|
await Promise.all(session_tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ae_promises.load__event_obj_li;
|
return ae_promises.load__event_obj_li;
|
||||||
@@ -450,6 +451,16 @@ export async function update_ae_obj__event({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Updated 2026-01-21
|
// Updated 2026-01-21
|
||||||
|
/**
|
||||||
|
* Unified Search for Events (V3 API)
|
||||||
|
*
|
||||||
|
* STRATEGY: Hybrid Search/Filter
|
||||||
|
* 1. Server-side (V3 Search): Used for high-level filtering (Account, Type, String Search).
|
||||||
|
* This reduces the data payload significantly.
|
||||||
|
* 2. Client-side (Filter Layer): Used for complex logic that the backend Search API
|
||||||
|
* may not handle natively yet, such as inclusive 'OR' logic for Physical/Virtual
|
||||||
|
* meetings or specific person ID matching.
|
||||||
|
*/
|
||||||
export async function search__event({
|
export async function search__event({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
for_obj_type = 'account',
|
for_obj_type = 'account',
|
||||||
@@ -491,17 +502,35 @@ export async function search__event({
|
|||||||
|
|
||||||
let result_li: ae_Event[] | null = null;
|
let result_li: ae_Event[] | null = null;
|
||||||
|
|
||||||
if (qry_str && qry_str.trim().length > 0) {
|
// Use V3 Search if ANY filter is active to ensure we query the full database
|
||||||
// Option A: Active Text Search
|
const has_active_filters = (qry_str && qry_str.trim().length > 0) ||
|
||||||
|
qry_physical === true ||
|
||||||
|
qry_virtual === true ||
|
||||||
|
(qry_type && qry_type !== 'all' && qry_type !== '') ||
|
||||||
|
qry_person_id ||
|
||||||
|
qry_conference !== null;
|
||||||
|
|
||||||
|
if (has_active_filters) {
|
||||||
|
// Option A: Active Search (Server-side filtering)
|
||||||
const search_query: any = {
|
const search_query: any = {
|
||||||
and: []
|
and: []
|
||||||
};
|
};
|
||||||
const params: key_val = {};
|
const params: key_val = {};
|
||||||
|
|
||||||
// Use default_qry_str for searching as requested
|
if (qry_str && qry_str.trim().length > 0) {
|
||||||
// Using 'like' with wildcards and populating params['lk_qry'] to match working event_session logic
|
// Use default_qry_str for searching as requested
|
||||||
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${qry_str.trim()}%` });
|
// Using 'like' with wildcards and populating params['lk_qry'] to match working event_session logic
|
||||||
params['lk_qry'] = { 'default_qry_str': qry_str.trim() };
|
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${qry_str.trim()}%` });
|
||||||
|
params['lk_qry'] = { 'default_qry_str': qry_str.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: We do NOT push 'physical' and 'virtual' to the server-side query here.
|
||||||
|
// The V3 Search API uses AND logic, which would exclude meetings that are
|
||||||
|
// only physical or only virtual. We let the Client-side Filter handle this below.
|
||||||
|
|
||||||
|
if (qry_type && qry_type !== 'all' && qry_type !== '') {
|
||||||
|
search_query.and.push({ field: 'type', op: 'eq', value: qry_type });
|
||||||
|
}
|
||||||
|
|
||||||
if (for_obj_id) {
|
if (for_obj_id) {
|
||||||
search_query.and.push({ field: `${for_obj_type}_id`, op: 'eq', value: for_obj_id });
|
search_query.and.push({ field: `${for_obj_type}_id`, op: 'eq', value: for_obj_id });
|
||||||
@@ -529,7 +558,7 @@ export async function search__event({
|
|||||||
log_lvl
|
log_lvl
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Option B: List All (No text search active)
|
// Option B: List All (No filters active)
|
||||||
// Fallback to standard list retrieval to ensure we get results when the search bar is empty.
|
// Fallback to standard list retrieval to ensure we get results when the search bar is empty.
|
||||||
result_li = await api.get_ae_obj_li_v3({
|
result_li = await api.get_ae_obj_li_v3({
|
||||||
api_cfg,
|
api_cfg,
|
||||||
@@ -572,7 +601,15 @@ export async function search__event({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-side Filter Layer (Retained for complex OR logic and Person ID checks)
|
/**
|
||||||
|
* Client-side Filter Layer
|
||||||
|
*
|
||||||
|
* WHY: The V3 Search API defaults to 'AND' logic for its top-level filters.
|
||||||
|
* Some UI requirements (like showing meetings that are EITHER physical OR virtual)
|
||||||
|
* are more reliably handled here after the broad server-side fetch.
|
||||||
|
* This also ensures that complex person-id matching across multiple legacy fields
|
||||||
|
* remains consistent without requiring massive backend search indices.
|
||||||
|
*/
|
||||||
const filtered_obj_li = processed_obj_li.filter((ev: any) => {
|
const filtered_obj_li = processed_obj_li.filter((ev: any) => {
|
||||||
// Handle conference filter
|
// Handle conference filter
|
||||||
if (qry_conference != null) {
|
if (qry_conference != null) {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<!-- <h1>Archives {$lq__archive_obj_li?.length}</h1> -->
|
<!-- <h1>Archives {$lq__archive_obj_li?.length}</h1> -->
|
||||||
|
|
||||||
<Help_tech
|
<Help_tech
|
||||||
e_class="m-auto"
|
e_class="mx-auto"
|
||||||
e_class_h1="novi_m0"
|
e_class_h1="novi_m0"
|
||||||
e_class_h2="novi_m0"
|
e_class_h2="novi_m0"
|
||||||
btn_class="novi_btn"
|
btn_class="novi_btn"
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
></Help_tech>
|
></Help_tech>
|
||||||
|
|
||||||
{#await lq__archive_obj_li}
|
{#await lq__archive_obj_li}
|
||||||
<div class="flex flex-col items-center justify-center p-8">
|
<div class="flex flex-col items-top justify-center p-8">
|
||||||
<span class="fas fa-spinner fa-spin text-4xl text-primary-500 mb-4"></span>
|
<span class="fas fa-spinner fa-spin text-4xl text-primary-500 mb-4"></span>
|
||||||
<span class="text-lg text-gray-600 dark:text-gray-400">Loading archives...</span>
|
<span class="text-lg text-gray-600 dark:text-gray-400">Loading archives...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
{#if $lq__archive_obj_li && $lq__archive_obj_li?.length}
|
{#if $lq__archive_obj_li && $lq__archive_obj_li?.length}
|
||||||
<Comp__archive_obj_li {lq__archive_obj_li} />
|
<Comp__archive_obj_li {lq__archive_obj_li} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center p-4 text-center">
|
<div class="flex flex-col items-top justify-center p-4 text-center">
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400 mb-4">No archives found.</p>
|
<p class="text-lg text-gray-600 dark:text-gray-400 mb-4">No archives found.</p>
|
||||||
<p class="text-md text-gray-500 dark:text-gray-300">
|
<p class="text-md text-gray-500 dark:text-gray-300">
|
||||||
Archives will appear here once created.
|
Archives will appear here once created.
|
||||||
|
|||||||
@@ -46,8 +46,12 @@
|
|||||||
let last_search_id = 0;
|
let last_search_id = 0;
|
||||||
|
|
||||||
// Standardized Reactive Search Pattern (Aether UI V3)
|
// Standardized Reactive Search Pattern (Aether UI V3)
|
||||||
|
// This effect manages the orchestration between UI state and data fetching.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// 1. Reactive Dependencies
|
// 1. Reactive Dependencies
|
||||||
|
const account_id = $ae_loc.account_id;
|
||||||
|
if (!account_id) return; // Wait for account context
|
||||||
|
|
||||||
// Track filters and the search version (trigger)
|
// Track filters and the search version (trigger)
|
||||||
const qry_params = {
|
const qry_params = {
|
||||||
v: $idaa_loc.recovery_meetings.search_version,
|
v: $idaa_loc.recovery_meetings.search_version,
|
||||||
@@ -57,8 +61,7 @@
|
|||||||
type: $idaa_loc.recovery_meetings.qry__type,
|
type: $idaa_loc.recovery_meetings.qry__type,
|
||||||
limit: $idaa_loc.recovery_meetings.qry__limit,
|
limit: $idaa_loc.recovery_meetings.qry__limit,
|
||||||
order: $idaa_loc.recovery_meetings.qry__order_by,
|
order: $idaa_loc.recovery_meetings.qry__order_by,
|
||||||
remote: $idaa_loc.recovery_meetings.qry__remote_first,
|
remote: $idaa_loc.recovery_meetings.qry__remote_first
|
||||||
account: $ae_loc.account_id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Debounce Logic
|
// 2. Debounce Logic
|
||||||
@@ -75,21 +78,28 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SWR (Stale-While-Revalidate) Search Orchestrator
|
||||||
|
*
|
||||||
|
* GOAL: Render matching meetings in < 50ms, then update with perfect server data.
|
||||||
|
*/
|
||||||
async function handle_search_refresh() {
|
async function handle_search_refresh() {
|
||||||
const current_search_id = ++last_search_id;
|
const current_search_id = ++last_search_id;
|
||||||
const account_id = $ae_loc.account_id;
|
const account_id = $ae_loc.account_id;
|
||||||
const remote_first = $idaa_loc.recovery_meetings.qry__remote_first;
|
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}...`);
|
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';
|
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
||||||
|
|
||||||
// If remote first, clear immediately to show fresh state
|
// If 'Remote First' is toggled (Admin only), we clear results immediately to show fresh state.
|
||||||
if (remote_first) {
|
if (remote_first) {
|
||||||
event_id_random_li = [];
|
event_id_random_li = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot current params to ensure Fast Path matches revalidation
|
// 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_str = ($idaa_loc.recovery_meetings.qry__fulltext_str ?? '').toLowerCase().trim();
|
||||||
const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
|
const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
|
||||||
const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
|
const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
|
||||||
@@ -97,55 +107,53 @@
|
|||||||
|
|
||||||
let local_ids: string[] = [];
|
let local_ids: string[] = [];
|
||||||
|
|
||||||
// 1. FAST PATH: Local IDB Search (SWR Pattern) - Skip if Remote First
|
// 1. FAST PATH: Local IDB Search
|
||||||
|
// We query Dexie first to show results from the 499-item cache pool instantly.
|
||||||
if (!remote_first) {
|
if (!remote_first) {
|
||||||
try {
|
try {
|
||||||
if (account_id) {
|
let local_results = await db_events.event
|
||||||
let local_results = await db_events.event
|
.filter(ev => {
|
||||||
.filter(ev => {
|
// Resilient account check
|
||||||
// Resilient account check: match either account_id or account_id_random
|
const acct_match = ev.account_id === account_id || ev.account_id_random === account_id;
|
||||||
const acct_match = ev.account_id === account_id || ev.account_id_random === account_id;
|
if (!acct_match) return false;
|
||||||
if (!acct_match) return false;
|
|
||||||
|
|
||||||
if (qry_type && ev.type !== qry_type) return false;
|
if (qry_type && ev.type !== qry_type) return false;
|
||||||
if (qry_physical || qry_virtual) {
|
if (qry_physical || qry_virtual) {
|
||||||
let match = false;
|
let match = false;
|
||||||
if (qry_physical && ev.physical) match = true;
|
// Loose equality to handle 1, '1', true from DB
|
||||||
if (qry_virtual && ev.virtual) match = true;
|
if (qry_physical && ev.physical == true) match = true;
|
||||||
if (!match) return false;
|
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();
|
|
||||||
return name.includes(qry_str) || desc.includes(qry_str) || loc.includes(qry_str);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
// Sort local results
|
|
||||||
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
|
||||||
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
|
||||||
} else {
|
|
||||||
local_results.sort((a, b) => {
|
|
||||||
const dateA = a.updated_on ? new Date(a.updated_on).getTime() : 0;
|
|
||||||
const dateB = b.updated_on ? new Date(b.updated_on).getTime() : 0;
|
|
||||||
return dateB - dateA;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
local_ids = local_results.map(e => e.id || e.event_id_random).filter(Boolean);
|
|
||||||
|
|
||||||
// Update UI immediately with local results
|
|
||||||
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_random_li = local_ids;
|
|
||||||
// If we found results locally, we can mark as done to stop spinning,
|
|
||||||
// revalidation will still finish in background
|
|
||||||
if (local_ids.length > 0) {
|
|
||||||
$idaa_sess.recovery_meetings.qry__status = 'done';
|
|
||||||
}
|
}
|
||||||
|
if (qry_str) {
|
||||||
|
const name = (ev.name ?? '').toLowerCase();
|
||||||
|
const desc = (ev.description ?? '').toLowerCase();
|
||||||
|
const loc = (ev.location_text ?? '').toLowerCase();
|
||||||
|
return name.includes(qry_str) || desc.includes(qry_str) || loc.includes(qry_str);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
// Sort local results matching UI selection
|
||||||
|
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
||||||
|
local_results.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
||||||
|
} else {
|
||||||
|
local_results.sort((a, b) => {
|
||||||
|
const dateA = a.updated_on ? new Date(a.updated_on).getTime() : 0;
|
||||||
|
const dateB = b.updated_on ? new Date(b.updated_on).getTime() : 0;
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ids = local_results.map(e => String(e.id || e.event_id_random)).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_random_li = local_ids;
|
||||||
|
if (local_ids.length > 0) {
|
||||||
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -153,7 +161,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. REVALIDATE: Slow API Request
|
// 2. REVALIDATE: Server API Search
|
||||||
|
// This hits the full database, bypassing the "cache pool" limitation.
|
||||||
try {
|
try {
|
||||||
const results = await events_func.search__event({
|
const results = await events_func.search__event({
|
||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
@@ -172,18 +181,51 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (current_search_id === last_search_id) {
|
if (current_search_id === last_search_id) {
|
||||||
const api_results = results || [];
|
let api_results = results || [];
|
||||||
const api_ids = api_results.map((e: any) => e.id || e.event_id_random).filter(Boolean);
|
|
||||||
|
|
||||||
// If API returns 0 but local search found broad results, protect the UI
|
// SECONDARY FILTER: Ensure API results respect exact UI filters (handles backend broadness)
|
||||||
if (api_ids.length === 0 && local_ids.length > 0 && !remote_first && !qry_str) {
|
api_results = api_results.filter((ev: any) => {
|
||||||
if (log_lvl) console.warn(`[Search #${current_search_id}] Revalidation returned 0. Preserving cache.`);
|
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;
|
||||||
|
}
|
||||||
|
if (qry_str && qry_str.length >= 3) {
|
||||||
|
const name = (ev.name ?? '').toLowerCase();
|
||||||
|
const desc = (ev.description ?? '').toLowerCase();
|
||||||
|
const loc = (ev.location_text ?? '').toLowerCase();
|
||||||
|
if (!name.includes(qry_str) && !desc.includes(qry_str) && !loc.includes(qry_str)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const api_ids = api_results.map((e: any) => String(e.id || e.event_id_random)).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';
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$idaa_slct.event_obj_li = api_results;
|
$idaa_slct.event_obj_li = api_results;
|
||||||
event_id_random_li = api_ids;
|
event_id_random_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';
|
$idaa_sess.recovery_meetings.qry__status = 'done';
|
||||||
if (log_lvl) console.log(`[Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`);
|
if (log_lvl) console.log(`[Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`);
|
||||||
}
|
}
|
||||||
@@ -191,9 +233,11 @@
|
|||||||
if (current_search_id === last_search_id) {
|
if (current_search_id === last_search_id) {
|
||||||
console.error('Revalidation failed:', error);
|
console.error('Revalidation failed:', error);
|
||||||
$idaa_sess.recovery_meetings.qry__status = 'error';
|
$idaa_sess.recovery_meetings.qry__status = 'error';
|
||||||
if (event_id_random_li.length === 0 && local_ids.length > 0) {
|
|
||||||
event_id_random_li = local_ids;
|
// 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_random_li = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,36 +37,44 @@
|
|||||||
import { idaa_loc, idaa_sess, idaa_slct, idaa_trig } from '$lib/stores/ae_idaa_stores';
|
import { idaa_loc, idaa_sess, idaa_slct, idaa_trig } from '$lib/stores/ae_idaa_stores';
|
||||||
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
||||||
|
|
||||||
// Derived list of visible items (Refactored 2026-01-27)
|
// Derived list of visible items (Refactored 2026-02-05)
|
||||||
// Ensures count matches exactly what is rendered to the user
|
//
|
||||||
let visible_event_obj_li = $derived((() => {
|
// WHY: The parent search logic fetches matching records into the local IndexedDB.
|
||||||
// Subscribe to the LiveQuery observable using $ prefix
|
// This derived store performs the final client-side visibility layer to ensure
|
||||||
const list = $lq__event_obj_li;
|
// that the results count matches exactly what is rendered to the user.
|
||||||
|
let visible_event_obj_li = $derived.by(() => {
|
||||||
|
// Subscribe to the LiveQuery observable using $ prefix
|
||||||
|
const list = $lq__event_obj_li;
|
||||||
|
|
||||||
if (!list || !Array.isArray(list)) {
|
if (!list || !Array.isArray(list)) {
|
||||||
if (log_lvl > 1) console.log('visible_event_obj_li: Waiting for data stream...');
|
if (log_lvl > 1) console.log('visible_event_obj_li: Waiting for data stream...');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = list.filter((item: any) => {
|
/**
|
||||||
if (!item) return false;
|
* DUAL-LAYER VISIBILITY LOGIC
|
||||||
|
* 1. Public Logic: If an item is enabled and NOT hidden, everyone sees it.
|
||||||
|
* 2. Trusted Logic: If an item is disabled OR hidden, ONLY trusted users see it.
|
||||||
|
*
|
||||||
|
* NOTE: We use loose equality (==) because the database often returns
|
||||||
|
* numeric 1/0 or string '1'/'0' for these tinyint fields.
|
||||||
|
*/
|
||||||
|
const filtered = list.filter((item: any) => {
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
// ADMIN/TRUSTED: See everything
|
const is_hidden = item.hide == true;
|
||||||
if ($ae_loc.trusted_access) return true;
|
const is_disabled = item.enable == false;
|
||||||
|
|
||||||
// PUBLIC: Filter hidden/disabled
|
if (!is_hidden && !is_disabled) return true;
|
||||||
// Safely handle null/undefined fields by assuming visible/enabled (permissive default)
|
|
||||||
const is_hidden = item.hide === true || item.hide === 1;
|
|
||||||
const is_disabled = item.enable === false || item.enable === 0; // Only block if explicitly false/0
|
|
||||||
|
|
||||||
return !is_hidden && !is_disabled;
|
return ($ae_loc.trusted_access === true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (log_lvl) console.log(`visible_event_obj_li: Input=${list.length}, Output=${filtered.length} (trusted=${$ae_loc.trusted_access})`);
|
||||||
|
|
||||||
|
// Final safety slice to respect the user's limit selection
|
||||||
|
return filtered.slice(0, $idaa_loc.recovery_meetings.qry__limit || 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (log_lvl) console.log(`visible_event_obj_li: Input=${list.length}, Output=${filtered.length} (trusted=${$ae_loc.trusted_access})`);
|
|
||||||
|
|
||||||
return filtered.slice(0, $idaa_loc.recovery_meetings.qry__limit || 150);
|
|
||||||
})());
|
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(`link_to_type: ${link_to_type}; link_to_id: ${link_to_id}`);
|
console.log(`link_to_type: ${link_to_type}; link_to_id: ${link_to_id}`);
|
||||||
|
|||||||
@@ -39,9 +39,17 @@
|
|||||||
let dq__where_type_id_val: string = `${link_to_type}_id`;
|
let dq__where_type_id_val: string = `${link_to_type}_id`;
|
||||||
let dq__where_eq_id_val: string = link_to_id;
|
let dq__where_eq_id_val: string = link_to_id;
|
||||||
|
|
||||||
// Stable LiveQuery Pattern (Aether UI V3)
|
/**
|
||||||
// We wrap in $derived to ensure Svelte recreates the observable
|
* Stable LiveQuery Pattern (Aether UI V3)
|
||||||
// whenever the input IDs (props) change.
|
*
|
||||||
|
* WHY: We wrap liveQuery in $derived to ensure that Svelte recreates the
|
||||||
|
* Dexie observable whenever the input props (event_id_random_li) change.
|
||||||
|
*
|
||||||
|
* TWO SCENARIOS:
|
||||||
|
* 1. Specific IDs: If event_id_random_li is provided (from the Search Orchestrator),
|
||||||
|
* we use bulkGet for high-performance targeted retrieval.
|
||||||
|
* 2. Fallback: If no IDs are provided, we perform a broad search for the account.
|
||||||
|
*/
|
||||||
let lq__event_obj_li = $derived(
|
let lq__event_obj_li = $derived(
|
||||||
liveQuery(async () => {
|
liveQuery(async () => {
|
||||||
const ids = event_id_random_li;
|
const ids = event_id_random_li;
|
||||||
@@ -51,6 +59,7 @@
|
|||||||
if (ids.length > 0) {
|
if (ids.length > 0) {
|
||||||
if (log_lvl) console.log(`Wrapper LQ: bulkGet ${ids.length} IDs`);
|
if (log_lvl) console.log(`Wrapper LQ: bulkGet ${ids.length} IDs`);
|
||||||
const results = await db_events.event.bulkGet(ids);
|
const results = await db_events.event.bulkGet(ids);
|
||||||
|
// bulkGet returns undefined for missing keys; we filter those out.
|
||||||
return results.filter(item => item !== undefined);
|
return results.filter(item => item !== undefined);
|
||||||
} else {
|
} else {
|
||||||
if (log_lvl) console.log('Wrapper LQ: Explicitly empty list');
|
if (log_lvl) console.log('Wrapper LQ: Explicitly empty list');
|
||||||
|
|||||||
@@ -37,6 +37,13 @@
|
|||||||
|
|
||||||
let ae_promises: key_val = $state({});
|
let ae_promises: key_val = $state({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session Persistent Search (Refactored 2026-01-20)
|
||||||
|
*
|
||||||
|
* WHY: If the user has 'save_search_text' enabled in their local profile,
|
||||||
|
* we map the long-term saved strings into the current session store on mount.
|
||||||
|
* This ensures the search bar is pre-populated with their last known query.
|
||||||
|
*/
|
||||||
if (
|
if (
|
||||||
$idaa_loc.recovery_meetings?.save_search_text &&
|
$idaa_loc.recovery_meetings?.save_search_text &&
|
||||||
$idaa_loc.recovery_meetings?.saved_search__session
|
$idaa_loc.recovery_meetings?.saved_search__session
|
||||||
@@ -53,6 +60,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *** Functions and Logic
|
// *** Functions and Logic
|
||||||
|
/**
|
||||||
|
* Reactive Search Trigger
|
||||||
|
*
|
||||||
|
* WHY: Instead of calling handle_search_refresh() directly (which can be messy with
|
||||||
|
* Svelte 5's fine-grained reactivity), we increment a 'version' number.
|
||||||
|
* The $effect in the parent +page.svelte tracks this version and handles the
|
||||||
|
* debounced search cycle automatically.
|
||||||
|
*/
|
||||||
function handle_search_trigger() {
|
function handle_search_trigger() {
|
||||||
if ($idaa_loc.recovery_meetings.search_version === undefined) {
|
if ($idaa_loc.recovery_meetings.search_version === undefined) {
|
||||||
$idaa_loc.recovery_meetings.search_version = 0;
|
$idaa_loc.recovery_meetings.search_version = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user