diff --git a/src/lib/ae_events/ae_events__event.ts b/src/lib/ae_events/ae_events__event.ts index 008f6eb2..0e903a60 100644 --- a/src/lib/ae_events/ae_events__event.ts +++ b/src/lib/ae_events/ae_events__event.ts @@ -1095,6 +1095,14 @@ export function sync_config__event_badges({ loc.enable_search_qr = badges_cfg_remote?.enable_search_qr ?? true; loc.qr_type = badges_cfg_remote?.qr_type ?? null; + // Per-tier search constraints + loc.anon_search_result_limit = badges_cfg_remote?.anon_search_result_limit ?? 15; + loc.anon_search_min_chars = badges_cfg_remote?.anon_search_min_chars ?? 3; + loc.auth_search_result_limit = badges_cfg_remote?.auth_search_result_limit ?? 25; + loc.auth_search_min_chars = badges_cfg_remote?.auth_search_min_chars ?? 2; + loc.trusted_search_result_limit = badges_cfg_remote?.trusted_search_result_limit ?? 150; + loc.trusted_search_min_chars = badges_cfg_remote?.trusted_search_min_chars ?? 1; + // Passcodes and permissions (may be null) loc.trusted_passcode = badges_cfg_remote?.trusted_passcode ?? null; loc.administrator_passcode = badges_cfg_remote?.administrator_passcode ?? null; diff --git a/src/lib/stores/ae_events_stores__badges_defaults.ts b/src/lib/stores/ae_events_stores__badges_defaults.ts index 297026d4..c86978bf 100644 --- a/src/lib/stores/ae_events_stores__badges_defaults.ts +++ b/src/lib/stores/ae_events_stores__badges_defaults.ts @@ -19,6 +19,17 @@ export interface BadgesRemoteCfg { enable_upload_badge_li_btn: boolean; // show the "Upload Badge List" button enable_search_qr: boolean; // enable QR scan search + // Per-access-tier search limits. + // anonymous — not signed in at all + // auth — public passcode or identity-verified (Authenticated/Public) + // trusted — onsite staff (Trusted) and above; above-trusted inherits trusted limits + anon_search_result_limit: number; + anon_search_min_chars: number; + auth_search_result_limit: number; + auth_search_min_chars: number; + trusted_search_result_limit: number; + trusted_search_min_chars: number; + // QR code configuration qr_type: string | null; // QR payload format (e.g. 'badge_id', 'url') @@ -117,6 +128,13 @@ export interface BadgesLocState { trusted?: { can_edit: string[] | '*' }; administrator?: { can_edit: string[] | '*' }; } | null; + // Per-tier search constraints (mirrored from mod_badges_json) + anon_search_result_limit: number; + anon_search_min_chars: number; + auth_search_result_limit: number; + auth_search_min_chars: number; + trusted_search_result_limit: number; + trusted_search_min_chars: number; // Timestamp when the remote config was last mirrored locally remote_cfg_last_synced_on: string | null; } @@ -177,6 +195,13 @@ export const badges_loc_defaults: BadgesLocState = { trusted_passcode: null, administrator_passcode: null, edit_permissions: null, + // Per-tier search constraints — conservative defaults; overridden per event via mod_badges_json. + anon_search_result_limit: 15, + anon_search_min_chars: 3, + auth_search_result_limit: 25, + auth_search_min_chars: 2, + trusted_search_result_limit: 150, + trusted_search_min_chars: 1, remote_cfg_last_synced_on: null }; diff --git a/src/routes/events/[event_id]/(badges)/badges/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/+page.svelte index 5cd9cef5..efa1e67a 100644 --- a/src/routes/events/[event_id]/(badges)/badges/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/+page.svelte @@ -111,8 +111,10 @@ let lq__event_badge_obj_li = $derived.by(() => { } // SCENARIO 2: Fallback broad search (Only if no active filters) + // Unauthenticated users must enter a query — never show the full attendee list. if ( event_id && + $ae_loc.trusted_access && !badges_loc.current.fulltext_search_qry_str && badges_loc.current.qry_printed_status === 'all' && !badges_loc.current.qry_affiliations && @@ -135,6 +137,28 @@ let lq__event_badge_obj_li = $derived.by(() => { // Standardized Reactive Search Pattern (Aether UI V3) // 1. Isolate dependencies into a stable derived object +// Resolve per-tier search constraints from the event config (mirrored from mod_badges_json). +// Tier order (highest wins): trusted_access → auth (public/authenticated) → anonymous. +let effective_search_limits = $derived.by(() => { + if ($ae_loc.trusted_access) { + return { + result_limit: badges_loc.current.trusted_search_result_limit, + min_chars: badges_loc.current.trusted_search_min_chars + }; + } + if ($ae_loc.authenticated_access) { + // public_access / authenticated — signed in but below trusted + return { + result_limit: badges_loc.current.auth_search_result_limit, + min_chars: badges_loc.current.auth_search_min_chars + }; + } + return { + result_limit: badges_loc.current.anon_search_result_limit, + min_chars: badges_loc.current.anon_search_min_chars + }; +}); + let search_params = $derived({ v: badges_loc.current.search_version, str: (badges_loc.current.fulltext_search_qry_str ?? '') @@ -145,7 +169,9 @@ let search_params = $derived({ aff: (badges_loc.current.qry_affiliations ?? '').toLowerCase().trim(), sort: badges_loc.current.qry_sort_order, event_id: $events_slct?.event_id, - remote_first: badges_loc.current.qry__remote_first + remote_first: badges_loc.current.qry__remote_first, + result_limit: effective_search_limits.result_limit, + min_chars: effective_search_limits.min_chars }); // 2. Controlled effect for triggering searches @@ -188,6 +214,18 @@ async function handle_search_refresh(params: any) { const type_code = params.type; const printed_status = params.printed; const aff_str = params.aff; + const result_limit = params.result_limit; + const min_chars = params.min_chars; + + // Defense-in-depth: enforce min_chars even if the search component lets one through. + if (qry_str.length < min_chars) { + untrack(() => { + event_badge_id_li = []; + $events_sess.badges.search_status = 'done'; + $events_sess.badges.search_complete = true; + }); + return; + } // 2. FAST PATH: Local IDB Search if (!remote_first) { @@ -283,6 +321,9 @@ async function handle_search_refresh(params: any) { } }); + // Cap results to the effective per-tier limit. + local_results = local_results.slice(0, result_limit); + const local_ids = local_results .map((b) => b.event_badge_id) .filter(Boolean); @@ -340,7 +381,7 @@ async function handle_search_refresh(params: any) { printed_status: printed_status, affiliations_qry_str: aff_str || null, order_by_li: order_by_li, - limit: 150, + limit: result_limit, log_lvl: 0 }); diff --git a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte index 673e633a..dfc23dbc 100644 --- a/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/ae_comp__badge_search.svelte @@ -8,7 +8,7 @@ let { event_id, log_lvl = 0 }: Props = $props(); // *** Import other supporting libraries import { - Library, + // Library, LoaderCircle, QrCode, RemoveFormatting, @@ -19,15 +19,15 @@ import { events_sess } from '$lib/stores/ae_events_stores'; import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte'; import Element_qr_scanner from '$lib/elements/element_qr_scanner.svelte'; import { ae_util } from '$lib/ae_utils/ae_utils'; -import { liveQuery } from 'dexie'; -import { db_events } from '$lib/ae_events/db_events'; +// import { liveQuery } from 'dexie'; +// import { db_events } from '$lib/ae_events/db_events'; -let lq__event_obj = $derived( - liveQuery(async () => { - if (!event_id) return null; - return await db_events.event.get(event_id); - }) -); +// let lq__event_obj = $derived( +// liveQuery(async () => { +// if (!event_id) return null; +// return await db_events.event.get(event_id); +// }) +// ); // ISHLT 2024 badge type codes let badge_type_code_li = [ @@ -46,7 +46,17 @@ let badge_type_code_li = [ { code: 'test', name: 'Test' } ]; +// Resolve the minimum characters required for the current user's access tier. +// Mirrors the 3-tier logic in +page.svelte so both enforce the same threshold. +let effective_min_chars = $derived.by(() => { + if ($ae_loc.trusted_access) return badges_loc.current.trusted_search_min_chars; + if ($ae_loc.authenticated_access) return badges_loc.current.auth_search_min_chars; + return badges_loc.current.anon_search_min_chars; +}); + function handle_search_trigger() { + const qry = (badges_loc.current.fulltext_search_qry_str ?? '').trim(); + if (qry.length < effective_min_chars) return; badges_loc.current.search_version++; } @@ -160,6 +170,12 @@ function handle_qr_scan_result(event: { title="Search by name, email, etc. Press Enter." /> + {#if (badges_loc.current.fulltext_search_qry_str ?? '').trim().length < effective_min_chars} +

+ Enter at least {effective_min_chars} character{effective_min_chars === 1 ? '' : 's'} to search +

+ {/if} +
+ {#if sections.search_limits} +
+

+ Controls how many results each access tier can see and how many characters + they must type before a search fires. Trusted and above use the trusted limits. +

+
+ +
+

Anonymous (not signed in)

+ + +
+ +
+

Auth (public passcode / identity-verified)

+ + +
+ +
+

Trusted+ (onsite staff and above)

+ + +
+
+
+ {/if} + +