badges: per-tier search limits — result cap + min chars, config UI

Add anonymous/auth/trusted search constraints to BadgesRemoteCfg with
conservative defaults (anon: 15 results / 3 chars, auth: 25 / 2,
trusted+: 150 / 1). Configurable per event via mod_badges_json.

- BadgesRemoteCfg + BadgesLocState: 6 new fields with defaults
- sync_config__event_badges: mirrors new fields from mod_badges_json
- +page.svelte: effective_search_limits derived by tier using $ae_loc
  cumulative flags; enforces min_chars guard and result cap on both
  local IDB path and API call
- ae_comp__badge_search: effective_min_chars derived same way; blocks
  search trigger below threshold; shows dynamic hint text
- Fallback broad search (SCENARIO 2) suppressed for non-trusted users
  so no results show on page load without a query
- config/+page.svelte: Search Limits section with 3-column number
  inputs (Anonymous / Auth / Trusted+) for result limit and min chars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-07 18:08:10 -04:00
parent be0b8baf62
commit ae9cdaf9f1
5 changed files with 198 additions and 12 deletions

View File

@@ -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
});