- Replace admin field editor with direct TipTap + Save Notes button for exhibitor notes;
show Add Notes button when notes are empty (no dead placeholder)
- Add one-click priority star toggle in header (always visible, no edit mode required)
- Remove Exhibit Context card (exhibitors don't need to see their own booth name)
- Move Captured By into profile card with human-readable labels
(shared_passcode → "Booth (Shared)", access type codes → Staff/Admin)
- Add location row (city/state + country) to profile card
- Gate Remove button to edit mode only to prevent accidental taps
- Fix button position stability: Edit/View always rightmost (same screen position),
Remove grows in from left — prevents double-tap accidents
- Add unsaved-changes guard (beforeNavigate) covering both notes and custom question form
- Custom questions form: hide Save when no questions configured, show
"Configure in Manage Tab" link instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- leads_api_access toggle in Admin Tools (manager only)
- Account Status section for end users (payment/licenses/API badges + CSV export button)
- Sign-out fix: use Object.fromEntries instead of delete on PersistedState proxy
- Shared passcode sign-in redirects directly to Manage tab (their role is config, not capture)
- Manage tab section reorder: Account Status → Lead Retrieval Config → Booth Profile → Access & Security → App Settings
- Filter dropdown: replace abstract "My Leads" with direct identity options (All / Booth (Shared) / per-licensee); auto-resolves and migrates stale 'my' values
- Lead detail: replace Element_ae_obj_field_editor notes with direct TipTap editor + Save Notes button; Add Notes button on empty state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**Session persistence bug** — leads_loc_defaults was missing __version: 1.
store_versions.ts wipes ae_leads_loc when parsed.__version !== 1 (always true
when the field is absent), so every page reload cleared auth_exhibit_kv and
forced re-login. Adding __version: 1 to both the interface and defaults fixes
this for all auth types.
**Manage tab fixes:**
- Description: collapsed by default with ChevronDown/Up toggle — same pattern
as session_view.svelte. Avoids long promo copy dominating the manage screen.
- Staff Passcode: removed duplicate green plain-text display for admins; the
Element_ae_obj_field_editor already shows the value (was showing twice).
- Booth Identifier: replaced static read-only display with Element_ae_obj_field_editor
so the booth code (exhibit.code) is editable inline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents silent no-op when user clicks submit before lq__exhibit_obj is ready
(exhibit not yet written to Dexie). Button now shows 'Loading...' spinner while
the exhibit record is resolving, eliminating the two-tap workaround needed on
first page load.
Also adds 7 Playwright tests for licensed user sign-in (leads_licensed_signin.test.ts)
covering success path, wrong credentials, email/identity tagging on captured leads,
identity isolation between staff members, and returning-session bypass.
Helpers: attach_leads_routes/setup_leads_test_page now accept exhibit_overrides
(e.g. license_li_json) to inject licensed users into mocked API responses.
seed_leads_loc import added to leads_auth.test.ts multi-exhibit test.
Total leads test coverage: 29 tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /events/[event_id]/leads/config page: administrator UI for
mod_exhibits_json. Controls leads_require_payment toggle and Stripe
keys (publishable key + buy button IDs per license tier).
- leads_require_payment (mod_exhibits_json) now gates all billing UI:
header CreditCard button in exhibit +page.svelte and Licenses & Billing
accordion in ae_tab__manage.svelte. Default false (client covers costs).
- Stripe keys migrated from site_cfg_json to mod_exhibits_json (per-event).
ae_comp__exhibit_payment accepts them as optional props; falls back to
site_cfg_json for events not yet migrated.
- Fixed "My Leads" bug for shared-passcode users: search_params now maps
licensee_email 'my' → 'shared_passcode' literal (not kv.key passcode
string) so filters correctly match stored external_person_id values.
- Event settings: Exhibits section replaced with config link + raw JSON
fallback, matching pres_mgmt/badges pattern.
- Docs updated: README.md, MODULE__AE_Events_Exhibitor_Leads.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove show__launcher_link_legacy from PressMgmtRemoteCfg, PresMgmtLocState, and
pres_mgmt_loc_defaults — the Flask/legacy launcher is retired
- Sync function now hardcodes hide__launcher_link_legacy=true (always hidden)
- Config page: back button to pres_mgmt, save buttons disabled until changes made
- Fix {#each} key expressions in config page
- Migrate e_app_access_type and element_manage_event_file_li to pres_mgmt_loc store
- Add temporary svelte type augments file (src/types/)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces untyped $events_loc.pres_mgmt (svelte-persisted-store) with a
dedicated pres_mgmt_loc (runed PersistedState) backed by a fully typed
PresMgmtLocState interface and PressMgmtRemoteCfg for the server-side JSON.
Key changes:
- ae_events_stores__pres_mgmt_defaults.ts: canonical interfaces + defaults
covering all hide__/show__ fields, labels, report prefs, query filters,
and lock_config sync fields; qry_enabled uses 'not_enabled' (matches API)
- ae_events_stores__pres_mgmt.svelte.ts: new PersistedState store
- ae_events__event.ts: sync_config__event_pres_mgmt() rewired to write
directly to pres_mgmt_loc.current; launcher link inversion preserved
- All 26+ pres_mgmt templates migrated from $events_loc.pres_mgmt.* to
pres_mgmt_loc.current.*
- New config UI at (pres_mgmt)/pres_mgmt/config/ — manager + edit mode only
- Event settings page: removed embedded pres_mgmt form, links to config page
- event_page_menu: Config button visible only when manager_access + edit_mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
start_datetime and end_datetime were visible as chips but had no edit control.
Added two datetime-local field editors shown in edit_mode below the display chip:
- Converts stored "YYYY-MM-DD HH:mm:ss" → "YYYY-MM-DDTHH:MM" for the input
(safe because dayjs has no timezone plugin — times are stored as local time)
- Falls back to event start date + 08:00/09:00 when session datetime is null,
so staff only need to adjust the time rather than retype the full date
- Editors are side-by-side in a flex-wrap row with min-width so they wrap on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The code badge was display-only — replaced with a field editor so staff
can correct session codes without going to a separate admin view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Event location (FK lookup) and description were both visible in the session
view but had no edit controls — lost during V3 migration. Restored both:
- event_location_id: select dropdown populated from this event's location list
(liveQuery on db_events.location filtered by event_id from the session object)
- description: textarea editor shown directly in edit_mode (no collapse needed
when actively editing)
Also added event_location_id to editable_fields__event_session, which was
missing and would have caused backend rejections on PATCH.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Jitsi invite dialog can expose backend room URLs and paths.
Previously invite was gated on is_moderator (any Novi group moderator).
Now restricted to $ae_loc.trusted_access (IDAA staff in Aether) so
regular member moderators cannot send invites. All other toolbar
buttons are unchanged.
Previously only moderators received a JWT; non-moderators joined
anonymously. Now all verified Novi users get a JWT with the
is_moderator flag set appropriately, allowing the Jitsi server to
enforce authentication and respect context.user.moderator for
all participants.
Also adds JWT payload decode logging (client-side, signature not
verified) so the moderator flag and user identity can be confirmed
in the browser console during testing.
- Add `inc_file_counts` flag to `load_ae_obj_id__event_session` — maps to
backend alt view (v_event_session_w_file_count) when true; default stays
lightweight. Callers never pass raw view names.
- Preserve-on-write fallback in `_refresh_session_id_background` keeps
cached file_count/file_count_all if API response omits them.
- Session detail +page.ts uses `inc_file_counts: true` so SvelteKit prefetch
no longer clobbers counts via bulkPut on hover.
- Remove explicit `view: 'alt'` from launcher +page.ts (now invalid param).
- Session list link: flex-1 + min-w-0 for full-row width; name flex-1 pushes
badge group right; code + file_count stacked in flex-col items-end.
- Hover styling: button-like appearance with slow fade-out (duration-500) /
fast snap-in (hover:duration-150).
- Session +page.svelte: use url_session_id (string) for link_to_id props and
auth__kv.session[] index — fixes TS type error from number|undefined.
- IDAA layout: dormant tech notice banner (guarded by 1==3, remove when ready).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Access keys cleared from all site_domain records. Bypassing the entire
key verification block to unblock IDAA. TODO: restore when keys are re-added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When navigating within the iframe (e.g. meeting list → meeting detail),
the UUID is only present on the initial iframe src URL — internal SvelteKit
<a href> links don't carry it forward. The layout effect was unconditionally
clearing novi_verified on every navigation that lacked a UUID, causing
"Access Denied" on every internal link click.
Fix: if a valid TTL-cached Novi session exists when no UUID is in the URL,
treat it as internal navigation and preserve the session rather than wiping it.
Non-Novi paths (no session, no UUID) still clear and deny as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
key_checked was set to boolean true in Case 3, which +layout.svelte then
persisted back to localStorage. On the next keyless navigation, the check
true === 'actual-key-string' always failed, causing Access Denied after
just one internal page navigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sites requiring a ?key= param (e.g. IDAA Novi iframe pages) no longer need
the key appended to every internal link after the first successful verification.
Stored key is always validated against the current site config from the API —
stale or rotated keys are denied immediately. Key present in URL always takes
the strict live-validation path with no cache shortcut.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unconditional assignment was overwriting $state defaults (incoming msg,
reactions, raise hand all muted) with false whenever the iframe template
didn't pass the sound URL params — which it never does.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Temporary rollback — non-moderators rejoin anonymously until Prosody is
configured with allow_empty_token=false to enforce JWT moderator claims.
TODO comment left in place to track the follow-up.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Issue JWT to all verified Novi users, not just moderators; unauthenticated
URL access no longer sufficient to join an IDAA video conference
- Remove 'embedmeeting' from Jitsi toolbar via explicit toolbarButtons whitelist;
the embed dialog exposed the Jitsi host/room URL violating IDAA privacy rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend updated (2026-03-31) to return default_qry_str in event API responses.
Frontend now stores it via properties_to_save and searches it in both the local
Dexie fast-path filter and the secondary post-API client filter. Previously, the
server searched default_qry_str (e.g. day-of-week, recurring_text) while the
client only checked name/description/location_text -- causing local results to
drop valid matches on revalidation (e.g. searching 'Thursday').
Also adds TODO note to audit other event search pages for the same mismatch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a URL access_key is present, skip the Dexie cache fast-path in
lookup_site_domain entirely — the key must be validated against the API.
Previously, a stale cached entry with a previously-valid key would be
returned immediately, allowing access even after the key changed or
was revoked in the URL.
Also: add site_domain_access_key to properties_to_save__site_domain
so domain-level keys are persisted to Dexie for cache validation;
remove shadow access_key re-declaration in +layout.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: url_uuid was read once from window.location.search (const),
assuming UUID changes always cause a full iframe reload (Novi impersonation).
Manual URL edits within the same SvelteKit session keep the layout mounted,
leaving url_uuid stale — the TTL cache then hit for the OLD valid UUID,
granting access under the wrong identity without re-verifying.
Fix:
- url_uuid is now $derived from $page.url.searchParams, updated on every
SvelteKit navigation
- url_uuid is read outside untrack() in Effect 2 so UUID changes trigger
a fresh verification run
- verify_failed (boolean) replaced with verify_failed_for_uuid (string|null)
so the retry-loop latch is keyed to the specific failed UUID — a different
UUID in the URL is always a clean slate that gets verified fresh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clipboard API is blocked by default in many browsers when running inside
an iframe (requires explicit permission grant). IDAA members shouldn't need
to navigate browser settings to get a meeting link.
Added a readonly textarea below the two action buttons — click it to
select all, then Ctrl+C/Cmd+C. Works in every browser without any
permissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Security fixes (3 layers):
1. layout: verify_novi_uuid now rejects Novi 200 responses with no member
data — prevents non-existent UUIDs from passing as verified members
2. layout: access gate now requires $idaa_loc.novi_verified in addition to
novi_uuid (stale UUID alone was insufficient)
3. video_conferences: onMount guard aborts Jitsi init if the layout-verified
UUID doesn't match the URL UUID (defense-in-depth)
Also fixes an infinite verification loop: when verification fails, writes to
$idaa_loc trigger storage events that cause $ae_loc to re-notify subscribers,
re-running Effect 2 indefinitely. Added verify_failed latch to stop retries —
the UUID is fixed for the page lifetime, retrying always produces the same result.
Feature: "Open Externally" button + modal (iframe mode only) lets IDAA members
escape the Novi iframe when scrolling/layout is broken. Options: copy link to
clipboard or open in new tab. Accessible to all users without edit-mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If the "Verifying identity..." spinner is still visible after 8 seconds,
show an escape-hatch button that clears ae_loc + ae_idaa_loc from
localStorage and reloads — forcing a fresh site config fetch which
re-populates novi_idaa_api_key so verification can actually run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full implementation of ae_comp__exhibit_payment.svelte (was a 9-line stub).
Reads Stripe config from $ae_loc.site_cfg_json per-event. License tier
selector (1/3/6/10 users) uses {#key} remount pattern to work around
stripe-buy-button web component ignoring attribute changes after mount.
Three states: paid confirmation (priority=true), not-configured hint, payment
form. client_reference_id=exhibit_id ties payments to booth records.
TypeScript declaration for stripe-buy-button added to app.d.ts via
svelte/elements augmentation. exhibit_id prop wired in +page.svelte and
ae_tab__manage.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$ae_loc.trusted_access is only ever upgraded, never downgraded — it sticks
across Novi impersonation even though a different UUID is in the URL. Instead,
check user_id directly against $idaa_loc.novi_admin_li / novi_trusted_li so
the moderator grant is tied to the specific UUID being used, not the inherited
session access level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>