Implement exhibitor search constraints and sync telemetry

- Restricted public exhibitor search to Priority (paid) items only.
- Enforced 3-character search minimum for non-trusted users on landing page.
- Implemented real-time sync telemetry (last refresh and countdown) in Manage tab.
- Added auto-refresh logic with configurable intervals.
This commit is contained in:
Scott Idem
2026-02-08 19:46:53 -05:00
parent b3114c619a
commit 16acae03d0
5 changed files with 67 additions and 5 deletions

View File

@@ -362,6 +362,7 @@ export async function search__exhibit({
fulltext_search_qry_str = null,
enabled = 'enabled',
hidden = 'not_hidden',
priority = 'all',
view = 'default',
order_by_li = { name: 'ASC' },
limit = 100,
@@ -374,6 +375,7 @@ export async function search__exhibit({
fulltext_search_qry_str?: string | null;
enabled?: 'enabled' | 'all' | 'not_enabled';
hidden?: 'hidden' | 'all' | 'not_hidden';
priority?: 'all' | 'priority' | 'not_priority';
view?: string;
order_by_li?: any;
limit?: number;
@@ -398,6 +400,9 @@ export async function search__exhibit({
if (hidden === 'hidden') search_query.and.push({ field: 'hide', op: 'eq', value: 1 });
else if (hidden === 'not_hidden') search_query.and.push({ field: 'hide', op: 'eq', value: 0 });
if (priority === 'priority') search_query.and.push({ field: 'priority', op: 'eq', value: 1 });
else if (priority === 'not_priority') search_query.and.push({ field: 'priority', op: 'eq', value: 0 });
try {
const result_li = await api.search_ae_obj_v3({
api_cfg,

View File

@@ -477,6 +477,9 @@ const events_session_data_struct: key_val = {
submit_status__search: null, // 'searching', 'complete'
last_refresh_time: null as string | null,
next_refresh_countdown: 0,
// The entered_passcode is the exhibit booths shared passcode for staff. This is used to initially access the lead retrieval service.
entered_passcode: null,

View File

@@ -85,17 +85,26 @@
const current_search_id = ++last_search_id;
const event_id = params.event_id;
const remote_first = params.remote_first;
const qry_str = params.str;
if (!event_id) return;
// --- Search Constraint: Min 3 characters for non-trusted users ---
if (!$ae_loc.trusted_access && qry_str.length < 3) {
if (log_lvl) console.log('🛑 [Trace] Search string too short for public user.');
untrack(() => {
exhibit_id_li = [];
$events_sess.leads.submit_status__search = 'idle';
});
return;
}
if (log_lvl) console.log(`🔎 [Trace] Exhibit Search #${current_search_id}: START (remote=${remote_first}, event=${event_id}, str=${params.str})`);
untrack(() => {
$events_sess.leads.submit_status__search = 'searching';
});
const qry_str = params.str;
// 1. FAST PATH: Local IDB Search
if (!remote_first) {
try {
@@ -103,6 +112,9 @@
.where('event_id')
.equals(event_id)
.filter((exhibit) => {
// Priority Filter for Public
if (!$ae_loc.manager_access && !exhibit.priority) return false;
if (qry_str) {
const name = (exhibit.name ?? '').toLowerCase();
const code = (exhibit.code ?? '').toLowerCase();
@@ -111,6 +123,9 @@
!code.includes(qry_str)
)
return false;
} else if (!$ae_loc.trusted_access) {
// Don't show default results to public if no search string
return false;
}
return true;
})
@@ -178,6 +193,7 @@
api_cfg: $ae_api,
event_id: event_id,
fulltext_search_qry_str: qry_str || null,
priority: $ae_loc.manager_access ? 'all' : 'priority',
order_by_li,
limit: 100
});

View File

@@ -123,6 +123,8 @@
search_debounce_timer = setTimeout(() => {
untrack(() => {
handle_search_refresh(params);
// Reset countdown on manual search
$events_sess.leads.next_refresh_countdown = $events_loc.leads.refresh_interval_sec || 25;
});
}, 300);
return () => {
@@ -130,6 +132,25 @@
};
});
// --- Auto-Refresh Timer Logic ---
$effect(() => {
if (!is_signed_in) return;
const interval = setInterval(() => {
untrack(() => {
if ($events_sess.leads.next_refresh_countdown > 0) {
$events_sess.leads.next_refresh_countdown--;
} else {
// Trigger refresh
$events_loc.leads.tracking__search_version++;
$events_sess.leads.next_refresh_countdown = $events_loc.leads.refresh_interval_sec || 25;
}
});
}, 1000);
return () => clearInterval(interval);
});
async function handle_search_refresh(params: any) {
const qry_key = JSON.stringify(params);
if (qry_key === last_executed_key) return;
@@ -268,6 +289,7 @@
untrack(() => {
tracking_id_li = api_ids;
$events_sess.leads.submit_status__search = 'done';
$events_sess.leads.last_refresh_time = new Date().toISOString();
});
}
} catch (error) {

View File

@@ -7,7 +7,7 @@
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events_functions';
import Element_ae_crud_v2 from '$lib/elements/element_ae_crud_v2.svelte';
import Comp_exhibit_license_list from './ae_comp__exhibit_license_list.svelte';
@@ -340,9 +340,25 @@
<!-- List Refresh -->
<div class="space-y-3">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest">Data Synchronization</div>
<div class="flex items-center justify-between">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest">Data Synchronization</div>
<div class="flex items-center gap-2 text-[10px] font-mono opacity-60">
<span class="fas fa-clock"></span>
{#if $events_sess.leads.last_refresh_time}
Last: {new Date($events_sess.leads.last_refresh_time).toLocaleTimeString()}
{:else}
Waiting...
{/if}
</div>
</div>
<div class="flex items-center gap-4 p-2 bg-surface-500/5 rounded-lg border border-surface-500/10">
<span class="text-sm flex-1">Refresh Interval (sec)</span>
<div class="flex-1 space-y-1">
<span class="text-sm block">Refresh Interval (sec)</span>
<div class="text-[9px] opacity-40 uppercase font-bold">
Next Sync in <span class="text-primary-500">{$events_sess.leads.next_refresh_countdown}s</span>
</div>
</div>
<input
type="number"
class="input w-20 text-right font-mono p-1 bg-transparent border-b border-surface-500/20"