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

@@ -109,7 +109,13 @@ const cfg_defaults: BadgesRemoteCfg = {
authenticated: { can_edit: [...default_authenticated_can_edit] },
trusted: { can_edit: [...default_trusted_can_edit] },
administrator: { can_edit: '*' }
}
},
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
};
let draft: BadgesRemoteCfg = $state({ ...cfg_defaults });
@@ -230,6 +236,7 @@ async function save() {
// Section collapse state
let sections: Record<string, boolean> = $state({
ui: true,
search_limits: true,
qr: true,
passcodes: true,
auth_fields: true,
@@ -325,6 +332,95 @@ function toggle(key: string) {
{/if}
</section>
<!-- ================================================================ -->
<!-- SEARCH LIMITS -->
<!-- ================================================================ -->
<section class="border-surface-200-800 rounded-xl border">
<button
type="button"
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
onclick={() => toggle('search_limits')}>
<span>Search Limits per Access Tier</span>
{#if sections.search_limits}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
</button>
{#if sections.search_limits}
<div class="border-surface-200-800 border-t px-4 py-3 space-y-4">
<p class="text-xs text-surface-400">
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.
</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<!-- Anonymous -->
<div class="space-y-2">
<p class="text-sm font-semibold">Anonymous <span class="text-xs font-normal text-surface-400">(not signed in)</span></p>
<label class="flex flex-col gap-1">
<span class="text-xs text-surface-500">Result limit</span>
<input
type="number"
class="input input-sm"
min="1"
max="500"
bind:value={draft.anon_search_result_limit} />
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-surface-500">Min characters to search</span>
<input
type="number"
class="input input-sm"
min="1"
max="10"
bind:value={draft.anon_search_min_chars} />
</label>
</div>
<!-- Auth (Public / Authenticated) -->
<div class="space-y-2">
<p class="text-sm font-semibold">Auth <span class="text-xs font-normal text-surface-400">(public passcode / identity-verified)</span></p>
<label class="flex flex-col gap-1">
<span class="text-xs text-surface-500">Result limit</span>
<input
type="number"
class="input input-sm"
min="1"
max="500"
bind:value={draft.auth_search_result_limit} />
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-surface-500">Min characters to search</span>
<input
type="number"
class="input input-sm"
min="1"
max="10"
bind:value={draft.auth_search_min_chars} />
</label>
</div>
<!-- Trusted+ -->
<div class="space-y-2">
<p class="text-sm font-semibold">Trusted+ <span class="text-xs font-normal text-surface-400">(onsite staff and above)</span></p>
<label class="flex flex-col gap-1">
<span class="text-xs text-surface-500">Result limit</span>
<input
type="number"
class="input input-sm"
min="1"
max="1000"
bind:value={draft.trusted_search_result_limit} />
</label>
<label class="flex flex-col gap-1">
<span class="text-xs text-surface-500">Min characters to search</span>
<input
type="number"
class="input input-sm"
min="1"
max="10"
bind:value={draft.trusted_search_min_chars} />
</label>
</div>
</div>
</div>
{/if}
</section>
<!-- ================================================================ -->
<!-- QR CONFIG -->
<!-- ================================================================ -->