tests/README.md — new "IDAA Auth Tests" section with three lessons:
1. ae_idaa_loc seed must include full bb/archives structure or
verify_novi_uuid() throws silently and resets novi_uuid to null
2. StorageEvent pattern for testing reactive persisted-store updates
without pre-seeding Dexie or navigating twice
3. getByText { exact: false } for UUID in multi-field spans
GUIDE__SvelteKit2_Svelte5_DexieJS.md — new "untrack() reactive tracking
trap" section: reading a store value inside untrack() makes it a one-shot
dependency; fix is to hoist the read outside untrack() and add a guard
to avoid redundant work on unrelated store updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers 5 scenarios with extensive inline comments explaining business
context and the 2026-03-25 stale-cache root-cause fix:
1. Auth gate (Sev-1 regression guard) — no UUID → Access Denied
2. Happy path — valid UUID + fresh cfg → access granted
3. Invalid UUID — Novi 404 → Access Denied
4. Stale cache — StorageEvent delivers fresh site_cfg_json →
Effect 2 retries verification without reload (tests the reactive
tracking fix in (idaa)/+layout.svelte)
5. iframe mode — Reload/Retry button visible on Access Denied
Key lesson found while writing: ae_idaa_loc seed must include the full
bb object or verify_novi_uuid() throws on bb.qry__hidden assignment,
caught silently, resetting novi_uuid to null even after a successful
Novi API call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WHY: Novi UUID verification is async — on first iframe load the API call
may not complete before the access gate renders, leaving the user stuck on
Access Denied with no way to retry without manually reloading the host page.
The Reload/Retry button calls location.reload() to re-trigger verification.
Only shown in iframe mode where the timing race is the known failure path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The legacy /hosted_file/{id}/clip_video route was decommissioned with the
rest of the hosted_file router. Updated to /v3/action/hosted_file/{id}/clip_video.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The legacy /hosted_file/upload_files router was decommissioned (commented
out in registry.py). Both upload components now point to the active V3
endpoint at /v3/action/hosted_file/upload. Response shape is identical
so no consumer-side changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Replace incorrect untrack() with idempotent write guard in the
sys_menu trusted-access effect. untrack() prevents new dep reads but
ae_loc was already tracked from the outer condition reads, so the write
still re-notified the effect every run. The guard (only write if value
!= false) breaks the cycle: run 2 finds value already false, skips the
write, effect stops. Max 2 runs vs the previous infinite loop.
2. Hide auth shield, font-size cycler, and dark/light toggle in the sys
bar when in iframe mode — host page owns those concerns. Edit mode
toggle and the main expand button remain visible for staff.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes in the IDAA root layout:
1. Add missing `untrack` import and wrap `$ae_loc.sys_menu.hide = false`
in `untrack()` inside the trusted-access effect. Without this, reading
$ae_loc.iframe/$ae_loc.trusted_access and then writing back to $ae_loc
caused an infinite reactive loop → effect_update_depth_exceeded error.
Only hit by trusted/admin users in iframe mode (regular Novi members
at authenticated_access were unaffected).
2. When iframe URL param is explicitly set to 'false', restore
$ae_loc.sys_menu.hide = false. The root layout sets it to true on
iframe=true but never resets it, leaving the system bar permanently
hidden after leaving iframe mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trusted admins embedded in the Novi iframe can't append show_menu=true
to the src URL, so watch trusted_access reactively and unhide the sys
bar automatically when they authenticate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The x-no-account-id bypass was hardcoded to resolve account_id=1 on the
backend, causing account-scoped lookup overrides (e.g. custom country names)
to leak to all callers regardless of their account.
Removing the bypass lets get_object auto-promote the real account_id from
api_cfg, so the backend's existing account filter works correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Country and state/province fields were showing as plain text inputs because
liveQuery used orderBy() on non-indexed columns, causing silent Dexie errors
that left the store as undefined indefinitely.
- Fix: replaced orderBy() with toArray() + in-memory sort across all three
lookup types (country, country_subdivision, time_zone).
- Sort convention matches Aether backend: sort DESC (higher = first, NULL=0
last), then name ASC — puts priority entries at the top.
- Added db_lookups.ts (IDB schema for lookup tables) and updated core__countries,
core__country_subdivisions, core__time_zones to IDB-backed SWR pattern.
- Affected: archive edit, archive content edit, recovery meeting edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Weekday chips: replace bind:checked (unreliable with dynamic bracket notation in
{#each}) with explicit onchange handlers + class: directives; read weekdays from
state in submit handler instead of FormData
- Recurring pattern/times: bind select and time inputs to working copy
so values display and edit correctly
- Times clearing: map empty string to null so times can be cleared once set
- liveQuery guard: skip event_obj sync while edit form is open to prevent
background refresh from overwriting in-progress user changes
- Timezone lookup: forward order_by_li, limit, offset through the full call chain
so priority sort and result count params are actually sent to the API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Merge Rapid + Qualify scan modes into single Confirm mode with two-button card:
"Add & Scan Next" (resets) and "Add & View Lead" (navigates to detail). Same
two-button pattern on the reenable card: "Restore & Scan Next" / "Restore & View Lead".
Stale 'qualify' localStorage values normalized to 'rapid' via $derived.by().
- QR scanner speed: fps 10→25, qrbox 82%→88%, useBarCodeDetectorIfSupported (native
BarcodeDetector API on Chrome/Edge — significantly faster than ZXing JS fallback)
- Fix capture identity stored in external_person_id / group:
licensed exhibit user → their email; shared passcode → 'shared_passcode' label
(not the raw passcode); Aether user bypassing exhibit sign-in → access_type string
('trusted', 'manager', 'super', etc.). Consistent across all three lead capture
components (single scanner, multi scanner, manual search).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- QR scanner (single + multi): detect previously-removed leads via IDB enable flag;
route to 'reenable' state instead of duplicate error; offer Re-activate button
- API fallback: if create fails and no IDB record, search API for disabled tracking
record by event_exhibit_id + event_badge_id (adds qry_badge_id param to
search__exhibit_tracking)
- Lead detail page: Replace raw enable checkbox with Remove Lead (two-click confirm,
navigates back after) and Restore Lead card (shown when enable is falsy)
- Fix flash of disabled records in leads list: filter !enable in both filtered_lead_li
derived and local IDB fast-path in handle_search_refresh
- eslint.config.js: disable svelte/no-navigation-without-resolve (no base path configured)
- Also includes _random field annotation cleanup (db_events, ae_types), iframe layout
fixes, badge view tweaks, test updates, and doc updates from prior session
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When create_ae_obj__exhibit_tracking returns false/null (API down, network
error, auth failure), the scanner was left frozen at 'adding' indefinitely.
Added else branch to surface an error state in both single and multi scanners.
Also fixes multi scanner which wasn't capturing the API return value at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The browser fires beforeinstallprompt very early (~1s after page load),
before Svelte's $effects run. Moving the event listener registration to
module level ensures we never miss the event regardless of when init()
is called from the root layout.
init() now only handles dismiss state (localStorage) and standalone
detection (DOM) — both safe to defer until after component mount.
Platforms:
- Chrome / Chromium / Android: native install button via captured prompt
- iOS Safari: manual Share → Add to Home Screen instructions (unchanged)
- Firefox desktop: no beforeinstallprompt support (browser-level limitation);
Firefox shows its own install button in the address bar automatically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Eye/EyeOff toggle to exhibit tracking search bar; wired to
existing $events_loc.leads.show_hidden store field
- Guard + init the show_hidden field in +page.svelte
- Add show_hidden to search_params so toggling triggers a refresh
- Apply hide filter in local IDB search path (skip hidden unless toggled)
- Pass hidden: 'not_hidden' | 'all' to API search__exhibit_tracking call
- Apply hide filter in filtered_lead_li for the broad liveQuery fallback path
- README: remove stale allow_tracking/PWA/export gaps; add Implemented section;
fix ae_events_functions import path (moved to ae_events/ subdir)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>