Badges without person_passcode are now viewable by anyone with the URL —
open access is granted on badge load. Previously this was explicitly
denied. The passcode entry form is only shown when the badge actually
has a passcode configured. Auto-validate effect expanded to cover the
no-passcode case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the single alert() call with an inline confirmation row per badge.
Clicking "Email Link" shows "Send / Cancel" in place so accidental sends
are avoided. Only one badge can be in confirm state at a time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces filter:hue-rotate() with CSS @property --ph-hue/--ph-lit animated
as numbers, applied as oklch() colors on SVG rect/line children. OKLCH keeps
perceptual lightness constant across hue rotation — no more brown/dark-blue
variance between slots. Pulse mode animates --ph-lit 0.42→0.78 for breathing.
Adds slow_pulse cfg_json flag + form checkbox. @property inherits:true lets
the animated value cascade from the div to its SVG children without per-child
animation declarations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds punch_holes.slow_pulse cfg_json flag. When enabled, replaces the fast
2.5s linear hue-rotate with a 6s ease-in-out breathing animation that dims
(0.55 brightness) to bright (1.30) while shifting 180° of hue and back.
Same 120° phase offsets apply (2s apart). Form shows a Slow Pulse checkbox
below the slot cards whenever at least one slot has rainbow enabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each slot gets a fixed animation-delay (left: 0s, right: -0.833s, center: -1.667s)
so they are 120° apart in the hue cycle — same speed, different start points.
Replaces the shared-wrapper approach (all same phase) with per-slot CSS classes
that encode the phase offset, giving a proper tri-phase RGB cycling effect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each rainbow-enabled slot now has a companion print-only div with an inline
SVG linearGradient (R→Y→G→C→B→M spectrum). On screen: the animated
hue-rotate div cycles. On print: CSS hides the animated div and shows the
static gradient instead. X lines print in semi-transparent black over the
gradient fill for visibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds left/right/center_rainbow to punch_holes cfg_json. When enabled,
applies a CSS hue-rotate animation (2.5s loop) to the marker div using
a saturated red base color so the full visible spectrum appears. Template
form shows a Rainbow checkbox per slot; hides color pickers when active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds header_padding_top/right/left alongside existing header_padding_bottom.
Removes hardcoded p-2 class — all four sides now set via inline style with
0.5rem default. Template form shows a 2×2 padding grid in Header & Branding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds left_fg/left_bg, right_fg/right_bg, center_fg/center_bg to punch_holes
cfg_json, plus shared fg/bg fallback. Template form shows color pickers per
slot (only when slot is enabled). Defaults: #777777 stroke, rgba white fill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
z-index: 10 ensures markers always render above the header image regardless
of DOM order. Inset 1mm on all sides from physical hole boundary to account
for printer registration variance (3mm-tall slot has no margin for error).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds cfg_json.punch_holes.{left,right,center} to mark pre-perforated badge
clip slots with X overlays. Slots are 5/8in x 1/8in, 1/4in from top,
3/8in from left/right edges. Markers print on the badge so attendees know
where to push out the perforations. Template form exposes checkboxes in
Header & Branding. Documented in MODULE__AE_Events_Badge_Templates.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded border-bottom/padding-bottom on badge_header div with
cfg_json fields: header_border_color, header_border_width, header_padding_bottom.
Empty color = no border. Template form exposes all three in Header & Branding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows a Templates button (manager+ edit mode only) before Create Badge,
linking directly to the badge templates management page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds cfg_json.header_margin_top to BadgeTemplateCfg. Badge view replaces
hardcoded mt-8 (2rem) with this value; falls back to 2rem when unset.
Template form exposes the field in the Header & Branding section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace show_hidden checkbox with visibility_filter select (Default /
Show Hidden / Show Disabled+Hidden) — collapses two orphaned boolean
fields (show_hidden, show_not_enabled) into one purpose-built value;
wires disabled-badge filter through to both IDB and API paths
- Add max-results stepper (edit mode only): steps of 25 up to 250,
steps of 100 up to 2550; tier-capped (trusted=250, manager=2550);
stepper uses pure reactivity — no handle_search_trigger() call needed
- Fix fallback liveQuery (SCENARIO 2): was hardcoded .limit(50);
now reads qry_result_limit in outer $derived.by so Svelte tracks it
and stepper updates the no-text browse list immediately
- Fix Search button disabled state: replace pointer-events-none +
class:opacity-50 with HTML disabled attribute + disabled:cursor-not-allowed
so hover cursor reflects disabled state correctly
- Global placeholder fix (app.css): add italic + opacity-0.6 rule for
.input/.textarea ::placeholder in light mode; add italic to dark rule —
prevents placeholder text from reading as typed content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
667 font files (~134MB) were being passed to cache.addAll() on every SW
install. cache.addAll() is atomic — a single failed request aborts the
entire install. Browser Cache Storage quota is typically 50-100MB on
mobile, so the SW has likely been silently failing to install on most
mobile devices, negating all caching and the skipWaiting fix.
Only 3 font files are referenced by the browser (app.css). The rest are
badge-print fonts for server/Electron use and do not need to be precached.
The browser's normal HTTP cache handles them when the print page is visited.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BOOTSTRAP__AI_Agent_Quickstart.md:
- Mistake #15 addendum: events/session modules use legacy tmp_sort_1 encoding
(priority=true→'1'), not build_tmp_sort — requires descending sort until migrated
- Mistake #16 (new): service worker without skipWaiting+clients.claim silently serves
stale code to long-lived tabs; explains the "can't reproduce in dev" pattern that
likely caused the IDAA recovery meetings issue for months
CLIENT__IDAA_and_customized_mods.md:
- New "Sort Encoding" section: table of legacy vs build_tmp_sort modules with
correct comparator direction for each
- New "Search Trigger" section: explains why $slct.account_id not $ae_loc.account_id
TODO__Agents.md:
- IDB Sort: added ae_events__event migration task + legacy encoding warning
- DevOps: marked service worker fix complete; replaced nginx caching item with
proxy buffer tuning task
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without these two calls, a new service worker installs in the background but
sits in 'waiting' state until every tab running the old version is closed.
Users who leave idaa.org open all day (common for IDAA members in the iframe)
would run the old JS bundle for hours or days after a fix is deployed, with no
indication that an update exists.
skipWaiting() — new SW activates immediately after install instead of waiting
clients.claim() — new SW takes control of all open tabs right away
This is the most likely root cause of "can't reproduce in testing but users
keep seeing the error": developers refresh/close tabs constantly, end users
don't. The old broken code kept running in their long-lived browser sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes for the IDAA Recovery Meetings load/display issues:
1. Sort direction: events use legacy encoding (priority ? 1 : 0), not build_tmp_sort.
priority=true→'1' requires DESCENDING sort to put priority items first. The prior
commit (ee79e33a2) incorrectly applied the build_tmp_sort-compatible ASC comparator
to the events module, which does not use that encoding. Reverted in +page.svelte
(both fast-path and API-results sort) and ae_idaa_comp__event_obj_li_wrapper.svelte.
2. Stale account_id gate: search $effect and handle_search_refresh now read
$slct.account_id (set only by the bootstrap Sync Effect, reliable) instead of
$ae_loc.account_id (persisted localStorage, may be stale from a prior session).
Follows the mistake #14 pattern from BOOTSTRAP__AI_Agent_Quickstart.md.
3. Clear Cache & Reload: now enumerates and deletes ALL IDB databases via
indexedDB.databases() (not just db_events.event), clears all localStorage and
sessionStorage (not just two keys), and preserves the iframe reload URL for
Novi re-authentication — matching the Full Reset pattern in e_app_help_tech.svelte.
4. IDB version bump: events.event → v3 (precautionary; flushes any stale cached
event records on next user load in case prior deploys missed a bump).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Controls column: max-w-sm (384px) prevents flex-1 from consuming all
remaining horizontal space on wide screens
- Row wrapper: justify-center + items-center centers the badge+controls
pair in the available width (equal margins on both sides)
- Mobile: full-width stacked layout unchanged; badge centered via items-center
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Print page: controls panel moves from position:fixed right overlay to an
in-flow flex column next to the badge. Sticky on lg+ screens; stacks below
badge on tablet/phone. Eliminates pr-64/pr-80 padding hacks on header and
badge area. Print behavior unchanged — .event_badge_wrapper uses position:fixed
relative to @page, so the surrounding layout structure is irrelevant.
- Remove is_editing (was only used to toggle fixed panel width; no longer needed).
- Debug JSON block removed from ae_comp__badge_obj_view (visual render component
should not contain admin tooling) and moved to controls Staff section.
- Debug JSON uses whitespace-pre + overflow-x-auto so long lines scroll
horizontally instead of wrapping into hundreds of short lines. Collapsible
via native <details>/<summary> with no JS state required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add pr-64/pr-80 + transition to page <header> so Review/Email actions
stay clear of the fixed right controls panel (same as badge_render_area)
- Remove redundant Re-print button from header — controls panel Test button
covers the same use case; having both caused clipping and confusion
- Fix datetime formatting: toLocaleString → ae_util.iso_datetime_formatter
(date_full_no_year + time_12_long) for consistent display with badge list
- Unify loading state to p-16/text-xl/mb-2 matching badge list style
- Polish Badge Not Found layout; swap button weights (Back = primary action)
- Add left-edge shadow to controls panel for better visual separation
- Badge view debug section: gate to administrator_access, add object ID
labels, add hover:max-h-64 expand and transition on pre blocks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a compact print info row below each printed badge for trusted users
(not in edit mode — debug row already covers that):
Printed 2× · First: June 9 9:14 AM · Last: June 9 11:32 AM
Gives staff quick at-a-glance confirmation that a badge was printed and when,
without needing to enter edit mode.
Also fixes a logic bug in the attendee "Checked in" card: the "last print"
line was comparing == (same datetime) instead of !== (different datetime),
so it only appeared when first = last (single print) — backwards. Fixed to
show only when multiple prints have occurred.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing the Search button or Enter with fewer than the required min chars
now focuses the input field instead of silently doing nothing, so the user
gets clear feedback about where to type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was: "Enter your name above to find your badge."
Now: "Search by name, email, or organization above to find your badge."
Mirrors the actual fast-path fields (given/family name, email, default_qry_str)
and the search input placeholder text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The +page.svelte {#if search_status=loading && ids.length===0} gate is now
redundant — ae_comp__badge_obj_li handles all states internally (Searching...,
Enter your name, No results). Removing it also fixes a UX regression where
trusted-user IDB fallback results would be hidden by the gate whenever a new
API search fired. Component is now always mounted and manages its own state.
Also replaces deprecated BarChart2 with ChartColumnBig (lucide rename).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
transition:fade on the initial spinner caused Svelte to keep the outgoing
element in the DOM for the full 200ms outro while the incoming badge list
was already rendered — both were live simultaneously, colliding on height
and producing the visible bounce. Initial cold-start load doesn't need a
transition; instant swap is fine.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scrollbar shift:
- Add [scrollbar-gutter:stable] to #ae_main_content in events layout so a
scrollbar appearing on first results load no longer reflows the centered
search form (was shifting ~8px left on Linux)
Empty/loading state consistency:
- Move search_status prop into ae_comp__badge_obj_li so it can swap its own
empty state: spinner + "Searching..." while a search is in progress,
UserSearch icon + prompt text otherwise
- Unify p-16 / size-3em / mb-2 / text-xl across all three states (initial
load, searching, no results) so height never jumps between transitions
- Pass search_status from +page.svelte to the component
Transitions:
- transition:fade on initial-load spinner div
- transition:slide on Create/Upload badge button row (appears with edit mode)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Adds fade/slide transitions throughout the search form: form mount/unmount,
filter row, QR scan button, QR scanner panel, Show Hidden, Remote First labels
- Min-chars hint switches from class:invisible to opacity-0/opacity-50 +
transition-opacity so it fades instead of snapping
- Clear button switches from class:hidden to opacity-0 + pointer-events-none
+ transition-all so it fades without causing layout shifts
- "Start Here" button gets transition-opacity for smooth dim on first keystroke
- Replaces FileSearch with UserSearch icon in the empty state
- Adds w-full to empty state div to prevent subtle page-width shift between
no-results and results states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Public (attendee) kiosk: unprinted badges link to /review; printed
badges show green "Checked in · Nx · First/Last" row (non-clickable)
- Public attendees no longer see Print button (staff-only action)
- Printed badges sort to end of list for public non-trusted users
- Manager access: Print (reprint) and Email Link buttons always visible
without requiring Edit Mode; main row behavior unchanged
- Empty state wording: context-aware — "Enter your name above to find
your badge" for public users with no query vs "No badges found" after
an actual search
- Docs: Epson C3500 fanfold section filled in (was empty placeholder);
style_href/duplex implementation status corrected in badge templates
doc; Axonius C3500 layout TODO marked complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build_tmp_sort() encodes priority=true as '0' for ascending sort. JS comparators
were using b.localeCompare(a) (descending), inverting the encoding so priority=false
items sorted first. Fixed to a.localeCompare(b) in ae_journals_search_helpers.ts (3
sites in recovery_meetings +page.svelte and wrapper component).
Also fixes a Dexie anti-pattern in bb/[post_id]: .reverse() before .sortBy() is a
no-op in Dexie; moved array .reverse() to after the await.
Documents the encoding rule and legacy inverted-encoding modules in
GUIDE__SvelteKit2_Svelte5_DexieJS.md and adds mistake #15 to BOOTSTRAP quickstart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds entry #14 to BOOTSTRAP__AI_Agent_Quickstart.md (section 7 "Mistakes
Agents Have Made") and a new "Bootstrap Race" subsection to
GUIDE__SvelteKit2_Svelte5_DexieJS.md ("Common Gotchas"), capturing the
fix from 5fce14980: gate account-scoped liveQuery triggers on
$slct.account_id (non-persisted), not $ae_loc.account_id (persisted,
potentially stale), and treat IDB records from a different non-null
account as a cache miss. Also fixes five pre-existing MD049 emphasis
style warnings (asterisk → underscore) in the Dexie guide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two guards added to the trigger effect:
1. Gate on $slct.account_id being set — prevents the fetch from firing before
the bootstrap Sync Effect has propagated the real account_id. Without this,
get_object's localStorage scavenge read a stale account_id (e.g. 1 from a
prior dev/demo session) and the API returned the wrong account's record.
2. Stale-account detection — if liveQuery returns an IDB row with a non-null
account_id that doesn't match the current account, treat it as a cache miss
and fetch the correct record. Null (global/default) rows are still accepted.
Root cause: ae_loc is a persisted store that hydrates from localStorage before
the bootstrap Sync Effect runs. Old account-specific IDB rows scored highest in
the liveQuery sort, suppressing the trigger and leaving the wrong record visible
until the next full page refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'Cvent Splash XLSX (registrant export)' upload mode hitting the new
/event/{id}/badge/import/splash_xlsx endpoint
- Admin controls: begin_at/end_at/return_detail (shared with Zoom mode) +
import_status_filter (splash only, default 'Attending')
- File picker accept attribute switches between .csv and .xlsx per mode
- Set timeout=300000 and retry_count=1 on both server-side upload paths to
prevent false 'no response' failures on slow imports; upsert-by-email on
the backend makes retries safe but a single attempt is sufficient
- Replace misleading 0/0 progress bar with an indeterminate progress bar
during server-side processing; real counter kept for client-side CSV mode
- Show 'Processing on server…' message once upload completes and server work begins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three related fixes for the hide/show toggle in Pres Mgmt:
1. ae_events__event_session.ts: remove redundant search_query.and hide
filter and instead pass `hidden` to api.search_ae_obj as a URL param.
The backend StatusFilterParams defaults to hidden='not_hidden', so
without this the API always filtered to hide=0 regardless of intent.
2. pres_mgmt/+page.svelte (SCENARIO 2): capture qry_hidden as a
$derived.by dependency so the liveQuery instance is recreated on
toggle — prevents hidden sessions briefly appearing before the
debounce fires (blink fix).
3. pres_mgmt/+page.svelte (API call): use params.qry_hidden snapshot
instead of the live store to prevent race if user toggles during a
pending search.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Group should partition before priority in the sort chain, consistent with
how all other AE objects are sorted (group → priority → sort → ...).
Was accidentally omitted when switching to build_tmp_sort.
Full order: group → priority DESC → sort ASC → start_datetime ASC → code ASC → name ASC
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates src/lib/ae_core/core__idb_sort.ts with build_tmp_sort() — a shared
helper for computing tmp_sort_1/2/3 fields stored in Dexie. Fixes two bugs
present in all generic _process_generic_props implementations:
- priority encoded as 0/1 ASC (true sorted last); now inverted: true→'0'
- sort stored as unpadded string ("10" < "2"); now 8-char zero-padded
Applied to:
- ae_events__event_presentation: replaces inline specific_processor code
- ae_journals__journal + ae_journals__journal_entry: replaces manual formulas;
journal liveQueries (.reverse().sortBy()) updated to plain .sortBy() since
the inverted encoding handles direction without needing Dexie's .reverse()
Other modules (sessions, presenters, locations, posts, core) left unchanged
until their sort behavior is reviewed separately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compute presentation-specific tmp_sort_1/tmp_sort_2 in specific_processor,
overriding the generic values from _process_generic_props which had two bugs:
- priority encoded as 0/1 ASC (backwards — true should sort first)
- sort stored as unpadded string ("10" < "2" lexicographically)
- start_datetime and code not included (presentation-specific fields)
New encoding: priority(inv)_sort(8-padded)_start_datetime_code[_name]
Both liveQueries (Pres Mgmt session page, Launcher session view) now use
.sortBy('tmp_sort_2') — cleaner and uses the indexed field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced single-field sortBy() (poster→name, oral→start_datetime) with
toArray() + JS comparator matching the same sort chain as Pres Mgmt.
Removes the sort_by branch since the comparator handles both session types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced single-field sortBy('name') with toArray() + JS comparator to
implement the full desired sort chain. Dexie's sortBy() only supports a
single indexed field, so multi-field ordering requires a JS sort pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Location page now calls load_ae_obj_li__event_file on mount so files
appear immediately without requiring a manual Refresh press.
- _load_event_location_sub_data (Launcher 60s sync) now uses hidden='all'
with default limit (100) instead of hidden='not_hidden'/limit=25, which
was pruning valid Dexie records when Pres Mgmt and Launcher were both
open on the same location simultaneously.
- Removed the legacy launcher button (Send icon, /event/ path) from the
Locations list; removed unused Send icon import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same nuclear cache-clearing bug as the upload component fix. Clicking "Refresh
files" for one Location was wiping every event_file record from Dexie, leaving
all other Locations and Presenters with no files until their own background
syncs fired.
Now does a targeted load for the specific object only — same pattern as the
upload component commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
db_events.file.clear() after every upload was nuking the entire Dexie file
table. Every liveQuery watching any file list (Launcher, other locations,
sessions, presenters) would immediately show 0 results. Only the uploaded-to
object's files were reloaded; all others remained empty until their own
background syncs fired — intermittent disappearance that depended on timing.
Fix: targeted load_ae_obj_li__event_file for only the current link_to_id,
which uses the SWR pattern (returns cache + background refresh that includes
the newly created file). Other objects' file caches are untouched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Launcher's background sync never called load_ae_obj_li__event_file for
presenter/session files. That function contains stale-record pruning that
removes deleted or hidden files from Dexie; without it, the Launcher's IDB
retained stale file records indefinitely until manually cleared.
Changes:
- refresh_presenter_data: add inc_file_li=true so presenter files are pruned
every 120s via the existing presenter loop
- refresh_current_session_files(): new function that fetches/prunes session-
level file lists for the selected session
- timer__file_list: 60s interval for refresh_current_session_files
- $effect on event_session_id: fires refresh_current_session_files immediately
on session switch (no wait for next timer tick)
Propagation time: deleted/hidden files visible on remote Launchers within
~60s (session files) or ~120s (presenter files) automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MODULE__AE_Events_Launcher_Config_Menu.md — v3.0 inventory of the
3-tab drawer layout (now superseded but kept as reference baseline).
MODULE__AE_Events_Launcher_Config_Menu_new.md — v3.1 unified design
spec that drove the sidebar tab migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two problems with the Flowbite <Modal> approach:
1. Built-in dismissable CloseButton rendered with no functional dismiss path
(no title/form), appearing centered in the panel.
2. size="xl" (max-w-7xl) left no backdrop area on typical laptop screens,
making outsideclose impossible to trigger.
Replace with a simple custom overlay: full-screen backdrop div that closes
on click, inner panel with stopPropagation. Matches the original Drawer
pattern. close_cfg() writes to store immediately on backdrop click for
reliable persistence independent of effect timing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs in the Launcher Config Modal after the Drawer→Modal migration:
1. Pre-existing persisted configs (missing hide_drawer__cfg field) caused
!undefined = true, opening the modal on every fresh load. Fixed by adding
a field-level initialization guard after the full-object guard.
2. $-syntax writes inside untrack() were suppressed by svelte-persisted-store,
so outside-click closure was never persisted. Fixed by using events_loc.update()
directly to ensure the write reaches localStorage serialization. Added equality
guard to effect 1 to prevent spurious modal flicker from whole-store re-fires.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When switching sessions within the same location, presentations and presenters
were not updating. The root cause: plain $derived(liveQuery(...)) never recreates
the Observable when slct__event_session_id changes, because liveQuery's async
callback runs in Dexie's zone where Svelte tracking is off. Dexie's range-level
change detection then ignores new session data (it arrives under a different
event_session_id index value, outside the originally observed range).
Replaced all four liveQuery declarations with $derived.by(() => { const id = ...;
return liveQuery(...id...); }) — the same pattern already used in +layout.svelte
for location-dependent queries. Svelte tracks the id read in the outer closure
and recreates the Observable on every session change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Proactively re-injects 'key' (site access key) and 'uuid' (Novi token)
into 'Open Externally' and 'Copy Link' URLs on the Video Conferences
page. This prevents authentication failures when members open meetings
in a new browser tab after SvelteKit internal navigation has dropped
the bootstrap parameters.
Updated CLIENT__IDAA_and_customized_mods.md to document the requirement
for these keys in breakout URLs.
+layout.svelte: add lg:min-h-8/12 and max-h-screen to main content area.
launcher_background_sync.svelte: reposition sync monitor panel (bottom-15,
left-2, z-10 — was bottom-20, left-4, z-9999).
launcher_menu.svelte: reorder Tailwind classes for readability, no change
to applied styles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts the change from d1f5d0e2f that removed ae_loc preservation.
The tech help component is used across non-Launcher contexts where
users are authenticated normally and should not be signed out.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
menu_launcher_controls: handle_cache_cleanup now removes both ae_events_loc
and ae_loc from localStorage, giving a true clean slate on reload.
e_app_help_tech: Clear & Reload button no longer silently re-saves ae_loc
after clearing — if edit mode wipes localStorage, ae_loc goes with it.
Updated confirm message and title tooltip to say "you will be signed out"
instead of the previous misleading "sign-in will be preserved."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fills in two new buttons added to menu_launcher_controls.svelte:
- Clear Cache: removes 'ae_events_loc' from localStorage and deletes the
ae_events_db IndexedDB database, then reloads — clears stale launch state
without touching downloaded file cache or user prefs (theme/font size).
- Reload Launcher: calls native.window_control({ action: 'reload' }) in
Electron, falls back to window.location.reload() in browser mode.
Also fixes a stray 'lucide-svelte' import (merged Recycle into '@lucide/svelte')
and separates cache_status from reset_apps_status so button labels stay correct
when multiple actions fire.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: run_cmd uses exec() which blocks until the child exits. The
direct VLC binary forks its GUI process and exits — exec returns and the
post_script begins. The old post_script polled for VLC focus (up to 10s)
then sent Cmd+F, which fired mid-playback and stopped the video.
Fix 1 — nohup + &: detaches VLC from exec immediately so run_cmd returns
in ~0ms. This decouples the launcher flow from VLC's lifecycle.
Fix 2 — --fullscreen flag: VLC opens fullscreen directly via CLI option.
Eliminates the Cmd+F keystroke that was the proximate cause of the stop.
Fix 3 — > /dev/null 2>&1: silences VLC's verbose logging to prevent
exec's 1MB stdout buffer from overflowing and killing the process.
post_script simplified to a single `tell application "VLC" activate`
to bring VLC to the foreground after the 3s startup delay.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a presenter-accessible "Reset Apps" button to menu_launcher_controls
that is always visible (no edit mode required). Kills presentation apps
(PowerPoint, Keynote, Acrobat, VLC, soffice) — critical recovery path for
presenters stuck on stage with a frozen app.
Also: warning colors on All Files / All Sessions when showing non-default
(hidden) content, and state-aware tooltips on the Display Mode toggle that
describe current state and what pressing will do.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kills the standard conference presentation app set between sessions:
Microsoft PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
- Calls native.kill_processes({ process_name_li }) via existing relay
- Process list overridable per device via event_device.other_json.launcher.kill_process_li
- Button lives in Native OS config > System Actions (edit mode only)
- Reuses system_status for feedback — shows which apps are being killed
- Original list recovered from git history of legacy architecture docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tooltip now shows file size, created/updated timestamps, and open_in_os
setting alongside the existing SHA256 and hosted ID info.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- open_in_os='win' now routes to Windows launch profiles (pptxwin/pptwin/odpwin/pdfwin)
via WIN_EXTENSION_MAP in get_launch_profile() — was silently ignored before
- Display override migrated from non-existent cfg_json backend field to localStorage
($events_loc.launcher.file_display_overrides) — only visible in edit mode; TODO added
for proper backend column when event_file gains cfg_json
- Onsite mode WIN extension rename now covers all 4 types (pptx, ppt, odp, pdf)
instead of only pptx/ppt
- open_in_os button shows LoaderCircle spinner during API call
- Remove cfg_json from properties_to_save (column does not exist on event_file table)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, the button depended on the liveQuery round-trip to show
the new state — invisible on stale IDB caches that predate the cfg_json
properties_to_save fix. Now mutates event_file_obj locally on click so
the button reflects the new state immediately, with the background
refresh as confirmation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
open_in_os = 'win': get_launch_profile() now maps pptx→pptxwin, ppt→pptwin,
odp→odpwin, pdf→pdfwin when open_in_os is 'win', routing to the Windows-variant
launch profiles (Parallels/CrossOver). Was never wired in native mode — feature
was silently lost in the MasterKey→Launcher port.
cfg_json missing from properties_to_save: the per-file display override was
always read as undefined from Dexie because cfg_json was never saved. Added
cfg_json to properties_to_save so display_override and any other cfg fields
persist correctly. NOTE: IDB_CONTENT_VERSIONS for event_file is not yet wired;
existing devices need a manual cache clear to pick up the new field.
Display override button: removed $ae_loc.is_native gate — must be configurable
from any device ahead of the event, not only on the podium Mac.
Display toggle persistence: quick_display_mode now reads from and writes to
$events_loc.launcher.display_mode so the last-set state survives page reloads
instead of always defaulting to 'extend'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All app post-scripts called activate once before the poll loop, then only
checked `frontmost` passively. When Electron retains focus (common when the
app is spawned via run_cmd), macOS focus-stealing prevention blocks the one-
shot activate and VLC/PowerPoint/etc. never come to front. The poll loop
times out and fires the keystroke at Electron instead.
Fix: move activate inside the repeat loop so it re-fires every 0.5s until
macOS yields focus. Also bumped VLC post_delay_ms 1000→2000 and iterations
15→20 for slow conference machines.
Affected profiles: VLC (mac), PowerPoint (mac), LibreOffice (mac+win),
Acrobat (mac+win).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Combine Extend/Mirror into a single toggle button, moved behind edit_mode
- All edit-mode controls (All Files, All Sessions, Display) now share consistent preset-tonal-tertiary styling
- Remove the always-visible display row and its non-native-mode disclaimer
- Wrap Menu_launcher_controls in mt-auto to keep it pinned to the bottom of the sidebar regardless of session count
- Add min-w-20 to file size chip to prevent collapse on narrow sessions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Moved PROJECT__AE_Events_Badges_Config_Cleanup.md to archive.
- Updated PROJECT__Use_AE_API_V3_CRUD_upgrade.md with latest audit and migration status.
- Migrated ae_comp__event_presenter_form_agree.svelte to modern V3 update_ae_obj helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Archived completed May 2026 tasks and streamlined the active list to
focus on upcoming events and the final V3 API surgical cleanup.
- Created TODO__Agents__ARCHIVE_2026-05.md with completed items.
- Streamlined TODO__Agents.md for active show support (CMSC, Axonius).
- Added V3 CRUD migration tracking for core site and utility helpers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Standardized documentation structure for Badges and Leads modules into
focused technical references and practical onsite guides.
- Refined MODULE__AE_Events_Badges.md (Core data integrity & sync logic)
- Renamed MODULE__AE_Events_Exhibitor_Leads.md to MODULE__AE_Events_Leads.md
- Renamed MODULE__AE_Events_Badges_Onsite.md to GUIDE__AE_Events_Badges_Onsite.md
- Expanded GUIDE__AE_Events_Onsite_Runbook.md with Badge and Leads sections.
- Maintained all critical business logic, including the 'Override Fields' pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split the monolithic MODULE__AE_Events_PressMgmt_Launcher.md into focused,
granular modules to improve maintainability and onsite utility.
- Created MODULE__AE_Events_Presentation_Management.md (Back-office focus)
- Created MODULE__AE_Events_Launcher.md (Podium display focus)
- Created GUIDE__AE_Events_Onsite_Runbook.md (SRR and onsite workflows)
- Promoted PROJECT__AE_Events_Launcher_Native_integration.md to
MODULE__AE_Events_Launcher_Native.md (Permanent technical reference)
- Renamed 'Press Mgmt' references to 'Presentation Management' for clarity.
- Removed redundant README.md in launcher route.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Onsite operators can now manually trigger a full pre-sync of all location
materials via a "Force Sync Location" button in the Launcher config.
This ensures podium Macs have all content cached before an event starts.
- Added trigger__force_location_sync to launcher session store.
- Implemented force_location_sync() in background sync engine to perform
recursive metadata fetches for all sessions in a location.
- Optimized download queue with a 4-tier chronological sort:
1. Event/Location assets (Global)
2. Sessions by start time (Earliest first)
3. Presentations by start time (Sequential order)
4. File creation date (First-in fairness for on-time uploads)
- Updated module and native integration documentation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TODO__Agents.md:
- Added the two additional fixes from the review pass to the PATCH/DELETE
retry hardening entry: default timeout 60s→20s, and DELETE missing
ae_auth_error banner on 401/403.
BOOTSTRAP__AI_Agent_Quickstart.md:
- Added mistake #13: breaking the API retry loop by returning errors from
the TypeError/AbortError block instead of throwing them. Documents the
Jan 2026 regression (commit a10accfaa), the three retry classes that must
be preserved, and a quick verification method.
- Filled the gap at item #7 (was missing, causing off-by-one numbering
from item 8 onward). Items renumbered 8-14 → 7-13.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two gaps found during review of the recent retry-hardening commits:
1. api_patch_object.ts and api_delete_object.ts still defaulted to 60s
timeout while GET/POST were lowered to 20s. No callers set an explicit
timeout, so the default was the only value used. With retry_count=5 and
the new backoff policy, 60s per attempt = 5+ minutes worst-case wait.
Lowered to 20s to match GET/POST and keep worst-case under ~2 minutes.
2. api_delete_object.ts had no ae_auth_error import and no session-expired
banner on 401/403. A stale-session DELETE would silently return false
with no user feedback. Added browser + ae_auth_error imports and the
ae_auth_error.set() call matching the pattern in GET/POST/PATCH.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Jan 2026 "offline-first fast-paths" commit (a10accfaa) inadvertently
broke retries for transient network failures (ERR_NETWORK_CHANGED, WiFi
roam events, etc.). The original code's .catch() returned undefined, which
fell through to the `if (!response) throw` path and correctly entered the
retry loop. After a10accfaa, .catch() returned the error as a value, and
the subsequent `instanceof Error` check returned false immediately —
bypassing all retries for the most common failure mode in
hotel/conference environments.
Changes:
- TypeError now throws into the retry loop instead of returning false
- AbortError still returns false immediately (intentional cancel, no retry)
- Per-attempt AbortController: moved inside the loop in both files so each
retry gets its own independent timeout (previously GET retries had no
timeout at all after the first attempt's clearTimeout ran)
- clearTimeout() added to catch block so timer is always cancelled on error
- Exponential backoff added: 2s→4s→6s→8s (capped) between attempts;
rapid retries on a flaky network accomplish nothing without a delay
- Default timeout lowered: 90s → 20s (generous for search/GET but avoids
the 90s worst-case hang that amplified ERR_NETWORK_CHANGED exposure)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"Try Again" resets auto_retry_count but reuses the same localStorage state — if
ae_loc or ae_idaa_loc holds a stale account_id or api_secret_key, every retry
fails identically and the user is stuck in an infinite error loop.
New button clears ae_loc + ae_idaa_loc from localStorage and db_events.event
from IDB, then reloads via the sessionStorage-preserved UUID URL (same logic as
the IDAA layout's Clear Cache & Reload). Forces a fresh FQDN handshake and
re-derives correct auth state. Guidance text shown so users know to try it when
Try Again keeps failing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- macOS: Uses direct binary path (/Applications/VLC.app/Contents/MacOS/VLC)
with --no-play-and-exit --play-and-pause flags and AppleScript fullscreen toggle.
Confirmed working: pause on last frame, stays in window.
- Linux: Uses shell command (vlc --no-play-and-exit --play-and-pause).
Known issue: not going fullscreen, not pausing on end. Deferred for later
investigation of flag interpretation vs launcher execution layer.
- Created separate profile constants: VLC_MIRROR_MAC_PROFILE and
VLC_MIRROR_LINUX_PROFILE in ae_launcher__default_launch_profiles.ts
- Updated TODO__Agents.md with Linux VLC issue for future debugging
Files: ae_launcher__default_launch_profiles.ts, documentation/TODO__Agents.md
- Add Extend/Mirror toggle to Native OS config section (always visible,
no Technical Mode required). Default: Extend. State updates on success.
- Replace .catch(() => {}) swallowing with console.warn logging on both
set_display_layout call sites so failures appear in the Electron console
- Remove old edit-mode-only Extend button (replaced by new toggle)
- Update PROJECT doc: displayplacer install note, binary lookup order, GitHub link
- Clean up TODO__Agents.md: resolve stale items, add new low-priority Electron items
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
set_wallpaper: update signature in PROJECT doc (§5.3 and §7) — actual params
are {path?, url?, url_external?, display?, api_key?, account_id?}, not just {path}.
set_display_layout: clarify in PROJECT doc (§5.3 and §7) — now auto-detects
displays via displayplacer list; configStr is optional manual override, not required.
MODULE profiles table: correct post_delay_ms values (Mac 2000→1000ms,
Win 3000→1500ms) and add polling-loop note explaining the new timing behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ring-1 uses CSS box-shadow which nearly disappears when the UI is scaled
down through VNC. Switched all preset chips and action buttons to border-2
so they remain clearly distinct at reduced resolution.
preset-tonal-surface + border-2 border-surface-400 for unselected chips;
disabled:opacity-60 (vs default 50) on Save & Apply so it is still
identifiable as a button when no URL is entered yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`open -a App` returns immediately; a fixed delay is unreliable for cold starts.
Each post_script now polls every 500 ms (up to 7.5 s) until the target process is
frontmost before firing the automation keystroke. Keynote polls document count
instead of frontmost since start(front document) requires the file to be loaded.
Mac profiles: post_delay_ms 2000 → 1000. Win/Parallels profiles: 3000 → 1500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumps chip buttons h-6→h-8 with text-xs and ring-1 borders so active/inactive
states are clearly distinct at VNC/remote scale. Save & Apply bumped to h-10
text-sm. Fixes: /50 opacity modifier in class: directives (uses class expression
instead), stale svelte-ignore comments replaced with onkeydown handlers, each
block key added. Documents wallpaper repeat-apply macOS caching bug in TODO with
workaround and fix location (Electron temp filename).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces free-text-only inputs with quick-select preset chips (1 primary,
4 client external/projector) that populate the URL field on click.
Consolidates Save/Apply/Save&Apply/Restore into a single Save & Apply
primary button plus an icon-only Restore button. All status messages
merged into one shared state variable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All Access Denied root causes fixed and deployed 2026-05-19. 503 auto-retry
regression also documented and fixed same session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLIENT__IDAA_and_customized_mods.md:
- Verification Flow: describe Aether proxy call, not direct browser-to-Novi fetch
- Replace old fetch() code snippet with new Aether endpoint call
- Update novi_idaa_api_key / novi_api_root_url field descriptions (server-side only now)
- Security notes: key never sent to browser; shape changes go in backend method
- Rate limit note: 12h TTL (was 5-min), add 503 auto-retry behavior
- Fix Redis cache key: idaa:novi_member:{uuid} (account_id was dropped from key)
GUIDE__AE_API_V3_for_Frontend.md §12:
- 503 frontend action: auto-retry once after 3s before api_error
- Mark migration section complete (2026-05-19); update table to show retry behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before: a 503 response from /v3/action/idaa/novi_member/{uuid} hit !response.ok
immediately, set verify_failed_for_uuid, and showed "Verification Unavailable" —
no automatic retry. In the old client-to-Novi flow an unreachable server threw
TypeError (auto-retried); the new server-side path returns a clean HTTP 503
(plain Error), bypassing the is_network_or_timeout branch.
After: 503 gets the same 3s wait + one retry that network/timeout errors get.
Only if the retry also 503s does it fall through to the error UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server-side migration removed the old novi_idaa_api_key check, which was
also acting as an implicit 'is IDAA configured here?' guard. Without it, any
domain that resolves (including ghost/domain-not-found with account_id='ghost')
would fire the Aether endpoint and get an error response, showing 'Verification
Unavailable' over the root layout's 'Domain Not Found' message.
Restore the site_cfg.novi_idaa_api_key presence check as the first guard:
- key absent → site_cfg_json still loading OR this is not an IDAA site → skip
- account_id='ghost' → domain lookup failed → added explicit ghost guard too
The key itself is unused for auth (server holds it); we only test its presence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
verify_novi_uuid() now calls GET /v3/action/idaa/novi_member/{uuid} instead
of fetching Novi directly from the browser. The Aether backend handles the
Novi call server-to-server, eliminating false Access Denied failures caused
by hotel/conference WiFi, VPNs, and Cloudflare IP filtering.
Response parsing simplified — full_name and email are normalized server-side.
Empty-200 anti-pattern handling, email space→+ normalization, and display-name
formatting moved to the backend (confirmed working per API agent).
Retry logic and error classification unchanged (429→rate_limited, network
error→retry once, all others→api_error).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Classify persistent network/timeout failures as 'network_error' (separate from
generic 'api_error') so the UI can show a targeted message
- Add actionable hint for members on hotel WiFi, VPN, or corporate networks:
turn off VPN, switch to cellular, try a different network
- Extend VERIFIED_TTL_MS_DEFAULT from 45 min to 12 hours — covers a full workday
so members at conferences do not need to re-verify mid-day
- Document planned server-side Novi verification FastAPI endpoint in
CLIENT__IDAA_and_customized_mods.md (once implemented, eliminates client-side
Cloudflare/IP-reputation exposure entirely)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 10 fixes applied and verified as of 2026-05-19. Collapsed the three
open issues into the completed checklist with commit references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VERIFY_TIMEOUT_MS 8s → 35s: worst-case auto-retry cycle is 27s (12s abort +
3s wait + 12s abort). At 8s the "Reset & Retry" banner fired while the second
retry was still in flight; members who clicked it cleared their stores and
reloaded mid-attempt, landing on Access Denied. At 35s the escape hatch only
appears if verification is genuinely stuck (slow Novi server or missing api_key).
sessionStorage try-catch: iOS Safari Private Browsing and certain iframe sandbox
configs throw on sessionStorage access. Wrap setItem (onMount) and getItem
(reload_with_uuid) in try-catch so the component degrades gracefully to
location.reload() rather than crashing silently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'origin' is a reserved web term (protocol+domain); the function restores the
initial Novi-provided iframe URL (which includes the ?uuid= param), not the
site root. Renamed to reload_with_uuid to make that clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap Novi API fetch in AbortController with 12s hard timeout — prevents
verify_in_flight from getting stuck if Novi's server hangs with no response
- Auto-retry once (after 3s) on network errors (TypeError: Failed to fetch)
and timeouts (AbortError) — these are almost always transient cellular/WiFi
blips and previously hard-failed with no second chance
- Rate-limit retries (429) already had a 10s wait; network retry is separate
- Update status message to "Connection issue — retrying..." during network retry
- Update error panel hint to suggest closing/reopening the Novi page as last resort
- Update Access Denied hint with same guidance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add reload_to_origin(): saves initial iframe URL (with ?uuid=) to sessionStorage
on mount; all reload buttons use it instead of bare location.reload() so the UUID
is preserved after internal SvelteKit navigation strips it from the URL
- Fix TTL short-circuit to also check $ae_loc permissions — without this, a store
reset (browser restart, stale localStorage) while the TTL was still valid would
skip re-verification and fall straight to Access Denied
- Extend Novi verification TTL from 5 to 25 minutes
- Add Clear Cache & Reload option to the Access Denied state (iframe mode)
- Move Novi UUID debug info on Access Denied page to edit_mode only; UUID line
at bottom of auth'd pages stays always visible for troubleshooting
- Remove expired temporary tech-notice variables (template block was already commented out)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.html: comment out 3 Google Fonts <link> tags (Noto Sans, Noto Serif,
Roboto Mono) — no theme or component applies these families; all themes use
system-ui. Saves 3 blocking network requests on every page load.
- app.html: add subtle CSS-only #ae_loader spinner (1.75rem ring, pointer-events:none)
that shows during JS download + root load function, before Svelte mounts.
- +layout.svelte: add onMount to remove #ae_loader as soon as Svelte bootstraps;
the existing is_hydrating frosted-glass overlay takes over from there.
- app.css: comment out orphaned Quicksand @font-face — font-family was never
applied to any element so browser never fetched it anyway.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Access Gate: document new verify_error_type states (rate_limited/api_error),
retry/reset UI buttons added in the previous session
- Search Architecture: correct 'contacts not searchable' — default_qry_str already
includes contact data; two bugs fixed 2026-05-19 (stale STORED GENERATED columns,
frontend secondary filter dropping API-matched results). IDB fast-path gap remains.
- TODO__Agents.md: update contact search task to reflect API path now working;
narrow remaining work to IDB fast-path only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The secondary client-side filter was re-checking qry_str against name, description,
location_text, and default_qry_str on the API response. This silently dropped meetings
that the API correctly matched via default_qry_str (a backend-generated combined index
containing contact name/email) — because that field is not always present in the
response body.
The API's LIKE search on default_qry_str is already exact. The secondary filter should
only correct structured dimensions (type, physical/virtual OR-logic) where the backend
uses AND logic that the frontend must compensate for. Text search is left to the API.
Root cause confirmed: STORED GENERATED columns in the event table needed a rebuild;
frontend filtering was masking results that the API returned correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
element_data_store fires its load trigger as soon as api_ready is true,
with no check for account_id. In the IDAA iframe flow, the outer layout
mounts before Novi UUID verification completes, so the footer fetch fires
with no x-account-id header and gets a 403.
Wrap the IDAA outer layout footer in {#if $ae_loc.account_id} so it only
loads once the member's identity is established. Apply the same guard to
the events layout header and footer for consistency.
Journals was already safe (data stores are inside the trusted_access gate).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Distinguish transient API failures (rate limits, server errors, network drops) from
real membership denials, so members see actionable messages instead of 'Access Denied.'
Layout: new verify_error_type state ('rate_limited' | 'api_error') surfaces a
yellow 'Identity Verification Unavailable' banner with three recovery options --
Try Again (no reload, clears latch), Clear Cache and Reload, and Full Reset.
Spinner now shows live status messages (e.g. 'High traffic - retrying in 10 seconds...').
Recovery meetings page: qry_error_detail distinguishes network drops (TypeError /
ERR_NETWORK_CHANGED) from server errors, showing specific guidance in the error UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The RE-SORT block after API revalidation only checked for 'name' (a legacy
sort mode removed when sort_modes was introduced). 'name_asc' and 'name_desc'
fell through to the else branch and were silently re-sorted chronologically
by tmp_sort_1, overriding the user's selection. Updated to match the fast-path
IDB sort logic which already handled all three modes correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bootstrap v3 (.btn) sets border:1px solid transparent which overrides
Skeleton/Tailwind preset-outlined border classes when loaded last in the
Novi iframe. Replacing the dead border-color comments with a box-shadow
ring — Bootstrap does not reset box-shadow on .btn so it survives without
!important. Adds a darker ring + faint bg tint on hover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update default qry__limit to 100 in idaa_loc
- Add 75 to limit_steps in recovery meetings query component
- Bump AE_IDAA_LOC_VERSION to 2 to apply changes to existing users
- Update IDAA documentation and TODO__Agents.md with SQL optimization task
- Mark implemented UI/UX ideas as done in documentation
Wrap the data store element in an accordion-style toggle. State persists
in idaa_loc (localStorage) so the user's preference survives page reloads.
Added ds_info_collapsed field to idaa_local_data_struct.recovery_meetings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace three sort chips with single cycling button (Last Updated → Name A→Z → Name Z→A → repeat)
- Add min-w-36 to sort button to prevent layout bounce between labels
- Move My Meetings chip to filter row (first position) alongside Virtual/In-Person
- Widen filter chips row from max-w-xl to max-w-2xl to match search bar
- Move sort/max/actions section inside <form> so all rows share the same width constraint
- Flatten span wrappers in bottom row — all buttons are direct flex children of justify-center container, fixing left-drift when conditional buttons are hidden
- Add keyed {#each (val)} to Type chip loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Group labels (Location, Type, Sort, Max) moved to sr-only — visually hidden,
accessible to screen readers. Chips are self-descriptive without them.
- Max results chips replaced with a [−] 150 [+] stepper that steps through
predefined values [25, 50, 100, 150] (+ 200/500 for trusted_access). Minus/plus
buttons disable at the ends of the list. limit_steps and limit_idx computed as
$derived in script so onclick closures have access without @const gymnastics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Max: 25 / 50 / 100 / 150 chips for all users; 200 / 500 visible to trusted_access
staff only (consistent with previous select behavior).
- Sort: Last Updated / Name A-Z / Name Z-A chips; clicking triggers the same
qry__order_by_li update and search_version bump as the old select onchange.
- Sort logic extracted into set_sort_mode() helper to keep onclick clean.
- Active state: preset-filled-tertiary-400-600; inactive: outlined + opacity-60.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ae_idaa_comp__event_obj_qry.svelte: replace Location checkboxes and Type radio
inputs with styled pill-chip buttons. Location chips (Virtual / In-Person) are
independent toggles; Type chips (All / IDAA / Caduceus / Family Recovery) are
mutually exclusive — clicking the active chip deselects back to All. Chips fire
the reactive search $effect directly via store updates; no explicit trigger needed.
Remote First dev toggle preserved in edit mode, now inline with filter chips.
- CLIENT__IDAA_and_customized_mods.md: update Recovery Meetings filter/sort docs,
add My Meetings / favorites section, correct idaa_loc and idaa_sess store schemas,
bump Last Verified date.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- +page.svelte: when search returns zero results and filters are active, show
"No meetings found for these filters" with a one-click "Clear all filters" button
instead of the bare no-results message. The 8s cache-reset escape hatch is
unchanged and still fires only when zero results appear with no filters set.
- [event_id]/+page.svelte: add star/favorites button to the detail page nav bar
alongside Back/Edit. Loads the same idaa_meetings_favorites data_store record
on mount; PATCHes the shared record on toggle. State is optimistic with rollback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Favorites are now stored in a dedicated data_store record (code:
idaa_meetings_favorites, scoped to the IDAA account_id) so toggling
never touches ae_event rows or their updated_on timestamps. Structure:
{ [novi_uuid]: [event_id, ...] }. Pre-created DB records for dev (id 150)
and live IDAA (id 151) accounts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store Novi UUID list in event.mod_meetings_json.favorite so favorites
persist cross-browser without requiring Novi API write access. Optimistic
IDB update with API rollback on failure. Star button uses inline styles
to override Bootstrap v3 iframe CSS conflicts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: stale IDB records from prior deploys persisted indefinitely.
Fast path returned 0 (account_id mismatch), API errored silently, and the
error state showed the same message as a genuinely empty result — making
the failure indistinguishable from real data.
Fix is layered defense:
- Bump IDB_CONTENT_VERSIONS.events.event to 2 (one-time force-clear for all users)
- Add check_and_clear_idb_table() helper to store_versions.ts; wire it in
(idaa)/+layout.svelte to catch future version mismatches on session start
- One silent auto-retry (3s) on API failure before surfacing error UI
- Distinct error state (Unable to load meetings) separate from empty state
- Escape-hatch cache-reset button after 8s when zero results + no active filters
- Document root cause and fix in README.md and BOOTSTRAP__AI_Agent_Quickstart.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two layout fixes for the badge_4x6_fanfold layout (no background_image_path):
1. badge_header max-height: 1.5in — the Axonius logo (624×232px) renders at
~1.49in tall at full badge width. The inherited max-h-[1.00in] was cropping
the bottom half of the image.
2. badge_body margin-top: 0 — overrides the component-level mt-54 (≈2.25in).
That margin was needed for the PVC layout where a full-bleed background image
covered the badge and body text needed to start in the image's designated zone.
For fanfold badges with a standalone header_path, mt-54 created a 2.25in blank
gap between the header and the attendee name.
Also updates default fit_heights for badge_4x6_fanfold to match the 4.0in body
height (was sized for 4.5in before the header zone was properly accounted for).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces badge_layout_epson_4x6_fanfold.css (layout code badge_4x6_fanfold)
for the Axonius Adapt 2026 June show. Wires @page size to 4in×6in in the print
page and cleans up the stale 4in×12in default. Imports new CSS in badge component.
Measured stock: 4in × 6in portrait with 5/8in lanyard hole 1/4in from top.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three redundant store fields encoding the same AM/PM choice replaced with a single
`use_12h: boolean` in PresMgmtLocState. iso_datetime_formatter gains a third param
(use_12h: boolean | null = null) that auto-resolves 24h↔12h format name variants via
a symmetric FORMAT_PAIRS lookup — null default leaves all ~100 existing call sites intact.
Toggle surfaces in three places: Clock icon in session time chip (hidden button, same
visual), event Options modal Display section, and session Options modal Display section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$idaa_trig is a key_val object (dict of boolean flags), not a string signal.
Adds update_zoom_full_url key to the store template and converts all 7
string comparisons/assignments to property access on the object.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, searching by presenter name in pres_mgmt returned no results
because event_presenter_li_qry_str / event_presentation_li_qry_str were
never requested or stored.
Changes:
- ae_types.ts: add event_presentation_li_qry_str + event_presenter_li_qry_str
to ae_EventSession interface
- db_events.ts: same two fields added to Session Dexie interface
- ae_events__event_session.ts:
* add ft_presentation_search_qry_str param
* auto-upgrade view to 'alt' when either ft_presenter or ft_presentation
search is used (backend requires v_event_session_w_file_count for these)
* add both fields to properties_to_save so they persist to Dexie cache
- +page.svelte (pres_mgmt):
* pass ft_presenter_search_qry_str + ft_presentation_search_qry_str in API call
* local IDB fast path now checks both new fields
* client-side filter guard also checks both new fields
Two Svelte-side bugs causing account_name to always show 'Account Name Not Set':
1. ae_core__site.ts: background site_domain refresh only pushed cfg_json back
into $ae_loc after a stale cache hit. Now also pushes account_name and
account_code when the store holds a placeholder value.
2. +layout.ts: duplicate ae_loc_init['account_name'] assignment at line ~475
was overwriting the correct one at line ~385 with a different fallback string
('Account Name Not Set' vs 'Ghost Account'). Removed the duplicate.
Also includes user-intentional changes during testing:
- events/+page.svelte: typo fix ('You access' -> 'Your access'); Pres Mgmt /
Launcher / Badges / Leads buttons now gated on trusted_access && edit_mode
- events/+page.ts: event list limit 25 -> 7
- events/[event_id]/+page.svelte: user edit
Consistent with the tech help panel update — all Clear Storage / Clear & Reload
buttons now enumerate IDB databases dynamically so new modules are included
automatically without needing to update these lists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reworks Clear & Reload to use indexedDB.databases() (dynamic enumeration)
instead of a hardcoded DB list. Edit mode clears localStorage/sessionStorage
while preserving ae_loc (sign-in + permissions). Normal mode clears IDB only.
Adds new Full Reset button — wipes all IDB, localStorage, and sessionStorage
for the origin with no preservation. Useful for diagnosing quota/storage issues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds check_and_clear_idb_tables() helper in core__idb_dexie.ts that clears
Dexie tables when their content version changes. Version numbers live in
IDB_CONTENT_VERSIONS (store_versions.ts); state tracked in localStorage
(ae_idb_ver__{module}__{table}) so each table clears exactly once per bump.
Wired into db_journals.ts (pilot): journal_entry at v3 clears stale
content_md_html/history_md_html data cached before the properties_to_save fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the content_md_html/history_md_html fix on journal_entry.
description_md_html is computed from description via marked.parse() on
every background refresh and does not need to be persisted to IDB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QuotaExceededError on db_save was propagating through .then() into the
outer .catch(), which returned undefined and discarded the successfully-
fetched API data. Wrapped all db_save calls in try/catch so IDB failures
log a warning but never kill the load/create/update return value.
Also removed content_md_html and history_md_html from properties_to_save
— these are computed from content/history on every background refresh via
marked.parse(), so storing the rendered HTML alongside the source was
doubling storage cost per entry and the primary cause of quota exhaustion.
Affects: load, list, create, update, and qry functions in both journal
and journal_entry modules.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Running gsettings on the dev workstation resets monitors on every test cycle.
Linux Electron handler now returns linux_test_mode:true with would_run details
instead of applying. Svelte cfg component shows a debug popup (mirrors Native
Test Mode style). Background sync logs to console and leaves applied-URL unset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- electron_relay: add restore_macos_default_wallpaper() — uses run_osascript
to find first .heic in /System/Library/Desktop Pictures/ (version-agnostic)
- wallpaper cfg: Restore macOS Default button (native or edit_mode); clears
applied-URL tracking so next configured URL re-applies correctly
- wallpaper cfg: fix Apply Now / Save & Apply enabled when only external URL
is filled; display target becomes 'external' to leave built-in unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stores wallpaper URL(s) in event_device.other_json.launcher.wallpaper and
auto-applies on all native Launcher instances within one heartbeat cycle (~60s),
eliminating manual per-Mac setup at events.
- electron_relay: typed set_wallpaper wrapper (url, url_external, display, auth headers)
- launcher_defaults: wallpaper_applied_url tracking + section_state__wallpaper
- launcher_cfg_wallpaper: new config section — save to device config + apply now
- launcher_cfg: add wallpaper section to device tab
- launcher_background_sync: auto-apply if config URL changed since last apply;
external-only config targets only the secondary display, leaving built-in unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1 (stale error_detail):
open_file_error_detail is now cleared at the start of every mode's
branch (native, onsite, default) alongside open_file_clicked=true.
Previously it was cleared mid-way through the native flow (Step 1),
so a stale error from a previous failed run could flash briefly.
Fix 2 (stuck 'Opening...' during post_script sleep):
After the Step 4 open call returns (run_cmd or open_local_file_v2),
update the status message immediately to 'App opened — running setup...'
so the button doesn't appear frozen for the full post_delay_ms wait.
Fix 3 (safety valve for hanging native calls):
A 60s setTimeout is registered at the start of the native branch.
If any native IPC call hangs indefinitely and the existing per-path
reset timeouts never fire, this force-resets open_file_clicked after
60s. All normal paths complete within ~8s so this only triggers on
true hangs. ERR_NETWORK_CHANGED cannot re-enter the download path
because the open_file_clicked guard blocks re-entry.
shell.openPath always resolves on the Electron side, but if the Svelte
renderer navigates before the IPC reply arrives, the promise rejects
with 'reply was never sent'. The file is already open at that point.
Catch the rejection and treat it as success on both call sites (Step 4
primary path and Step 7 fallback). This eliminates the unhandled
promise error without masking real failures.
URL files: event_file.filename = 'https://...' is now a first-class
file type in the launcher.
- is_url derived rune detects https/http filename prefix
- URL branch in handle_open_file() runs before cache/native branches
(no download, no temp copy, no hash)
- Offline guard: warns and blocks click if navigator.onLine is false;
online/offline listeners registered only for URL rows (no-op on files)
- Native mode: opens via native.open_external({ url, app: 'chrome' })
with silent fallback to default browser
- Browser mode: window.open() with noopener
- display_mode default: 'mirror' (URLs typically just need the screen
mirrored, not extended presenter view)
- Button badge shows Link2 icon + WifiOff warning when offline
- Button text uses event_file.title as label (falls back to URL)
- Test mode popup: skips Steps 1-2 (N/A), shows open_external call
DEFAULT_LAUNCH_PROFILES: add 'url' key (display_mode: 'mirror')
Electron TODO: added set_display_layout / displayplacer per-device
config task to aether_app_native_electron/documentation/TODO_AGENTS.md
with full contract details and resources
The sleep step was running unconditionally, meaning files that open
with the 'default' catch-all profile (zip, unknown ext, etc.) would
wait 2 seconds for no reason — there's no AppleScript to prepare for.
Gate the sleep inside the post_script check so it only delays when
there's actually automation to run after app launch.
Also update the test mode popup to show '0ms — skipped (no post_script)'
and display post_delay_ms as '(default: 2000ms)' when unset.
Enables testing the LaunchProfile system from any device (no Mac/Electron
needed). When active, the Open button simulates the full native flow and
shows a debug popup with everything that WOULD be sent to Electron.
- ae_events_stores__launcher_defaults.ts: add native_test_mode boolean
(persisted, default false) to LauncherLocState and launcher_loc_defaults.
- launcher_cfg_app_modes.svelte: add Native Test Mode checkbox toggle in
the Advanced Toggles (Edit Mode Only) section with active-state warning.
- launcher_file_cont.svelte:
- Add test_mode_popup_open/test_mode_popup_data state vars.
- Add branch 0 in handle_open_file(): when native_test_mode + app_mode=native,
skip all Electron calls; resolve the real LaunchProfile, build a data
snapshot, open the debug popup.
- Debug popup shows: file info, simulated temp path, cache/copy pass,
resolved LaunchProfile fields, set_display_layout call, open command
(run_cmd or open_local_file_v2 fallback), sleep delay, post-script
(AppleScript or shell: prefix). Click backdrop or Close to dismiss.
- Add ae_launcher__default_launch_profiles.ts with LaunchProfile interface,
DEFAULT_LAUNCH_PROFILES constant, and resolve_launch_profile() helper.
Covers pptx/ppt/key/odp/pdf, all VLC media formats, Windows/Parallels
variants (pptxwin/pptwin/odpwin/pdfwin), and a catch-all 'default'.
- Replace get_launch_script_template() with get_launch_profile() in
launcher_file_cont.svelte. Override priority: device API config >
local persistent config > built-in defaults > 'default' catch-all.
- Rewrite handle_open_file() native branch with 9-step profile-driven flow:
copy_from_cache_to_temp → resolve profile → set_display_layout (silent fail)
→ open (run_cmd or OS default) → sleep(post_delay_ms) → run post_script
→ fallback to OS default on open failure → surface status/error detail.
- Add open_file_error_detail state var; show error pre block in status
alert for native error state, show fallback note for fallback state.
- Add display override toggle button in event_file_meta (visible when
trusted_access + is_native): cycles null → extend → mirror → null,
PATCHes event_file.cfg_json.display_override via V3 CRUD.
- TODO__Agents: expand CMSC Charlotte launcher task into done/remaining
sections; enumerate Svelte-side migration items (default templates,
composable open flow, error fallback, slide control config, kill list
config, Launcher config UI editor, end-to-end Mac test gate)
- PROJECT__AE_Events_Launcher_Native_integration: update Last Updated to 2026-05-11
- Bootstrap lifecycle now reflects actual two-step flow (event_device → site_domain/search).
Removes stale /v3/data_store/code reference and corrects the JWT claim — auth is
x-aether-api-key + x-account-id throughout; no JWT is issued or used.
- check_hash_file_cache corrected to check_cache (actual method name in preload bridge).
- cleanup_tmp_files removed from relay list — stale .tmp cleanup is handled inline inside
download_to_cache, not via a standalone IPC channel.
- Known Issue section replaced: all AppleScript handlers are now hardened (launch_presentation
converted to temp-.scpt approach in the companion Electron commit ca4fddd).
- Markdown lint fixes: blank lines around tables, table pipe spacing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop the _random key copy loop from _process_generic_props in both files —
V3 API returns {obj_type}_id directly as the random string ID, so copying
from {obj_type}_id_random is a no-op. Simplify .id assignment to use
baseIdKey only. Remove commented-out _random entries from properties_to_save.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix download button to use hosted_file_id instead of id (which resolved
to event_file_id via _process_generic_props, hitting the wrong endpoint)
- Fix Dexie file table query in event_file_obj_tbl_wrapper to use _id
fields (the indexed ones) instead of _id_random variants
- Remove _random fields from properties_to_save in event, event_session
- Drop _id_random fallbacks from launcher device ID resolution and
background sync heartbeat
- Clean up dead comments and old FA anchor in post edit component
- Update TODO__Agents.md: BGH section removed, CMSC/Axonius shows added,
download button fix marked complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hide AI tools panel when entry is not in edit mode
- Clicking AI button when a summary already exists opens it directly
instead of triggering a new API call; Re-run still available in modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add hover titles to all save buttons
- Match warning color scheme across floating, inline, and header save buttons
- Fix floating save button visibility (Tailwind v4 hidden/md:inline-flex conflict)
- Hide floating save when no unsaved changes using {#if}
- Hide Config button when not in admin edit mode
- Remove the mobile/backup explicit Save button from header (redundant)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The auto-save $effect wrapped has_unsaved_changes in untrack(), which meant it
was never tracked when save_status was 'saved' (JS short-circuit in the else-if
branch). After every successful save the effect lost its reactive dependency on
user edits and never re-fired until something else changed save_status first.
Fix: track tmp_entry_obj.content and tmp_entry_obj.name directly (void reads)
so the effect re-runs on every keystroke and the debounce timer resets correctly
(fires 2 s after the last change, not the first). has_unsaved_changes is also
tracked directly so the status indicator resets cleanly when changes are cleared.
All side-effects remain in untrack() to prevent reactive loops.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entry: 50 chars for entry name, 30 for journal name
Journal: 60 chars for journal name
Appends ellipsis (…) when truncated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire indentWithTab into keymap (Tab=indent, Shift-Tab=dedent, 4 spaces)
- Set indentUnit to 4 spaces
- Wrap lineNumbers() in a Compartment for live toggle without editor rebuild
- Add Hash toolbar button to toggle line numbers; respects show_line_numbers prop as initial value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chromium's native audio player shadow DOM collapses when max-height is
applied to the <audio> element — audio plays but controls are invisible.
Remove the inline style (carried over from video context) and use w-full
max-w-lg instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Expose archive_content.code in edit form (trusted + edit_mode only)
- Add code to properties_to_save so it persists on every API load/save
- Add code field + index to Archive_Content Dexie interface (schema v2)
- Minor: center "Add" button rows in archive and content list components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
XHR upload % in the button + disabled states now communicate
upload/save progress; the top Saving.../Finished saving block
is no longer needed (and its out:fade was broken on re-entry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The outer bordered div was always in the DOM; only the label and input
inside were hidden during upload, leaving a visible empty dashed box.
Apply the same hidden guard to the container div.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass track_progress: true to post_object so the XHR path fires live
percent_completed events, making the upload % display functional.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New track_progress param (default false) switches to XMLHttpRequest for
form_data uploads so xhr.upload.onprogress can fire percent_completed
postMessages into api_upload_kv. fetch() has no upload progress events.
No retry loop on XHR path — silently retrying a large video upload is
bad UX; caller re-submits on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restrict upload to one file (each archive content item maps to one file);
contextual toggle button text (Switch to Select / Switch to Upload);
swap FontAwesome upload icon for Lucide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guard task_id effect against resetting mid-upload; add prevent_default
wrapper; add 20-min timeout for large video/audio files; add null result
guard before result[0]; fix for= attribute to use variable; console.error
on failure; remove dead params/comments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
archive_obj.archive_id_random → .archive_id in load function and post-create
assignment; remove archive_id_random and hosted_file_id_random from editable
fields list — V3 returns the random string as the primary ID field directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ArchiveContentForm interface + factory for controlled input bindings
- obj_changed bindable prop wired to Cancel button visibility in parent page
- Split Save button: edit mode disables when clean, create mode always enabled
- Post-upload/select/remove syncs orig snapshot so file ops do not dirty the form
- Fix archive_content_id_random / archive_id_random → V3 field names in edit component
- Add missing file_extension field to ae_ArchiveContent type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Svelte 4 store nested property mutations don't call set()/update(), so
$effect on $idaa_slct never fired after upload. Replaced store property
binds with local $state variables that Svelte 5 proxies track natively.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use Jitsi url_params.uuid for exclusion where available, preserve url_params in cached activity logs, and add the temporary staff-name fallback behind the same edit-mode toggle.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Novi UUID exclusion and known-meeting filtering, default the report date range to the last 60 days, and hide Room Name unless global edit mode is enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the bootstrap quickstart focused on general platform knowledge, while preserving the Jitsi Reports reminder in the project docs and todo list.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Polish the Journal Entry Config modal to match the desired section outline, hide alert messaging unless enabled, update the shared draft typing for entry flows, and replace deprecated privacy icons.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the site domain resolves to ghost (not found or missing access key),
$ae_loc.site_access_code_kv is undefined, causing a TypeError on .super.length.
Add early return if kv is absent and use optional chaining on each access
level so the function gracefully returns "no match" on unregistered domains.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TODO__Agents.md: mark IDAA IDB caching item complete (audited 2026-04-28);
all protection layers confirmed in place, no code changes needed
- GUIDE__SvelteKit2_Svelte5_DexieJS.md: add "SvelteKit Layout Hierarchy:
Security and Execution Order" section explaining execution order, auth-gate
consequences, pre-gate risks in +page.ts/+layout.ts, and the reactivity-guard
vs auth-guard distinction for IDAA $effect blocks
- BOOTSTRAP__AI_Agent_Quickstart.md: add Mistake #7 — treating $effect blocks
as auth bypass risks vs understanding the real layout hierarchy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The key gate was disabled 2026-04-01 after a page-refresh lockout bug.
Root cause: +layout.ts unconditionally wrote ae_loc_init['allow_access'],
which the +layout.svelte merge spread clobbered the persisted key string
on every navigation/refresh without ?key= in the URL, causing the gate
comparison to fail and showing "Access Denied".
Fix: only write allow_access to ae_loc_init when access_key is present
in the URL. On refresh/navigation without the key param, the persisted
value survives the spread unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Defensive fallback for root_url: $ae_loc.base_url || window.location.origin
so the backend email builder always gets a valid URL (guide warns that a null
root_url produces a broken magic link "None?user_id=...")
- handle_lookup_user_email: drop stale array-response branch; use user_id (V3
primary field) instead of user_id_random (legacy alias, same value)
- handle_change_password: same cleanup — user_id preferred over user_id_random,
dead array-response else-if removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Legacy GET /user/authenticate and GET /user/lookup_email were returning 404
because the backend has removed those routes. Updated all 5 auth functions in
ae_core__user.ts to use V3 equivalents:
- auth_ae_obj__username_password: GET /user/authenticate → POST /v3/action/user/authenticate (body)
- auth_ae_obj__user_id_user_auth_key: GET /user/authenticate → POST /v3/action/user/authenticate (body)
- send_email_auth_ae_obj__user_id: GET /user/{id}/email_auth_key_url → GET /v3/action/user/{id}/email_auth_key_url
- qry_ae_obj_li__user_email: GET /user/lookup_email → POST /v3/crud/user/search
- auth_ae_obj__user_id_change_password: PATCH /user/{id}/change_password → POST /v3/action/user/{id}/change_password
Credentials are now in the POST body (not query params) for authenticate calls.
Updated two call sites in e_app_sign_in_out.svelte to drop removed null_account_id param.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filenames like .PPT or .Ppt bypassed the extension checks entirely because the
comparison was case-sensitive. Lowercasing guessed_extension at the point of
computation fixes this for all checks (legacy, untrusted, block_upload).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trusted-access users (Pres Mgmt admins) were getting file_list_status='ready'
when selecting .ppt/.doc/.xls files, so the prominent warning banner never
rendered — only the small per-row warning in the file table was visible.
- element_input_files_tbl: introduce 'warn_legacy' status for trusted users;
show a yellow warning banner (vs red blocked banner for non-trusted users)
- ae_comp__event_files_upload: change button disabled check from != 'ready'
to === 'blocked_legacy' so 'warn_legacy' does not accidentally block upload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bulkPut only upserts — files deleted on the server stayed in Dexie forever,
showing in the Launcher and Manage Files UI until the browser cache was cleared.
After each _refresh_file_li_background call, deleted records are now pruned
from Dexie. Scope-guarded so we only remove records that would have appeared
in the query (e.g. hidden files are not pruned after a hidden='not_hidden' fetch).
Also covers the disable (enable=false) case the same way.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refresh_location_config() was missing inc_file_li:true, so location-level
files were never fetched from the API and lq__location_event_file_obj_li
always returned empty from Dexie. Files only appeared when Pres Mgmt had
previously loaded them on the same device.
Also added a reactive $effect so files load immediately when the operator
switches rooms, rather than waiting up to 60s for the next timer tick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both found during 2026-04-22 late-night review of Manage Files upload/download flow.
Downloads confirmed working despite wrong ID (backend silently accepts event_file_id
at hosted_file endpoint). Needs proper fix before backend tightens validation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Background file loads for session, presentation, and presenter were using the
default hidden='not_hidden', so hidden files never reached Dexie. The Manage
Files liveQuery reads straight from Dexie, making hidden files completely
invisible until the Refresh button was clicked (which already used hidden='all').
The Launcher is unaffected — it has always had a render-time guard that hides
files with event_file_obj.hide unless show_content__hidden_files is enabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set post_object timeout to 1200000ms (20 min) for hosted file uploads;
the 90s default was killing large presentation file uploads
- Guard result[0] access in .then() to prevent crash when upload
times out or is aborted (TypeError: can't access property "hosted_file_id")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On session click/hover, the menu was calling load_ae_obj_id__event_session
directly AND then navigating via goto(), which re-runs +page.ts and calls
it again. Both fired concurrently on cold cache, causing two identical API
requests for the same session.
Fix: remove the direct load call entirely. The goto() promise is assigned
to ae_promises.slct__event_session_id so the existing #await spinner still
works — it now reflects actual navigation + page.ts load time rather than
a redundant parallel fetch. Remove events_func and ae_api imports (unused).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- menu_session_list: move class:hidden to <li> so fixed-height rows fully collapse
- launcher/+layout.svelte: sort sessions by start_datetime (ascending) instead of name
- Rename hide_content__draft_files → show_content__internal_files (default false);
remove redundant show_content__draft_files; rename prop hide_draft →
show_internal_purpose_files in launcher_file_cont; update all 7 call sites and
the menu_launcher_controls toggle. Now hides admin/draft/outline purpose files
by default with consistent naming across the flag, prop, and toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes a gap where $ae_loc could be reset externally (sign-out) while
$idaa_loc retained novi_verified within TTL, causing Case 2 to return
early and skip the IDB purge even though the render gate shows Access Denied.
Now Case 2 only preserves the session when $ae_loc also reflects active auth;
inconsistent state falls through to Case 1 (purge).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SvelteKit load functions fire during link prefetch before Novi auth completes;
`if (browser)` guards do not prevent this. Moving all IDAA data fetching into
$effect hooks gated on `novi_verified || trusted_access` closes the IDB
pre-population race across archives, bb/[post_id], and recovery_meetings/[event_id].
Also documents the Auth-Before-Cache rule and per-route status in
AE__Permissions_and_Security.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+layout.ts was firing on SvelteKit link prefetch, writing events to IDB
before Novi auth ran. Stripped to thin shell; the existing search $effect
in +page.svelte already handles SWR load+revalidation — just needed an
auth gate (novi_verified || trusted_access) at the top.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Case 1 purge in the layout was firing for manager/trusted users (no UUID),
causing a loop: db_events.event cleared → liveQuery updates → refetch →
store write → Effect 2 re-runs → clear again.
BB $effect was also blocking managers since novi_verified is always false
for non-Novi auth paths.
Both now check trusted_access before gating/purging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+page.ts runs before layout effects and fires on SvelteKit link prefetch,
causing private IDAA posts to be written to IDB before Novi auth runs.
Moving to $effect gated on novi_verified eliminates the race entirely —
$effect only runs post-mount, after the layout has verified the user.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, +page.ts fires the API call before +layout.svelte
effects run, causing posts to be written to IDB after the purge.
Anonymous users (novi_verified=false) now return early with no fetch.
Cached verified sessions (within TTL) continue to load normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three distinct log messages for each trigger:
- No UUID / no session path
- Novi auth failure (catch block)
- Reset & Retry button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous purge only fired inside verify_novi_uuid() catch,
which requires a UUID in the URL. Unauthenticated visits without
a UUID (Case 1 in Effect 2) now also clear posts, comments,
archives, and events from IDB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends the IDB purge from the previous commit to include
db_events.event — covers cached IDAA recovery meeting records.
No module overlap in current client deployments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Novi UUID verification fails (or the manual Reset & Retry is
triggered), clear db_posts.post, db_posts.comment, db_archives.archive,
and db_archives.content from IndexedDB. Prevents private IDAA data
from persisting in the browser after a session ends or auth is denied.
db_events.event intentionally excluded — shared with conference modules.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TODO: mark BGH file-warning and hide-draft items complete; add detailed
Dockerfile env-file simplification task (deferred post-April 21 show);
strip stale completed DevOps entries from the active list
- package.json: remove build:docker:test/prod (never used locally; deploys
go through remote deploy.sh on Linode)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- element_input_files_tbl: only block upload for non-trusted users; trusted_access
users see the same warnings but can still proceed
- element_input_files_tbl: improved warning message wording for .ppt and .doc
- element_manage_event_file_li: minor tweaks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set file_list_status to 'blocked_legacy' when any selected file is .ppt or .doc,
disabling the Upload button until the file is removed
- Show a red banner at the top when upload is blocked
- Add a per-file warning message row in the file table for all legacy/untrusted
extensions (previously computed but never rendered — only a pink cell highlight)
- Red styling for blocking extensions (.ppt/.doc), yellow for warn-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- launcher_file_cont: add 'admin' file_purpose to hide_draft filter (alongside outline/draft)
- element_manage_event_file_li: remove event_file_id from data_kv passed to update_ae_obj;
it was being sent in the PATCH body causing 'Unknown column event_file_id in SET' (400)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add +page.ts to trigger load_ae_obj_li__event_location on page load (locations
were never fetched without a manual trigger)
- Fix ae_comp__event_session_obj_li_wrapper: query used event_location_id_random
(deprecated index) instead of event_location_id, causing empty session lists
under each location
- Wire hide__session_poc to pres_mgmt_loc.current.show__session_li_poc_field so
the Options toggle actually takes effect in the per-location session list
- Also set hide__session_location=true since location is implicit in that context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On a fresh Electron install the events_loc persisted store has no
app_mode value set, causing the native file launch path to fall through
to a browser save dialog. Auto-initialise app_mode='native' in the
launcher layout when is_native is detected so all three modes (default,
onsite, native) continue to work correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract session search form into ae_comp__pres_mgmt_session_search.svelte
(parallels ae_comp__badge_search.svelte); removes ~145 lines from +page.svelte
- Add time window filter: Clock icon toggle button reveals compact before/after
selects; trusted users get 3d/7d options; active state highlighted in amber
- Add passes_hide_filter to IDB fast path to mirror API qry_hidden logic and
eliminate the hidden-session blink on revalidation
- Add passes_time_window applied to both IDB fast path and API results
- Add time window state fields to PresMgmtLocState + pres_mgmt_loc_defaults
- Add contextual warning in "No sessions found" when time filter is active
- badges: hide "Start Here" button for trusted_access users; tweak button shade
- badges: scope placeholder CSS fix to input only (not textarea)
- Add MODULE__AE_Events_PressMgmt_Launcher.md doc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old guard locked on badge ID after the first liveQuery tick. If
Dexie had a cached badge without cfg_json.font_sizes, the guard fired
with no sizes to apply, then blocked the SWR background refresh that
delivered the real saved sizes. Result: font sizes appeared unsaved on
any browser that had visited the badge before sizes were set.
Fix: track the cfg_json string last applied (_font_sizes_applied_cfg)
instead of just the badge ID. Re-applies whenever cfg_json changes on
a background refresh, but skips if local sizes have drifted from the
last apply (user is mid-adjustment — auto-save will sync shortly).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tailwind v4 renders placeholder text too dark on light backgrounds,
making it indistinguishable from real input values. Same scoped CSS
fix already applied to ae_comp__badge_print_controls.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
badge_type_code_li was returning [] when the template had no badge_type_list,
causing the Badge Type field to be hidden entirely in the Staff section.
Add same fallback default as create form and search filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was at the bottom after print position controls — now first item in
Staff Adjustments, before Hide/Unhide Badge, where staff expect it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was: visible to everyone pre-print, Trusted+Edit for reprints.
Now: Administrator + Edit Mode only (all three locations).
Temporarily restricted for Axonius 2026 — restore broader access after event.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- default_badge_type_code_li now matches ae_comp__badge_search.svelte list
(attendee, sponsor, staff, guest, volunteer, member, nonmember, test)
- badge_type_code initializes to 'attendee' (In-Person Attendee)
- $effect re-applies preferred default when template badge_type_list loads async,
falling back to first item if 'attendee' isn't in the template's list
- Remove the blank '-- Select Badge Type --' placeholder option
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
btn + preset-filled-* renders transparent on gray/surface backgrounds
(Skeleton v4 CSS variable specificity issue — documented in
GUIDE__AE_UI_Style_Guidelines.md §12).
Replace all three buttons in field_actions (Save, Revert, Cancel) with
direct Tailwind token classes: bg-warning-500, bg-error-500,
bg-success-500, bg-surface-200-800 etc. Save button now visibly renders
in amber (dirty), red + pulse (pending_close), green (saved).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a field accordion has unsaved changes and the user tries to close
(X button, same-header click, or switching to another field), we now set
pending_close = true instead of silently discarding.
- Save button turns bright red + animate-pulse with label "Save first (or × to discard)"
- X button turns red with "Discard changes" tooltip
- Field stays open — no data is lost
- Second close attempt (pending_close already true) actually discards
- Saving normally clears pending_close and closes the accordion
WHY: kiosk attendees at a live event were silently losing typed overrides
(professional title, affiliations, etc.) when switching fields mid-queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
toggle_field only changed active_field — it never called cancel_field for the
previously open field. Unsaved typed values stayed in edit_full_name_override etc.,
so reopening a field would show the stale typed value and re-apply it to the badge
preview, even though the user had already moved on.
New logic: capture was_open, always call cancel_field for the current field (resets
edit vars + sets active_field = null), then open the new field if it wasn't the one
being closed. Closing a field by re-clicking its pencil now also discards unsaved state,
consistent with the explicit [X] button behavior.
Also: add global placeholder CSS fix to TODO__Agents.md (scoped workaround already
in ae_comp__badge_print_controls; long-term fix belongs in app.css or theme file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auto-focus: requestAnimationFrame (~16ms) fired before the 185ms accordion
animation ended — input was still 0px/clipped so focus() silently failed.
Changed to setTimeout(210ms) so focus lands after the animation completes.
Placeholder color: placeholders show the current badge value (e.g. 'John Smith')
so without explicit styling they look identical to filled text. Added scoped
CSS rules setting placeholder to gray-400 (light) / gray-500 (dark) so it reads
clearly as a hint rather than existing content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
update_ae_obj__event_badge returns false on API failure without throwing,
so the old code always treated a failed PATCH as success — badge printed,
count not saved, page navigated away silently.
Now: check the return value explicitly. On failure — still fire window.print()
(physical print must never be blocked) and still navigate back, but show a
visible red error state ('Printed — count NOT saved (see staff)') and hold
for 4s so a kiosk operator can see it before the loop resets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MariaDB rejects ISO 8601 with milliseconds ('2026-04-14T20:29:15.784Z').
print_last_datetime and print_first_datetime must be 'YYYY-MM-DDTHH:MM:SS'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, a public user who navigates to a printed badge's print page
sees a blank controls panel with no explanation. Now shows an amber notice
directing them to event staff. Gated on !can_print && is_printed && !is_trusted
so it never shows to staff.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The name|layout|v3 info row was always visible. Gate it on
trusted_access && edit_mode — attendees and volunteers should
not see internal template metadata above their badge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HTML comments don't suppress Svelte {#if} blocks — the content was rendering
unconditionally. Switch to {#if false} so the blocks are genuinely hidden.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lead scanning was canceled last-minute; pronouns not on this badge template.
Both blocks left in source with AXONIUS 2026 markers for easy restoration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hide/Unhide and print count edit belong on the per-badge page (print controls
staff section), not the search list — the list was getting too crowded.
- ae_comp__badge_obj_li: removed hide toggle, print count input, and the
ae_api/events_func imports that were only there to support them
- ae_comp__badge_print_controls: added Hide Badge button (Trusted, top of staff
section) and Print Count editor (Admin+, below hide); both reuse the existing
save_field/field_save_status pattern for consistent spinner/done/error feedback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lower access levels (authenticated, public) can have edit_mode active.
Show Hidden must be trusted+ only — split it out of the generic edit_mode block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hide/Unhide toggle button (Trusted + Edit Mode) on each badge row in the list; badge disappears immediately when hidden unless Show Hidden is active
- Print count inline editor in debug row (Admin + Edit Mode); updates count only, no timestamp changes
- "Show Hidden" checkbox in search filters (Trusted + Edit Mode); wires through IDB fast-path, API hidden param, and visible_badge_obj_li filter
- show_hidden requires edit_mode to be active — reverts to hiding hidden badges when edit mode is off
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Edit mode should not override the filter state — staff set their
filters and turn off edit mode all the time. The real split is
trusted staff vs kiosk/public, not edit mode on/off.
Trusted and above: qry_printed_status is the sole control over
printed badge visibility, regardless of edit mode state.
Public (kiosk) / authenticated / anonymous: always unprinted only.
Badge kiosks run at public_access and should never expose a list
of already-printed badges to attendees.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. visible_badge_obj_li gated on trusted+edit_mode, but the filter
dropdown is also accessible to manager+ without edit_mode. Changed
gate to (trusted+edit) || manager_access to match the filter's own
access condition.
2. not_printed API query used print_count eq 0, which does not match
NULL in SQL. Unprinted badges have print_count = NULL, so the API
was returning 0 results and overwriting the correct IDB fast-path
results. Removed the not_printed condition from the API query —
IDB fast path (print_count ?? 0) < 1 and visible_badge_obj_li
both handle NULL correctly and are the authoritative filter for
that case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously edit_mode was a blunt override: trusted+edit showed all
badges regardless of the filter setting. This meant the Printed Status
dropdown had no effect on what was visible in the list.
Now trusted+edit mode respects qry_printed_status as the single source
of truth: 'all' shows everything, 'printed' shows only printed, and
'not_printed' shows only unprinted. The filter dropdown is only
accessible to trusted+edit users so it is safe to use as the control.
Kiosk/attendee behavior (trusted no edit, public, anonymous) unchanged:
only unprinted badges are shown regardless of filter state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sort changes without a text query were falling through to the fallback
liveQuery (50 badges sorted by given_name), ignoring the selected sort
entirely. Added params.sort to has_active_filters so any explicit sort
selection triggers the full search path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All four sort options in the dropdown were falling through to the
default (given_name ASC) because their cases were missing from both
the IDB fast-path sort and the API order_by_li mapping:
- Affiliations ASC: IDB sorts by affiliations_override → affiliations;
API sorts by affiliations column
- Badge Type ASC: badge_type_code ASC
- First Printed DESC: print_first_datetime DESC
- Last Printed DESC: print_last_datetime DESC
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update badge type codes for Axonius 2026 (replaces ISHLT 2024 list).
Added TODO to drive this from event templates in the future.
Fix printed status and badge type filters not working without a text
query. The min_chars guard was blocking all filter-only searches,
causing "Printed" and "Not Printed" to always return empty results.
Now bypasses min_chars when any non-default filter is active (printed
status, type code, or affiliations), since selecting a filter is
explicit user intent regardless of the text query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Badge print kiosks authenticate at the public_access level (site-wide
passcode). Previously the print gate was trusted_access, meaning kiosk
operators had to sign in at the trusted level just to print.
Changed in both the list view and the badge detail controls panel:
- First print: public_access and above (kiosk use case)
- Reprint: still requires trusted_access + edit_mode
ae_comp__badge_obj_li.svelte: added is_public derived; updated
can_print and the print button #if condition.
ae_comp__badge_print_controls.svelte: added is_public derived; updated
can_print comment and logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Font size changes now persist automatically (600ms debounce) without
requiring the user to find and click Lock Sizes in the collapsed Staff
section. reset_font_sizes_to_auto continues to handle its own save.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per V3 convention, {obj_type}_id IS the random string — send
event_badge_template_id (not _random). The backend not saving it is
a backend bug, not a frontend concern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The IDB stores the random string in event_badge_template_id (overwritten
by _process_generic_props). Sending this as event_badge_template_id
passed a string to an int(11) FK column — backend silently ignored it.
Using event_badge_template_id_random lets the V3 CRUD handler resolve
it to the correct integer FK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-template events auto-select silently but gave no visual
confirmation. Added a read-only display of the template name so staff
can verify the correct template is in use before submitting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every badged event must have a template — without one the badge cannot
render. Changed auto-select from === 1 to >= 1 so multi-template events
also get a default (first template). Added an error message and disabled
submit when no templates are configured at all. Removed blank
"-- Select Template --" option so the form never submits with null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Professional title, organization, and location entered during manual
badge creation were being stored in the *_override fields. Override
fields are intended for overriding imported/AMS person data — for new
manually created badges, the base fields (professional_title,
affiliations, location) are correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each data-theme selector is fully self-contained — CSS custom properties
do not inherit across theme selectors. The Axonius file only defined the
primary ramp, leaving surface/secondary/tertiary/success/warning/error
undefined, causing the UI to render in grey/black/white.
Added full color ramps and dark-mode contrast tokens (matching base
AE_Firefly) to both light and dark blocks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `bleed` field to BadgeTemplateCfg (CSS length, e.g. "0.125in")
- Badge view: derive bleed_offset, move bg-image to absolute positioned div
that extends past card edges; add isolation:isolate to badge_front stacking context
- Template form: add bleed input in Advanced > Appearance; wire to cfg_json save/load
- PVC layout CSS: change overflow:hidden → overflow:visible in print rule to allow
bleed div to render at physical card boundary (Zebra driver clips at card edge)
- Prevents white borders on PVC cards when printer has slight alignment variance.
Screen preview shows bleed visually extending past the card outline.
Allows coordinators to pre-tune font sizes for attendees with long names
and have those sizes apply automatically on every kiosk, not just one machine.
- ae_types.ts: add cfg_json to ae_EventBadge interface
- db_events.ts: add cfg_json to Badge Dexie interface
- ae_events__event_badge.ts: add cfg_json to properties_to_save so it is
persisted to IndexedDB on load and returned by the API
- print/+page.svelte: on first load per badge, read cfg_json.font_sizes and
initialize font_size_name/title/affiliations/location state from saved values
(guarded by _font_sizes_loaded_for to avoid clobbering user adjustments on
background liveQuery refreshes)
- ae_comp__badge_print_controls.svelte: add lock_font_sizes() and
reset_font_sizes_to_auto() functions; add Lock Sizes / Auto reset UI in the
Staff adjustments section (trusted-only); button shows warning style when
sizes are unsaved vs success when locked; status indicator shows what is
currently locked
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- badge_template_form: fix default field visibility (location off render, pronouns/leads excluded from controls); fix duplicate QR checkboxes by removing orphan show_qr_front/back state vars; reorganize Advanced cfg_json into labeled sub-groups; make all 5 non-Advanced sections collapsible (general starts open, rest collapsed)
- print_controls: add DEFAULT_SHOWN constant so field_shown() uses explicit whitelist fallback instead of showing all fields when no controls_cfg is set
- badges config +page: add Templates navigation button in header (FileText icon)
- templates +page: add back-nav header with ArrowLeft to badges/config, Settings icon, page title
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TODO__Agents.md: added task for contact search — backend to whitelist
contact_li_json_ext in event search, frontend to add OR condition in
search__event() and update local IDB fast-path filter. Blocked on backend.
- CLIENT__IDAA_and_customized_mods.md: documented the search architecture
gap under Recovery Meetings — what default_qry_str contains, why
contact_li_json is unsearchable as raw JSON, what contact_li_json_ext is
and what needs to happen to enable contact name/email search.
Backend agent notified via ae_send_message 2026-04-08.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added Section 4 'Staff Editing Rules': documents per-object behavior when
trusted/admin staff edit member content. BB Post external_person_id is readonly
for non-admin staff; Post Comment preserves existing record identity; Recovery
Meeting external_person_id is intentionally editable for ownership reassignment.
Clarifies that staff identity only fills when the record has no existing linkage.
- Added Section 5 'Recovery Meetings — Contact 1 Convention': states the business
rule that Contact 1 is nearly always the same person as external_person_id (the
meeting owner). Documents the distinction between ownership link vs. display contact.
- Added race condition defense note to Section 3 Implementation Patterns: creation
buttons and edit submit handlers must scavenge from localStorage when the Svelte
store is briefly null on mount.
- Updated test coverage table: Recovery Meetings now has substantial Playwright
coverage (idaa_recovery_meeting_edit.test.ts). Noted pending BB Post/Comment tests.
- Updated Last Verified date to 2026-04-07.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three targeted fixes following code review of the Novi UUID linkage commit:
1. ae_idaa_comp__post_obj_id_edit.svelte — Add localStorage scavenge fallback
in handle_submit_form() for external_person_id / full_name / email.
WHY: The form input falls back to $idaa_loc.novi_uuid at render time only.
On a race-condition mount where the store was null, the input captures an
empty string. Without this, a subsequent PATCH on a legacy post (no
external_person_id) would overwrite the field with an empty string, permanently
breaking the Novi linkage for that record. The scavenge re-checks the live
store and then localStorage before submitting.
2. ae_idaa_comp__post_options.svelte — Fix double alert() on creation failure.
WHY: The .catch() handler alerted the user and reset 'creating'. The
.finally() block then ran unconditionally and fired a second alert when
final_id was null (which it always is on failure). User saw two dialogs.
Fixed by removing the duplicate alert from .finally() — it now only resets
the 'creating' flag, which .catch() may have already done (harmless reset).
3. ae_idaa_comp__post_comment_obj_id_edit.svelte — Remove 'log_lvl = 1' mutation.
WHY: log_lvl is a $bindable prop. Assigning to it inside handle_submit_form()
unconditionally mutated the parent binding on every single form submission,
overriding the caller's logging preference. This was debug code accidentally
left in. Removed; the existing 'if (log_lvl)' guard is sufficient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CRITICAL IDENTITY FIX:
Ensures all member-generated content (Meetings, Posts, Comments) is explicitly linked to the creator's Novi UUID via 'external_person_id' at the moment of creation.
Changes:
- Added 'external_person_id' to creation payloads in Recovery Meetings and BB Posts.
- Implemented 'identity scavenging' from localStorage in submit handlers to prevent race conditions where Svelte stores are briefly null.
- Refactored Post Comment edit component to robustly initialize and save creator identity.
- Added 'The Novi UUID Rule' to IDAA documentation to mandate this pattern for future development.
- Added Playwright test to verify creation linkage and fixed a version-mismatch bug in the test auth helper.
Note: Archives and Archive Content are excluded as they do not require member ownership.
- Center modal horizontally; position 10vh from top instead of centered vertically
- Add Allow/Do-not-allow toggle buttons inside the TC modal so attendees
can set their lead scanning preference while reading the terms
- Buttons reflect current DB value on open and use solid color fills
(green/red) so selection state is unambiguous in light and dark mode
- Save & Close calls existing save_field('allow_tracking') then closes;
Cancel closes without saving
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
Two corrections to the qry_files filter:
1. Switch from file_count to file_count_all — covers files on presentations
and presenters under the session, not just direct session files.
2. Switch "without files" from eq:0 to is_null — the view uses a LEFT JOIN
so sessions with no files get NULL, never 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
qry_files was accepted as a parameter but never applied to the search query,
causing the "Sessions With/Without Files" report toggle to always return all
sessions regardless of the setting.
When qry_files !== null, automatically switch to the 'alt' view
(v_event_session_w_file_count) which exposes file_count, then add:
true → file_count > 0 (sessions with files)
false → file_count = 0 (sessions without files)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The field exists on the DB object but was missing from the TypeScript interface,
causing a false error in recovery_meetings search. Added it to db_events.ts where
it belongs. Removed the incorrect global DOM Event augment from the temp augments
file (was patching the wrong interface).
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.
hide__session_code was defaulting to true, suppressing the code badge
in the session list on fresh sessions. Flip to false so codes are
visible out of the box — users can still hide via the menu toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- AE__Architecture.md: minor wording fix
- TODO__Agents.md: add Svelte 4→5 store migration task (root cause of IDAA
Novi re-auth bug; prerequisite for Phase 2c store refactor)
- PROJECT__Stores_Svelte5_Migration.md: new migration planning doc
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>
AE_LOC_VERSION 1→2: force-clears stale ae_loc localStorage on next page
load for all users. Fixes users stuck on "Verifying identity..." in the
IDAA iframe — their cached site_cfg_json predated novi_idaa_api_key being
added to the site record, leaving api_key null so verification never ran.
AE_IDAA_LOC_VERSION 1: ae_idaa_loc (Novi auth state) was never included in
store_versions.ts — no wipe mechanism existed for it. Added now so future
schema changes can be handled cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records the root cause of the 2026-03-27 hidden-error discovery (broken ambient
declaration masking 31 pre-existing svelte-check errors), the lesson learned, and
two follow-up tasks: fix ModalProps.children across 26 files, remove shadcn-svelte
and bits-ui from package.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lucide-augment.d.ts: add `aria-hidden?: string | boolean` to IconProps
(SVGAttributes drops this too in @lucide/svelte ≥ 0.577.0)
- Remove src/lib/components/ui/ — ShadCN primitives with zero importers;
bits-ui API drift was generating ~20 type errors for dead code
svelte-check: 31 errors remaining (all ModalProps.children — flowbite-svelte
API change, deferred to next session), 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.d.ts is a script-context declaration file. A `declare module 'x' {}`
in a script file is an ambient module declaration that completely replaces
the package's types — not an augmentation. This caused svelte-check to see
@lucide/svelte as exporting only IconProps, producing 1131 "class" errors
and 237 "no exported member" errors for every icon import.
Moving the augmentation to src/lucide-augment.d.ts with `export {}` makes
it a module file, so `declare module` becomes a proper augmentation that
merges with the package types. Result: Lucide errors drop from 1368 to 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lucide/svelte >=0.577.0 dropped `class` from IconProps — it now derives
props purely from SVGAttributes<SVGSVGElement>, which TS types without
`class`. Every <SomeIcon class="..." /> in the codebase errored (1131
errors). Augment IconProps in app.d.ts to re-add `class?: string`.
Root cause: 0.561.0 → 0.577.0 bump in commit 366c6629 (2026-03-10).
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>
<svelte:head> scripts load asynchronously with no lifecycle hook to await
completion, so onMount could call init_jitsi() before JitsiMeetExternalAPI
was defined. Replace with a dynamic script loader that is awaited between
fetch_novi_data() and init_jitsi(). Also uses the domain from URL params
rather than the hardcoded jitsi.dgrzone.com hostname.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rather than hardcoding the IDAA admins group UUID or making an extra
API call, re-use the access level already established by the IDAA layout.
If $ae_loc.trusted_access is set (verified against novi_trusted_li /
novi_admin_li), the user is a moderator immediately. Only regular
authenticated members fall through to the group membership check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Older Novi pages that haven't been updated to pass g_uuid still need
the moderator check to work. Use [g_uuid] when present, otherwise fall
back to novi_idaa_group_guid_li from site config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of checking membership across all groups in novi_idaa_group_guid_li
(site config), pass the single g_uuid from the URL param. Each Novi iframe
page supplies the group relevant to that specific meeting, so checking just
that one group is both more precise and avoids unnecessary Novi API calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The {#if} gate only allowed the sys bar to mount for admins or
trusted+edit_mode users in an iframe. Trusted staff using show_menu=true
had sys_menu.hide set correctly but the component never mounted. Add
!sys_menu.hide as an escape hatch so the URL override actually works.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
20 calls/sec, 600/min, 100k/day. Notes the 10s flat backoff + single retry
and the 5-min TTL cache that prevents normal-use rate limiting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On a 429 response, waits 10 seconds then retries once. If the retry also
returns 429, throws and denies access (Reload/Retry button covers that case).
verify_in_flight and novi_verifying stay true during the wait so the spinner
remains visible and no concurrent calls can sneak in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UUID is set by Novi via iframe src at page load and never changes within a
session (impersonation = full iframe reload). Reading it once from
window.location.search eliminates reactive noise from SvelteKit client-side
navigation causing spurious re-verification runs.
Removed:
- verify_dep $derived.by (reactive UUID + site_cfg narrowing)
- dedupe snapshot + last_effect_* tracking variables
- verify_backoff_attempts and exponential backoff retry logic
- novi_rate_limited_until writes and UUID-change guards
- ~80 lines of complexity
Kept:
- site_cfg_json read outside untrack (effect still re-runs when API key loads async)
- verify_in_flight concurrency guard
- TTL cache (prevents duplicate calls on SWR site_cfg updates)
- All permission upgrade and store write logic
NOTE: If Novi adds dynamic impersonation (no full reload), see comment at
url_uuid declaration for what to restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical bugs fixed:
- $derived(() => {}) stored the function itself; uuid/api_key were always
undefined so verification never fired. Fixed to $derived.by(() => {}).
- novi_verifying pre-initialized to true (flash prevention) was also used as
the concurrency guard — guard saw it as in-flight and exited immediately,
leaving the spinner stuck forever. Split into separate verify_in_flight flag.
- $idaa_loc reads in dedupe snapshot (outside untrack) subscribed the effect
to idaa_loc writes, causing needless re-runs post-verification.
- Rate limit was not UUID-aware: 429 on one UUID blocked impersonation
(new UUID). TTL and rate-limit guards now both bypass when UUID changes.
Also includes: store defaults for novi_verified_ts + novi_rate_limited_until,
docs update, iframe template g_uuid param (prior agent changes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-step creation: POST event_person first, then event_badge linked to it.
Badge create route (event_person parent) pending backend fix — frontend is
ready and passing event_person_id + event_badge_template_id in payload.
- ae_events__event_person.ts: new create function (nested under event)
- ae_events_functions.ts: export create_ae_obj__event_person
- ae_comp__badge_create_form.svelte: modal form with live name preview,
conditional display-name override, template selector (auto-selects when
only one template), badge_type_code_li derived from selected template's
badge_type_list JSON, two-step submit status labels
- +page.svelte: load template list via liveQuery, wire Create Badge button
(edit_mode only), native <dialog> modal with backdrop, remote-first
refresh on success
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace stale deploy:staging/deploy:prod references with current
build:docker:*, deploy:remote:*, and .env.dev/test/prod file names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deploy:dev/test/prod → build:docker:dev/test/prod to distinguish
local Docker builds from remote server deploys
- Add deploy:remote:test and deploy:remote:prod — SSH to linode.oneskyit.com
and run deploy.sh on the server
- Trailing whitespace cleanup in .env.*.default files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The staging default template had real OSIT account_id and event_id values
in inline comments. These are not secrets but shouldn't be in a committed
template — they'd be misleading on any non-OSIT deployment.
Replaced with plain XXXX placeholders.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added section 7 covering /v3/action/user/ endpoints: authenticate, verify_password,
change_password, new_auth_key, email_auth_key_url — including the legacy→V3
migration table and auth key one-time-use behavior.
Also clarified the response shape note to explicitly list all response types
(GET single, GET list, POST create, PATCH, search) that use the V3 envelope.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added mode, credentials, redirect, and cache options to the GET fetchOptions
object. These were previously left to browser defaults, which vary by environment
and can produce opaque CORS failures that are hard to diagnose. Being explicit
avoids environment-dependent surprises.
Also added a try/catch around response.headers logging (log_lvl >= 1) so header
dumps don't throw in environments that restrict header access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_object() returns false on network failure; the .then() handler was
running with result=false and accessing result.hosted_file_id (evaluates
to undefined, valid JS key, no throw) so all success state was set even
though the request failed.
- Guard result in .then(): if !result.hosted_file_id → set status='error'
- Add 'Failed — Retry?' button state in error branch
- Raise client-side AbortController timeout 300s → 1800s (30 min)
- Add comment explaining root cause (get_object returns false, not throw)
Root cause of the connection drop is proxy_send_timeout or NAT hairpin
timeout (both default 60s) — not a frontend issue; tracked separately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Fulfills Phase 4 open item from PROJECT__AE_Object_Field_Editor_V3_upgrade.md.
Covers import, basic text usage, all field types, select with nullable FK,
key props table, and behavior notes (optimistic display, edit_mode visibility).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PROJECT__AE_Object_Field_Editor_V3_upgrade.md: mark datetime support complete (was already implemented, just not checked off)
- AE__Components.md: replace stale ae_crud entry with element_ae_obj_field_editor_v3 description; correct TipTap "marked for removal" note (TipTap is actively used)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
element_ae_crud.svelte and element_ae_crud_v2.svelte had zero active
importers; only a commented-out reference remained. Moved both to trash
and removed the dead comment from ae_comp__event_presentation_obj_li.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Relocates the functions file from lib root into its module directory,
matching the pattern used by all other modules (ae_journals, ae_archives, etc.).
Updated all 85 import paths from \$lib/ae_events_functions → \$lib/ae_events/ae_events_functions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add justify-center + h-6 to the debug info row above the badge so it stays centered
and doesn't cause vertical shift when conditional elements show/hide on edit mode toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add field_shown() and field_editable() functions driven by event_badge_template.other_json:
controls_cfg: { shown?: string[], auth_editable?: string[] }
Access rules:
- No authenticated_access → display-only, no edit buttons shown
- authenticated only → can edit fields in auth_editable (default: title/affiliations/location/allow_tracking/pronouns)
- trusted + edit_mode → always sees and edits all fields, ignores config
Each attendee field card (name, title, affiliations, location, allow_tracking, pronouns)
is now wrapped in {#if field_shown()} and its edit button/accordion gated by field_editable().
No backend changes needed — other_json is an existing longtext JSON column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add name_two_lines toggle (default: true) — uses CSS horizontal padding scaled by
character count to coax short names (e.g. "Scott Idem") into a natural two-line wrap
without a hard <br>; three tiers: ≤12 chars (18%), ≤20 (8%), ≤28 (2%), >28 no pad
- Inner <div> (block element) used inside Element_fit_text for class: directives —
Svelte scoped CSS requires static class names in the template; dynamic strings and
class: on component elements both fail to match scoped CSS rules
- Add leading-none to all four Element_fit_text fields (name, title, affiliations,
location) — line-height must be set at the wrapper div level where fit_text measures
scrollHeight, otherwise the binary-search scaler returns inflated sizes
- name_two_lines state persisted to localStorage (ae_badge_print_tweaks key) alongside
existing print_offset, hide_chrome, and banner_full_width tweaks
- Rewrite badge_header_calibration.svg as a precise SVG ruler with labeled tick marks
(major at 1in intervals, minor at 0.25in) for accurate physical print calibration
- Gate debug outline CSS on html.debug_outlines class (set by controls panel) so
outlines never appear in normal print mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 4 badge test files had identical ~35-line beforeEach blocks (pageerror listener,
inline V3 route mocks, addInitScript localStorage seed). Replaced with two helpers
in minimal_v3_mocks.ts:
seed_trusted_session(page, event_id, account_id?)
— seeds ae_loc localStorage with trusted/manager auth via addInitScript;
account_id defaults to testing_account_id
setup_badge_test_page(page, event_id)
— one-call beforeEach: pageerror listener + attach_minimal_v3_routes +
seed_trusted_session
Each test file's beforeEach is now 1-3 lines. All 12 tests still pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MODULE__AE_Events_Badge_Templates.md: mark style_href, duplex, and
badge_back suppression as done; correct v1→v2 component references
- PROJECT__AE_Events_Badges_Review_Print.md: update project status
- TODO__Agents.md: close debug-outlines, style_href, duplex TODOs;
mark window.print() wiring as done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Debug outlines were applying to all print jobs. Now scoped to
html.debug_outlines so they only appear when the "Show debug outlines"
checkbox is active in the controls panel (trusted users only).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause fix: tests/_helpers/ae_defaults.ts was missing __version: 1, causing
store_versions.ts to wipe ae_loc from localStorage on every test page load. This
made trusted_access fall back to false, hiding the print button (can_print guard)
and failing all attendee workflow tests.
Changes:
- ae_defaults.ts: add __version: 1 with comment explaining the store_versions guard
- idb_helpers.ts: extract inject_badge_and_template() from print layout test into
shared helper; now used by all three print/render test files
- event_badge_render.test.ts: new — 4 tests covering full_name_override priority,
full_name fallback, duplex=0 hides badge back, duplex=1 shows badge back
- event_badge_attendee_workflow.test.ts: cleaned up (diagnostic code removed);
all 3 tests now pass
- event_badge_print_layout.test.ts: renamed from badge_print_layout.test.ts;
inline inject_idb() replaced with shared idb_helpers import
- event_badge_smoke.test.ts: renamed from event_badge.test.ts
- playwright.config.ts: use system /usr/bin/chromium on Arch Linux (avoids
Playwright's bundled Chromium which requires Ubuntu libs not present on Arch)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iframe=true now hides the sys bar for all users (previously trusted_access
users still saw it). Admins can pass show_menu=true to re-enable it while
testing an embedded page like video_conferences.
hide_menu=true remains for non-iframe hide use cases (kiosk, etc).
Updated URL builder: hide_menu checkbox → show_menu checkbox.
Updated GUIDE__Development.md URL params table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ae_ prefix belongs on Svelte component/variable names, not URL params.
Updated both the consumer (+layout.svelte) and the builder (jitsi_url_builder).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The URL builder generates ae_hide_menu=true but nothing consumed it.
Now layout.svelte reads the param on mount and sets $ae_loc.sys_menu.hide,
which flows through bind:hide into E_app_sys_bar's class:hidden.
Applies even for trusted_access users who bypass the iframe guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ae_hide_menu=true query param option to suppress the AE navigation
chrome when embedding the Jitsi video conference page in Novi or other
host pages that provide their own navigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New component ae_idaa_comp__jitsi_url_builder.svelte builds and previews
Jitsi iframe URLs for testing and Novi page configuration. Features:
- Environment selector (prod / dev / local / custom)
- Room name, Novi UUID, site key inputs
- Moderator toggle (explains JWT + logging implication)
- Advanced: domain, start muted/hidden, all 5 sound settings
- Output in URL or iframe HTML snippet mode with copy button
- "Open in new tab" for quick testing
Embedded on jitsi_reports page as a collapsible panel, gated to
trusted_access users only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jitsi_reports was previously at src/routes/idaa/jitsi_reports/ and
was not protected by the (idaa) layout auth gate. Moved to
src/routes/idaa/(idaa)/jitsi_reports/ — same URL, now requires
trusted_access or Novi-verified authenticated access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a passcode matched, entered_passcode was cleared and the trigger
was set, but show_passcode_input was never set to false. This left the
input visible so users could keep typing after access was granted.
Set show_passcode_input = false immediately after clearing entered_passcode
on a successful match, consistent with the intent described in the
handle_clear_access() function which resets it to true on clear.
Export button now only renders when event_exhibit.leads_api_access === true,
preventing a 403 that would always fire otherwise. Endpoint confirmed live on
backend. TODO updated to reflect export + allow_tracking gate both resolved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updated download_export__event_exhibit_tracking to call
/v3/action/event_exhibit/{exhibit_id}/tracking_export instead of the
legacy /event/exhibit/{exhibit_id}/tracking/export (V1 path).
Added doc comment listing the expected export columns so the backend
agent has the full field spec when implementing the endpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- QR scanner: after badge loads, blocks add with 'Tracking Opt-Out' warning
card if allow_tracking !== true; replaced deprecated CheckCircle → CircleCheck
- Manual search: shows ShieldOff 'Opt-Out' label per row for blocked badges;
add_as_lead() also guards against programmatic bypass
- Fix: ae_comp__exhibit_tracking_obj_li — Loader2 from wrong package
@lucide/svelte → LoaderCircle from lucide-svelte
- ae_types.ts: added allow_tracking and agree_to_tc to ae_EventBadge interface
- README.md (leads): full rewrite reflecting actual current state and known gaps
- TODO__Agents.md: updated Leads entry from stale 'NEXT MAJOR FEATURE' to
accurate in-progress status with remaining checklist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ae_stores.ts
- Extract auth/identity section (~80 lines) into ae_stores__auth_loc_defaults.ts
- Spread auth_loc_defaults into ae_app_local_data_defaults (zero consumer changes)
ae_events_stores.ts (both loc and sess structs)
- badges → ae_events_stores__badges_defaults.ts
- launcher → ae_events_stores__launcher_defaults.ts
- leads → ae_events_stores__leads_defaults.ts
- pres_mgmt → ae_events_stores__pres_mgmt_defaults.ts
Each new file exports *_loc_defaults and *_sess_defaults. The store files
now reference these by name instead of embedding inline objects. All
$ae_loc.* and $events_loc.* consumer paths are unchanged.
svelte-check: 0 errors, 0 warnings
- Remove unused `import { offset } from '@floating-ui/dom'`
- Remove ver_idb constant and field (same as ae_stores / ae_events_stores)
- Remove commented-out personal Novi UUIDs (security hygiene — these belong
in site_cfg_json on the server, not in source; idaa layout already reads
them from $ae_loc.site_cfg_json and writes to idaa_loc at mount)
- Add comment explaining novi_admin/trusted/jitsi_mod_li are server-driven
- Remove dead writable/persisted alternatives and console.log lines
- Remove stale 'Updated 20xx-xx-xx' date comments
- Condense redundant tracking comments to single-line form
Verified all FA spans and variant-* patterns in launcher files are inside
HTML comment blocks — no live FA or legacy variant classes remain.
The freeze was precautionary; the actual migration was already done.
Phase 3 for Launcher is now limited to UX/card styling polish only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Marked all non-frozen, non-IDAA style migration work as done:
- FA→Lucide across events, pres_mgmt, core, badges, leads, hosted_files
- variant-* → preset-* across all modules
- badge code_to_icon refactor, FA CDN scoped to IDAA layout
- global svg.lucide inline fix in app.css
Phase 3 deferred: Launcher (post-April 2026) and IDAA (last priority).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lucide renders <svg> elements which default to inline-block in browsers,
causing icons to break onto their own line when mixed with text — unlike
FA spans which were display:inline.
Added svg.lucide { display: inline; vertical-align: middle; } to app.css
so all Lucide icons flow inline with adjacent text globally, matching the
FA icon behavior without needing class="inline" on every icon instance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the FA 5.15.4 CDN <link> from app.html (global) into
src/routes/idaa/+layout.svelte <svelte:head> so it only loads
on /idaa/* routes. All other modules now use Lucide exclusively.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert 6 template-level FA spans to Lucide components (Star, Biohazard,
Asterisk, Wifi). The code_to_html JS string dict (dietary symbols used
with {@html}) retains FA spans since they are raw HTML strings, not
Svelte template markup — FontAwesome CSS (app.html CDN) renders them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ae_idaa_comp__event_obj_id_edit.svelte: the component <style> block used
@reference + @apply for ~10 local classes (.section-card, .field-label,
.toggle-chip, .day-chip, etc.). svelte-check's CSS language service does
not understand Tailwind v4 @reference/@apply directives and emitted 23
'Unknown at rule' warnings.
Fix: all local class usages inlined as Tailwind utility strings directly
on each element (~80 template sites). The <style> block is removed.
Conditional classes on toggle-chip/day-chip converted to ternary expressions.
svelte-check now reports 0 errors and 0 warnings across all files.
Svelte 5 does not support <style> or conditional {#if} blocks wrapping
<style> tags inside <svelte:head>. The parser treats them as raw-text
elements and reports '<script> was left open' at EOF.
Fix:
- Print media CSS moved to static/ae-print-badge.css (plain static file,
no framework magic needed — all selectors target global elements).
- svelte:head now uses a simple <link> to that file.
- $effect injects the @page size dynamically per template layout field,
avoiding the Svelte 5 parser limitation for conditional style injection.
- Badge_template interface in db_events.ts: added cfg_json / data_json
(standard Aether object fields that were missing from the type).
Replaces all FontAwesome <span class="fas/fab fa-*"> with Lucide Svelte
components across 20 launcher files. launcher_cfg_section.svelte icon prop
changed from FA string to AnyComponent (svelte:component for dynamic render).
Dynamic file-extension icon now uses ae_util.file_extension_icon_lucide().
Fixes class: directives on components (invalid in Svelte 5) → ternary class.
Removes title prop from Lucide components → wrapping <span title="...">.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Defaults to true (authenticated_access required) — no change to existing consumers
- Launcher file buttons set require_auth=false to allow unauthenticated downloads
Replaces all alert() calls in the user/pass auth flow with reactive state.
Button shows: Verifying… (disabled) → Failed — retry? (red) →
Enter credentials first (amber) → Username/User ID Sign In (default).
Error messages (wrong password, no person record, no server response)
appear as small text below the button on failure.
Clicking the button resets to default so retry is clean.
Also removes dead commented-out alert and cleans up the promise chains.
No type="button" issues found — all non-submit buttons were already typed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the debug alert() calls from the email magic-link flow.
Button now shows live feedback inline:
- 'Sending…' while the lookup is in flight (disabled + cursor-wait)
- 'Email sent ✓' on success (green fill)
- 'No account found' if no user matches the email
- 'Error — retry?' on network/API failure
- 'Enter an email first' if submitted empty
Clicking the button while showing a result resets it to the default label.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign In/Out (e_app_sign_in_out.svelte):
- Remove redundant internal header (sr-only was broken by :global CSS override)
- Full-width form inputs and buttons with 'or' divider between the two methods
- Signed-in state shows centered username and full-width action buttons
Access/Passcode (e_app_access_type.svelte):
- Fix 'Locked' button: was running trigger=true (no-op permission reprocess);
now correctly toggles show_passcode_input so the input shows/hides on click
System bar (e_app_sys_bar.svelte):
- Dynamic section headers: Sign In/Out shows username when signed in;
Access/Passcode shows ShieldEllipsis/ShieldMinus/ShieldUser based on state
- Fix passcode input not showing on re-open via menu button:
onDestroy resets show_element__passcode_input=false; toggle_expand now
restores it to true for anonymous/no-access state (matches handle_shield_click)
- Broaden anonymous check from === 'anonymous' to !access_type || === 'anonymous'
- Remove dead getElementById focus call (DOM not ready at that point;
focus_input binding in Element_access_type handles it correctly)
- Appearance section: mode/font buttons at top, dark mode gets amber tint
in light mode for visual context, theme select uses text-sm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- [journal_id]/+page.svelte was rendering a second Journal_obj_id_edit
alongside the one already in ae_comp__journal_obj_id_view.svelte,
causing the modal to open twice simultaneously.
- ae_comp__journal_obj_id_view: changed show={} to bind:show={} so that
closing the modal properly writes back to journals_sess, preventing the
store from fighting the close and re-opening it.
Flowbite's built-in outsideclose was unreliable for this side-drawer setup.
Root cause: Flowbite's _onclick handler uses dlg.getBoundingClientRect() to
detect backdrop clicks. This works in theory for showModal() dialogs, but
something in the Svelte transition / stacking context prevented it firing.
Fix: bypass Flowbite's detection entirely.
- onclick prop on <Drawer> is spread through Flowbite's restProps chain
to the native <dialog> element, overriding the broken _onclick handler.
Handler: just closes the drawer unconditionally.
- A stopPropagation wrapper div around all visual panel content ensures
that clicks INSIDE the panel never bubble up to the dialog element.
Only genuine backdrop clicks (outside the visual panel area) reach
the dialog and trigger the close handler.
ESC key is unaffected — it fires oncancel not onclick.
- Hover info strip is always in DOM (opacity-only toggle, no {#if} mount)
so first hover no longer triggers a layout recalc/flash
- Bar strip gets fixed h-9 height so inline label text appearing on
group-hover can never shift the bar vertically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hover info strip is now absolute-positioned above the bar (opacity fade
with delay-500) so it never shifts the bar layout — fixes the bounce
- Panel widened to w-80 with overflow-x-hidden — fixes horizontal scroll
caused by sub-components hardcoding w-72 inside the padded panel
- All panel sections are now collapsible (Access open by default, others
closed) — reduces vertical crowding; matches launcher_cfg pattern
- Section headers show current state inline (access level, theme name/mode)
- Admin section groups cfg + debug trigger together cleanly
- Bar transitions use duration-200 for snappier feel without bounce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New compact bar + expandable panel design:
- Compact strip (bottom-right): auth shield, font cycler, dark/light toggle,
edit mode toggle (authenticated+), menu expand — icon-only by default,
labels reveal on hover
- Hover info strip shows person name + access level when logged in
- Expanded panel: sign in/out, access/passcode, appearance (theme), admin
(config + URL builder + debug trigger) — all gated same as before
- Debug overlay trigger moved into admin section (edit mode only)
- All business logic preserved via existing sub-components (unchanged)
- e_app_sys_menu.svelte retained but no longer mounted (import commented out)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New e_app_url_builder.svelte component lets admins construct and copy
shareable URLs with any combination of core global params (iframe, theme,
theme_mode, key). Outputs full URL by default; toggleable to params-only
string for pasting onto existing links. Integrated into e_app_cfg Utilities
section (visible in edit mode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Flowbite Dialog component (which Drawer wraps internally) renders a
<CloseButton type='submit'> by default when dismissable=true. Since the
Drawer does not use the form/dialog mechanism, that button appeared at the
bottom of the drawer but did nothing. Fix: dismissable={false} on the cfg
Drawer suppresses it.
launcher_cfg.svelte footer redesigned:
- Lower-left: Close button (always visible) — mirrors top-right X, useful
when the user has scrolled down through long config sections
- Lower-right: Reload (always visible, shorter label)
- Full-width Debug Panel button (edit_mode only, below the row)
Click-outside behavior unchanged — Flowbite Dialog outsideclose defaults
to true, so tapping outside the drawer still closes it.
- launcher_cfg_section.svelte: Remove md:grid-cols-2 from content wrapper.
Root cause of the middle-width layout issue: at md breakpoint the drawer
is only 384px wide but the section body switched to 2-column, cramming
full-width content blocks into ~170px each. Always grid-cols-1 now.
- launcher_cfg.svelte: Rename Hardware tab to Device (neutral — applies
even in browser). Reorder Device tab content: Sync Timers first (relevant
to all devices), then native sections behind $ae_loc.is_native || edit_mode.
Updates still hidden behind is_native only (no useful preview).
- launcher_cfg_native_os.svelte: Add dev-preview banner when edit_mode is
on but not running in Electron. electron_relay.ts guards all calls with
'if (!native) return null' so there are no import errors or crashes —
controls simply show with a warning indicator. Removes stale placeholder
comment left by a previous agent.
- Setup (default): Oral/Poster preset, WS Controller, Screen Saver
- Hardware: Electron health/OS/updates (native only) + Sync control
- Dev: Local reset/debug tools — tab hidden until Edit Mode is on
Edit Mode toggle added as subtle pencil icon in the cfg drawer header
(low opacity when off, primary-colored when active). This is intentional
— onsite kiosk operators should not stumble on it, but admins doing
setup can find and toggle it without leaving the drawer.
Dev tab visibility and the Debug Panel button both gate on edit_mode,
keeping the default view clean for non-technical operators.
Adds a two-button Session Mode Preset toggle in Display & App Modes cfg:
- 'Oral / Default' restores all menus/headers/iframe off
- 'Poster Kiosk' sets iframe=true + hides menu, header, footer
When WS is connected (local_push or remote controller), tapping a preset
sends ae_mode:poster / ae_mode:oral to all connected devices so an operator
can reconfigure the whole room from one device.
ae_mode:{poster|oral} command handler added to handle_ws_recv() in
+layout.svelte — receives and applies the same preset on remote devices.
When on a Poster Session in Pres Mgmt, the Launcher nav link now appends
?iframe=true&launcher_menu=hide so the recipient opens a clean kiosk view
without the site header or left session panel.
- ae_comp__events_menu_nav: add events__launcher_extra_params prop; update
launcher_sess_qry to merge extra params after session_id
- session_page_menu: derive is_poster from type_code and pass the kiosk
params into the Launcher link
events/+layout.svelte:
the events nav is actually rendered. Adds pt-0 for the launcher case
(nav never rendered there; launcher manages its own header offset).
- pb-48 (fixed 12rem) → pb-[25vh] — scrolls ~25% of viewport below
last card on any screen size, scales properly on phone/tablet/desktop.
launcher/+layout.svelte outer wrapper:
- Static mt-4 replaced with conditional:
mt-12 when launcher header is visible (matches h-12 header height,
keeps content clear of the absolute overlay)
mt-2 when launcher header is hidden (minimal breathing room only)
- Result: iframe mode + launcher_header=hide → near-zero top dead space
Allows sharing a clean link that auto-configures display state on any
device — no manual setup required, ideal for tablet PWA kiosk deployment.
All four params are optional and independent. Values persist in
localStorage so they survive reloads; the URL param always wins when
present. Read inside the existing reactive URL-sync $effect, which
fires on every SPA navigation (unlike the root onMount which only
fires once on initial load).
Supported params:
?iframe=true/false — hide/show global sys & debug menus
?launcher_menu=hide/show — hide/show left session/location panel
?launcher_header=hide/show — hide/show 'Æ Launcher v3' header bar
?launcher_footer=hide/show — hide/show the status footer
Example clean poster-kiosk link:
/events/{id}/launcher/{loc_id}?session_id={sess_id}
&iframe=true&launcher_menu=hide&launcher_header=hide
- Add (pres_mgmt)/+layout.svelte with shared section wrapper (max-w-7xl mx-auto)
so all child pages center correctly on wide viewports
- Strip per-page outer <section> tags from session, presenter, location pages;
replace with inner <div max-w-7xl mx-auto> for detail constraint
- Restructure all page menus from flex-row to flex-col so nav bar occupies its
own row and options/actions sit in a separate justified row below — prevents
unwanted wrapping when nav is w-full
- Standardize Æ Core button visibility to edit_mode && manager_access across all
menus; update button style to ae_btn_warning for visual distinction
- Add w-full + justify-between to ae_comp__events_menu_nav outer div
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create launcher_session_view_posters.svelte — a touch-first card-grid
layout for Digital Poster sessions, designed for tablet/phone PWA use.
Layout:
- 1 column on mobile, 2 on sm, 3 on xl
- Each poster card: title (line-clamp-3) + presenter name/affiliation +
'Open Poster' action button at card bottom
- Poster code (or 1-based index fallback) badge in top-right corner
- Card active:scale-[0.98] for tactile touch press feedback
- Sticky compact session header strip with name, code, and poster count
- Optional 'Session Resources' strip for rare session-level files
- overflow-y-auto + grow so the grid scrolls; header strip stays fixed
Integration:
- launcher_session_view.svelte: import + delegate when type_code==='poster'
- launcher_file_cont.svelte: min-w-96 → w-full on poster button so it
fills its container (card or list row) without overflow on small screens
- WS open/close/zoom command pipeline unchanged (all in launcher_file_cont
and +layout.svelte which were not modified for the WS paths)
The event_session_type_code field on file objects is often null, causing
poster-mode files to fall back to 'oral' and render as download buttons
instead of modal openers.
- launcher_session_view: session-level files now use derived type_code
- launcher_presenter_view: add session_type prop (default 'oral'); parent passes type_code
- launcher_presenter_view_posters: hardcode poster/modal (only ever rendered in poster context)
- launcher_menu event/location files unchanged: no session context, field is the only signal
Session ID page: replace `container mx-auto` with `w-full` — removes the
Tailwind breakpoint max-width cap that was narrower than the outer layout's
max-w-7xl on smaller desktop viewports, and eliminates the double-padding
from nested container classes.
Presenter ID page: replace the conflicting `md:container items-center mx-auto
h-full min-w-full max-w-max` with a clean `w-full`. The `items-center` was
the main bug — in flex-col context it horizontally centered children (presenter
details, file upload form, file list) at their content width instead of
stretching them to fill the available space. Removes all of the conflicting
min/max width overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add modal_zoom_fit state (default: fit); resets on every new poster open
- Zoom/Fit toggle button + image double-tap on controller tablet
- Both zoom triggers send ae_zoom:fit/zoom over WS to remote display (local_push)
- ae_zoom: handler added to handle_ws_recv() for remote device
- Replace 3 scattered close buttons with single bottom control bar:
- [Zoom/Fit] always visible on controller
- [Close Both] sends ae_close WS + clears local modal (local_push only)
- [Back to List] clears local modal only; remote keeps showing current poster
- Bottom bar hidden on controller=remote (kiosk display-screen mode)
- native pinch-to-zoom via touch-action: pinch-zoom on img (no JS library needed)
- pb-14 on modal bodyClass prevents buttons from overlapping poster content
Adds a 🎨 button to the badge debug bar (edit-mode only) that cycles
through 5 SVG background patterns keyed by badge type color palette:
stripes, dots, diamonds, grid, swirls.
Swirl tile is 64×64 with corner-to-corner cubic bezier S-curves so
edges align seamlessly when tiled. Off by default; never prints.
- ae_comp__badge_obj_view_v2.svelte: removed all inline edit-mode logic
(floating Edit/Save/Cancel panel, placeholder list, input fields, save/cancel
functions). V2 is now purely a display component — editing happens in the right
panel (ae_comp__badge_print_controls.svelte) via liveQuery reactivity.
display_* values are now $derived directly from lq__event_badge_obj.
Fixes effective_badge_type_code CSS class (was always empty in previous v2).
- ae_comp__badge_print_controls.svelte: added "Print Badge" button at the top of
the panel. Increments print_count, fires window.print(), then navigates to badge
search. This is now the canonical print action for v2; the header "Print Now"
button is a shortcut that calls window.print() only (no count tracking).
Clarified $ae_loc.edit_mode comment — global AE Edit Mode is not a badge-specific
edit toggle; used here only to gate reprints.
- print/+page.svelte: added clarifying comments on is_edit_mode (global vs local)
and the header "Print Now" button (shortcut only, no count tracking).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds binary-search font auto-scaling for badge text fields, replacing
the character-count heuristic in v1. New files:
- action_fit_text.ts: Svelte action using binary search + MutationObserver
+ ResizeObserver. Pass null to disable (manual override mode).
- element_fit_text.svelte: Component wrapper with min/max/manual_size/
height/width props. height prop required for overflow detection to work.
- ae_comp__badge_obj_view_v2.svelte: Badge render using Element_fit_text
for name/title/affiliations/location in display mode. font_size_* props
default to undefined (auto-scale) instead of numeric defaults.
fit_heights derived object provides layout-aware section heights for
badge_3.5x5.5_pvc, badge_4x5_fanfold, and badge_4x6_fanfold layouts.
flex_justify() maps shorthand ('around','between','even') to CSS values.
Edit mode uses plain divs — inputs are never auto-scaled.
print/+page.svelte: Added v1/v2 toggle button in header. V1 preserved
as fallback. font_size_* passed as null (not ?? undefined) to v2 so
auto-scaling is active by default; manual override from print controls
still disables it per-field.
Docs: PROJECT__AE_Events_Badges_Review_Print.md updated with kiosk
workflow design intent, email address rule (always event_badge.email),
permission model alignment gap (TASK 4.0), and v2 implementation status.
TODO__Agents.md: completed items removed, badge polish tasks updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Generate and persist crypto.randomUUID() as controller_client_id on first
launcher load (events_loc is persisted so it survives page reloads).
Previously fell back to Date.now() on every reload.
- ae_open: handler now resolves presentation name via Dexie lookup chain:
file.for_id -> presentation.name, falling back to file.filename.
Remote modal now shows the presentation title instead of raw filename.
- launcher_file_cont: poster 'Open Poster' click now sends ae_open:event_file
command to remote display when controller=local_push and WS is connected
- +layout.svelte handle_ws_recv: ae_open: handler now looks up the file obj
from Dexie after setting modal__open_event_file_id, so the remote modal
has the hosted_file_id needed to render the poster image (was showing
'No image selected' on remote device)
- Add ae_auth_error writable store to ae_stores.ts
- Wire api_get_object, api_post_object, api_patch_object to set
ae_auth_error on 401/403 (browser-only guard, never fires SSR)
- Root layout watches ae_auth_error; only raises flag_expired when
a JWT is present (prevents false trigger on unauthenticated loads)
- Dismissible amber banner added to root layout (non-blocking, above content)
- Tested via debug menu trigger; banner fires and clears correctly
state_referenced_locally warnings in ae_idaa_comp__event_obj_id_edit_v2.svelte:
- lu_country_list and lu_country_subdivision_list $state runes were read in
top-level synchronous if/else blocks; moved into onMount
- Add onMount to Svelte imports
- Remove unused .field-richtext CSS selector
Remaining 32 warnings in 2 files are either:
- CSS @apply / @reference warnings from the CSS language service not understanding
Tailwind v4 at-rules (harmless, build works fine)
- Warnings in the legacy v1 edit form (no code references it)
Two state_referenced_locally warnings on data prop in
recovery_meetings/[event_id]/+page.svelte: reading a $props()
rune synchronously in a top-level if (browser) block only captures
the initial value.
Move the postMessage block into onMount (browser-only by nature);
remove the now-redundant 'browser' import.
- Update stale comment in menu_location_list.svelte: prop is already
$bindable(null), comment incorrectly said it was not
- Confirm cleanup_tmp_files is wired in launcher_background_sync.svelte
- Mark both items done in TODO__Agents.md
Rollup was splitting svelte/src/internal/client/runtime.js into a
different chunk from svelte/src/index-client.js, producing ~35 warnings
about untrack/tick re-exports leading to broken execution order.
Add manualChunks function to vite.config.ts to colocate all svelte
node_modules into a single 'svelte-vendor' chunk, keeping runtime.js
and index-client.js together.
- Remove vestigial try_cache param from generate_qr_code (never used in body)
- Remove vestigial try_cache from ae_core_functions: load_ae_obj_id__site_domain,
update_ae_obj_id_crud, update_ae_obj_id_crud_v2 (none referenced it in body)
- Add proper SWR pattern to load_ae_obj_id__sponsorship_cfg and
load_ae_obj_id__sponsorship; change defaults from false to true
- Change load_ae_obj_id__event_file default try_cache from false to true
(consistent with load_ae_obj_li__event_file)
- Change load_ae_obj_id__hosted_file default try_cache from false to true
(consistent with load_ae_obj_li__hosted_file)
- Remove stale try_cache arg from element_ae_crud.svelte caller
element_manage_event_file_li_all.svelte — also derives context_session_type_code
via Dexie chain (event_presentation → session, or event_presenter → presentation →
session) and passes it to element_manage_event_file_li. Fixes the button not showing
when viewing a presenter's files from the session view.
element_manage_event_file_li_direct.svelte — extends the Dexie chain to also handle
event_session (direct lookup) and event_presentation, not just event_presenter.
Both: correct API URL to /v3/hosted_file/ per backend agent's examples.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The endpoint is registered under the older hosted_file router at /hosted_file/{id}/convert_file,
not under the v3 actions router. Both list and table convert buttons were sending to the wrong path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v_event_file joins event_session only via event_file.event_session_id.
Files with for_type='event_presenter' have event_session_id=NULL on the
file record itself, so event_session_type_code is structurally always NULL
from the API for these files — no amount of refreshing can fix it.
Instead of relying on the file's event_session_type_code, derive the session
type in element_manage_event_file_li_direct via the Dexie chain:
presenter.event_presentation_id → presentation.event_session_id → session.type_code
Pass the result as context_session_type_code to element_manage_event_file_li,
which now checks EITHER the file's own event_session_type_code OR the context
prop against 'poster' to show the PDF→Image convert button.
Sessions are guaranteed in Dexie because the pres_mgmt layout loads
inc_session_li:true on every navigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
onMount fired before the parent presenter liveQuery resolved, so
link_to_id was undefined and the refresh was silently skipped.
Using \$effect makes the background refresh re-run once link_to_id
becomes available (after the presenter Dexie lookup completes),
ensuring event_session_type_code is written to Dexie and the
PDF→image convert button renders correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The presenter detail page loads files with try_cache:false, which fetches
fresh data from the API but does NOT write it to Dexie (by design in the
SWR implementation). The file list's liveQuery then reads stale Dexie
records that lack event_session_type_code, causing the PDF→image convert
button condition to silently fail for presenter files in poster sessions.
Fix: trigger a try_cache:true background refresh on mount in the direct
wrapper so fresh API data (with event_session_type_code='poster') is
persisted to Dexie and the liveQuery re-renders with the correct field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the convert button added to the table view (ae_comp__event_file_obj_tbl).
The list view (element_manage_event_file_li) is the primary Pres Mgmt UI
for managing event files per object (session, presenter, location, etc.).
Same conditions: edit_mode on, extension=pdf, event_session_type_code=poster.
Per-row status: idle → converting → done | error with retry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a per-row "Convert PDF → Image" button in ae_comp__event_file_obj_tbl.
Only shown when edit_mode is on, the file is a PDF, and the session
type_code is 'poster' — poster sessions need images in the Launcher modal
(which uses <img>, not a PDF viewer).
Calls GET /v3/action/hosted_file/{id}/convert_file (pdf2image, 3840px wide,
first page, saves as a new hosted_file linked to the same parent object).
Per-row status tracking: idle → converting → done | error with retry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Persistent stores grow and change over time. svelte-persisted-store deep-merges
old localStorage values with new defaults, so stale values (e.g. hash_prefix_length: 1)
silently survive schema changes and cause subtle bugs.
- src/lib/stores/store_versions.ts:
Single source of truth for AE_LOC_VERSION / AE_EVENTS_LOC_VERSION.
Side-effect on import: reads raw localStorage and wipes if __version mismatches.
Must be imported first in ae_stores.ts and ae_events_stores.ts so the wipe
happens before persisted() hydrates from localStorage.
- ae_stores.ts + ae_events_stores.ts:
Import store_versions as first import; add __version to persisted store defaults.
- documentation/TODO__Agents.md:
Added stores refactor task — both store files need a cleanup pass.
Bump AE_LOC_VERSION or AE_EVENTS_LOC_VERSION by 1 on breaking schema changes.
Non-breaking changes (new optional fields, default value tweaks) do not need a bump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- launcher/+layout.svelte: dead {:else if $events_slct.event_session_id} branch
(condition identical to preceding {#if}) replaced with correct
{:else if $events_slct.event_location_id} — shows "Select a session"
prompt when a room is chosen but no session is yet selected.
- launcher/[event_location_id]/+page.svelte: removed unreachable
handle_get_device_info() function (never called; pre-relay pattern
superseded by launcher_background_sync.svelte's run_device_heartbeat()).
Cleaned up now-unused imports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove tmp_shell_handlers.ts from SvelteKit repo — this was a draft of the
Electron main-process shell handler that was placed in the wrong repo. It used
ipcMain (Electron main-process only) and could never run in SvelteKit. Was not
imported anywhere but was misleading.
- Fix src/lib/electron/README.md:
- Correct broken doc link (was AE_EVENTS_LAUNCHER_NATIVE_INTEGRATION.md,
actual filename is PROJECT__AE_Events_Launcher_Native_integration.md)
- Mark electron_native.js as DEPRECATED — do not import (was described as
active "Bridge Logic", which was incorrect and confusion-causing)
- Add clear bridge architecture diagram showing the three-layer flow
- Note that aether_app_native_electron/ is the active Electron repo
- Update PROJECT__AE_Events_Launcher_Native_integration.md:
- Fix Layer 1 file path: aether_app_native/ → aether_app_native_electron/
- Section 5.3: Phase 5 actuators are all implemented, not "planned" — update
recording, display layout, power control, window control, wallpaper to reflect
actual implementation status; note update_app is a stub
- Section 7: Expand IPC whitelist to cover all relay functions including new
cleanup_tmp_files, system handlers, and full parameter signatures
- Document get_seed_config / get_jwt as intentionally not relayed to UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The event_device table has no hash_prefix_length column, so incoming_dev.hash_prefix_length
is always undefined from the API. The old merge logic preserved whatever was in localStorage,
which had a stale value of 1 from earlier testing — causing the background file sync to create
1-char cache subdirectories instead of the correct 2-char SHA-256 prefix dirs.
Background sync now consistently creates 2-char dirs matching launch_from_cache behavior.
If the API ever returns a hash_prefix_length, it will be used; otherwise the floor is 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- launcher_cfg_local_actions.svelte: add "Cache Maintenance" block
(native-only, gated by is_native && cache_root); number input for
max age in hours, "Clean Now" button with status feedback
- launcher_background_sync.svelte: read cleanup_tmp_max_age_hours
from events_loc.launcher store (default 24h) instead of hardcoded
1440 minutes; setting persists across sessions via persisted store
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- manifest.webmanifest/+server.ts: swap PUBLIC_AE_API_SECRET_KEY →
PUBLIC_AE_BOOTSTRAP_KEY (least privilege; endpoint only needs a
site-domain lookup, same as the bootstrap use case)
- electron_relay.ts: add cleanup_tmp_files() — runs `find ... -name
"*.tmp" -mmin +N -delete` via native run_cmd bridge
- launcher_background_sync.svelte: call cleanup_tmp_files() on mount
when is_native && cache_root are present (once per startup)
- AE__Permissions_and_Security.md: close Sev-1 audit language
- TODO__Agents.md: mark PUBLIC_AE_API_SECRET_KEY audit as complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New: menu_launcher_controls.svelte — bottom control bar for the launcher
sidebar with font size cycler (A / A+ / A−) and light/dark mode toggle,
always visible regardless of access level; visibility toggles (All Files /
All Sessions) moved here from launcher_menu.svelte and restyled to
preset-tonal-tertiary with descriptive title attributes
- launcher_menu.svelte — replace inline visibility-toggle block with
<Menu_launcher_controls />; add full header doc-comment describing
component structure and data flow
- menu_location_list.svelte — add header doc-comment covering purpose,
visibility rules, data flow, and the intentional non-bindable prop pattern
- documentation/TODO__Agents.md — mark font size cycler task complete
- RUN --mount=type=cache,target=/root/.npm for both npm install steps
Persists npm cache across builds so unchanged deps aren't re-downloaded
- deploy:staging now targets ae_app only (skip api/nginx rebuild)
- compose:down includes --profile database to remove mariadb/pma containers
and cleanly release ae_dev_net (fixes 'Resource is still in use' warning)
PUBLIC_AE_BOOTSTRAP_KEY replaces the hardcoded 'IDF68Em5X4HTZlswRNgepQ' in:
- src/routes/+layout.ts (site-domain bootstrap request)
- src/routes/testing/+page.svelte (trace agent key)
Added to .env.staging, .env.prod, .env.local (gitignored), and updated
.env.staging.default / .env.prod.default with XXXX placeholders.
Key can now be rotated independently from the main API secret key.
- core__crud_generic.ts: guard patch result logs (lines 246/252) with
if (log_lvl) — these fired on every successful patch call
- e_app_sign_in_out.svelte: already committed in previous round
- element_manage_hosted_file_li.svelte: already committed in previous round
All other console.log calls in launcher/lib files confirmed already guarded
via $B2 context check. Remaining unguarded logs are in event handlers
(fire on user action only, not hot render paths) or testing/admin pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- launcher/+layout.svelte: convert lq__event_session_obj from $derived to
$derived.by() so Svelte tracks event_session_id as a dependency; the old
pattern read the store inside the Dexie async callback where Svelte's
tracking is off, so the liveQuery never updated on session change
- ae_events__event_file.ts: fix hardcoded log_lvl: 2 in SWR fire-and-forget
background refresh (always-on debug logging on every cache hit) → 0
- e_app_sign_in_out.svelte: lower 6 call-site log levels (1×log_lvl:2,
5×log_lvl:1) to 0; sign-in runs on every page load
- element_manage_hosted_file_li.svelte: log_lvl:2 → 0 in refresh call;
remove log_lvl=1 assignment + debug block inside click handler; log_lvl:1
→ 0 in delete call
- AE__Performance_Guidelines.md: add 5 Svelte 5 runes rules covering
$derived.by() for reactive liveQuery, liveQuery purity, cheap equality
guards ($id+updated_on, ID-join, shallow_equal), untrack() requirement,
and log_lvl discipline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TODO__Agents.md: Mark QR code on badge front as done — ae_comp__badge_obj_view.svelte
already generates the QR via core_func.js_generate_qr_code() and renders it
inside a {#await qr_data_url} block on the badge face.
PROJECT__AE_combined_front_back_Docker.md: New reference document covering
the combined front+back Docker orchestration architecture (consolidated
notes from the session).
Set log_lvl to 0 in all pages and layouts that had it left at 1 or 2
from development. Also remove two hardcoded `log_lvl = 2` overrides
inside function bodies in reports_files.svelte and
reports_presenters.svelte that were forcing verbose output regardless
of the module-level setting.
Affected: launcher location page, leads pages (2), pres_mgmt reports (2),
presenter +page.ts, IDAA layouts (2), IDAA archives, IDAA recovery
meetings page, journals pages (2).
Multiple failures caused by the SWR background fetch pattern and
collapsed UI sections:
1. IDB settle wait: add waitForFunction that polls IndexedDB for
tmp_sort_1 on the event record. tmp_sort_1 is only written by
_process_generic_props, so its presence signals the background
API fetch has completed and no further liveQuery re-fires will
overwrite form inputs the test is about to fill.
2. JSON.parse for _json fields: update_ae_obj_v3 auto-serializes any
key ending in _json to a JSON string before sending the PATCH.
Tests now parse location_address_json, attend_json, and
contact_li_json from the captured body before asserting field values.
3. Contact 2 section: collapsed by default when contact_2.full_name
and email are null. Collapsed state renders type='hidden' inputs
which Playwright's fill() rejects. Add click on the 'Contact 2
(Optional)' toggle before filling those fields.
4. Admin Options section: same collapse pattern. Add click on the
'Admin Options' toggle before filling status/sort/group/hide fields.
5. Increase suite timeout to 60 s: open_edit_form awaits real lookup
API responses (pass_through_lookups=true) which can take 20-25 s
on slow network, leaving no margin at the default 30 s limit.
The Zoom button onclick initialises attend_json.zoom. However the
background SWR list fetch (load_ae_obj_li__event in +layout.ts) can
overwrite $idaa_slct.event_obj with a fresh IDB record where
attend_json = {} (no zoom key), even if the Zoom button was already
clicked.
Without the guard, the $effect that rebuilds the Zoom full URL and the
template bindings below the Zoom fields access attend_json.zoom.passcode_enc
on an undefined object, throwing a TypeError and crashing the component.
Fix: add `&& $idaa_slct.event_obj.attend_json?.zoom` guard to both the
$effect condition and the {#if} block that renders the Zoom input fields.
Replace $derived(liveQuery(...)) with $derived.by(() => { const id = ...; return liveQuery(...) })
for lq__event_presenter_obj and lq__auth__event_presenter_obj in the
session page.
$derived.by() captures only the specific ID at derivation time, so
unrelated store changes do not recreate the observable. Plain $derived
closes over the entire $events_slct store object, causing unnecessary
observable recreation on any store mutation.
ae_events__event.ts: Add legacy flat address fields (address_name,
address_line_1, address_city, etc.) to properties_to_save. Events
predating location_address_json still return these flat fields from
the API; they must survive an IDB round-trip without being stripped
by _process_generic_props, or the edit form's fallback reads return
undefined.
launcher_background_sync.svelte:
- Move file_sync interval into loop_info $state alongside other
intervals; load from cfg/device config with fallback (30 s). Keeps
all intervals in one place for UI display consistency.
- Align onMount fallback values to match loop_info defaults.
- Add sync_paused mid-loop break: long sync cycles now honour a pause
request per-iteration rather than waiting for the entire batch.
JSON.stringify on large store objects (ae_loc, ae_api, slct, event lists)
was running on every navigation, comparing serialized strings of potentially
deep objects. Replace with targeted comparators:
- Root +layout.svelte: add shallow_equal() helper — O(n keys) key-by-key
identity check instead of O(serialized bytes). Used for ae_api, ae_loc,
and slct sync guards.
- Launcher +layout.svelte: ID-list join-compare for event_location_obj_li
and id_li__event_location (O(n) string vs O(n*m) stringify). Refactor
liveQuery closures to be pure (data-only, no store reads/writes inside
the async Dexie context where Svelte reactivity tracking is undefined).
Move store sync into separate $effects that compare updated_on + id
(O(1)) or ID-join (O(n)) rather than full object serialization.
Fixes a build warning about missing .svelte-kit/tsconfig.json that
appears when building inside Docker. Running npx svelte-kit sync
pre-generates the required SvelteKit scaffolding before the main
build step.
Also update README title to reflect Svelte v5 and trim a trailing
whitespace in the environment handling section.
- menu_location_list.svelte: mark slct_event_location_id as $bindable(null) to
resolve Svelte 5 compiler warning (bind:value used on non-bindable prop)
- TODO__Agents.md: audit and close resolved launcher items:
- Location select auto-load bug: fixed via $derived.by() liveQuery pattern
- Session Search button visibility: was never a real bug, hardcoded false
- Dark mode select fix: already applied via app.css color-scheme rules
Add html.dark/html.light color-scheme rules to app.css so native controls
(select dropdowns, scrollbars, date pickers) follow the app's class-based
dark mode rather than the OS theme.
- Replaced manual rsync/npm_deploy workflow with multi-stage Docker builds.
- Added Dockerfile and .dockerignore for staging and production environments.
- Added 'deploy:staging' and 'deploy:prod' scripts to package.json.
- Updated README.md with new deployment instructions.
Three new Firefly-family themes following the AE_Firefly design system:
- AE_Firefly_SteelBlue: metallic steel blue primary (~214°), burnished gold
secondary, cobalt navy tertiary, chrome silver surfaces
- AE_Firefly_Indigo: deep indigo primary (~266°), violet secondary, dusty
rose tertiary, velvet slate surfaces
- AE_Firefly_Rainbow: coral-red primary (~15°), emerald green secondary,
rich violet tertiary, sunrise cream surfaces (spans the visible spectrum)
All variants share consistent semantic colors (success/warning/error) with
AE_Firefly for cross-theme recognizability. All WCAG 2.1 AA compliant.
Also adds URL param support for theme switching:
- ?theme=AE_Firefly_SteelBlue&theme_mode=dark
- Params applied to ae_loc (persisted), then silently removed via replaceState
Replace the old float-right Show/Hide toggle button + separate collapsible
div with an inline caret-button heading inside a consistent section card
(bg-surface-100-900, border-error-400) — matching the recovery meetings v2
form style. All four forms updated: Post, Post Comment, Archive, Archive
Content. Existing class:hidden behaviour preserved so FormData is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin Options are rarely changed; collapsing by default reduces visual
noise on the long edit form. Hidden inputs preserve status/enable/hide/
priority/sort/group values when the section is collapsed so a save never
silently resets admin fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Save Changes button at top of form so users don't scroll to the
bottom to save; existing meetings only (mirrors v1 behaviour)
- Collapse Contact 2 subsection behind a toggle; auto-expands when the
meeting already has Contact 2 data saved
- Add hidden inputs when Contact 2 is collapsed so FormData preserves
existing contact info rather than overwriting with nulls on save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full payload-verification test suite for ae_idaa_comp__event_obj_id_edit_v2.
Root cause fixed: $ae_loc.lu_time_zone_list empty at mount caused Svelte 5 to
render <input type=text name=timezone required value=''> instead of the <select>
branch. HTML5 required validation silently cancelled onsubmit with no JS error
and zero network activity — waitForRequest timed out with no obvious cause.
Fix: pre-seed lu_time_zone_list in addInitScript so the <select> branch renders
on first mount with a valid value already set.
Key patterns established:
- setup_idaa_auth(): pre-seeds ae_loc + ae_idaa_loc in localStorage via
addInitScript; includes lu_time_zone_list and window.__ae_test_mode = true
- setup_api_mocks(): selective pass-through flags for lookups and site_domain
- open_edit_form(): waitForFunction guards for name field, country lists, and
the timezone required field before any interaction
- capture_patch_body(): registers waitForRequest before click, awaits after
README.md updated with deep-dive section covering:
- HTML5 form validation silent block and how to diagnose it
- Svelte 5 one-time value= bind trap
- addInitScript store pre-seeding pattern
- __ae_test_mode email suppression
- waitForFunction patterns for reactive state
- Route mock strategy (pass-through vs fixture)
Add a guard at the top of send_email() that checks globalThis.__ae_test_mode.
If truthy, logs a suppression message and returns null immediately so no HTTP
request is made. This prevents real emails being sent when Playwright tests
exercise components that call send_staff_notification_email() after a successful
save. Activate by setting window.__ae_test_mode = true in addInitScript.
- [post_id]/+page.ts: remove await on post load (same pattern as archives);
liveQuery renders immediately, replace error(404) with console.warn.
- ae_idaa_comp__post_options: add in-flight creating guard + spinner on Create
New Post button to prevent double-submit; remove ~15 lines of dead commented
code left over from previous refactors.
- ae_idaa_comp__post_obj_id_view: remove confirm() dialog before opening the
Add Comment form — no need to confirm an intent to type a comment.
- ae_idaa_comp__post_comment_obj_id_edit: remove redundant block that manually
injected post_id into the payload for new comments; post_id is already
handled correctly by the form payload builder.
- +page.ts: remove await on archive load so navigation is not blocked;
liveQuery renders from Dexie cache immediately, API result updates reactively.
Replace error(404) with console.warn — soft failure is correct for IDAA.
- ae_idaa_comp__archive_obj_li: add Create New Archive button (trusted+edit_mode
only) with in-flight spinner and creating guard to prevent double-submit.
Layout adjusted to justify-between to accommodate the new button.
- ae_idaa_comp__archive_content_obj_li: add Create New Archive Content button
with same spinner/guard pattern; pre-populates original_timezone from parent
archive so staff do not need to re-select it for every content item.
Two bugs fixed:
1. scroll_to handler scrolled to page top (0,0) instead of the iframe's
position in the Novi page. The iframe sits below Novi's own header/nav,
so the user ended up looking at the Novi header instead of the iframe
content after navigation. Fixed to use getBoundingClientRect() to scroll
to 20px above the iframe's actual document position. Also added the
missing scroll_to handler to idaa_novi_iframe_archives.html (it had none).
2. Parent URL not updating with event_id/post_id/archive_id on navigation.
Detail pages sent postMessage using $idaa_slct.<id> (the store), which
is still null at synchronous init time — the $effect that populates it
runs later. Fixed to read from data[data.account_id].slct.<id> directly
(set by the +page.ts load function from URL params before render).
Also added afterNavigate to idaa/+layout.svelte to send scroll_to on all
client-side navigations, covering cases the per-page blocks miss (e.g.
navigating back to the list view).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, IDAA iframe access relied on trusting URL params (uuid, email,
full_name) passed from Novi — any 36-char string granted authenticated access
with no actual verification.
The (idaa)/+layout.svelte now performs an async Novi API call on every UUID
load to verify the UUID exists, fetches name/email directly from Novi (cannot
be spoofed via URL), and sets $idaa_loc.novi_verified on success.
All-or-nothing: if novi_idaa_api_key is absent or the call fails, access denied.
- ae_idaa_stores.ts: add novi_verified boolean field to idaa_loc
- (idaa)/+layout.svelte: async UUID verification with spinner to prevent
Access Denied flash; permission upgrade-only strategy preserved
- video_conferences/+page.svelte: skip duplicate Novi member details call if
layout already verified ($idaa_loc.novi_verified check)
- iframe HTML files: remove browser-side Novi API fetch and email/full_name
params; pass only uuid; add README/START/STOP/WARNING comments for client
staff; fix iframe-before-script DOM ordering bug
- documentation: CLIENT__IDAA_and_customized_mods.md updated with full
verification flow, site_cfg_json fields, permission table, access gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V3 CRUD returns 'id' as the random identifier, not 'person_id_random'.
The person check and assignment were using the old field name, causing
the 'no person record' alert even when the lookup returned valid data.
Now checks person_rec.id ?? person_rec.person_id_random as a fallback
for backwards compatibility.
- Check user_response?.detail (FastAPI standard) before user_response?.error
- Distinguish null response (network/server error) from bad credentials
- Remove silent console.error-only path; user now always sees a message
- Fix misleading 'auth_ae_obj__username_password' label in user_id+key flow
- Clarify 'no person record' message to suggest contacting administrator
- Simplify success log messages (remove dead commented-out code)
The /user/authenticate endpoint returns 'user_id' not 'user_id_random'.
Both auth flows (user_id+auth_key and username+password) were checking
user_response?.user_id_random, which was always undefined, causing the
user_id to never be set and falling through to the email lookup fallback.
Fixed both .then() handlers to check user_response?.user_id and assign
user_obj.user_id.
The three liveQueries that depend on $events_slct.event_location_id were
plain liveQuery() calls, not $derived.by(() => liveQuery(...)). This meant
Svelte store changes did NOT cause them to re-run.
Root cause of the 'hung' bug:
- On initial load at /launcher (no location in URL), id = null
- Dexie watches the event_location_id = null index range
- User selects a location (or navigates to /launcher/{id}): store updates
- Sessions for the real location are in a DIFFERENT Dexie range
- Dexie never fires because the null range was never touched
- If sessions are already in IndexedDB cache (no new DB write), the list
stays permanently frozen at []
Fix: convert lq__event_session_obj_li, lq__location_event_file_obj_li, and
lq__event_location_obj to $derived.by(() => liveQuery(...)). When
event_location_id changes, $derived.by creates a new Observable targeting
the correct location, which immediately queries Dexie for existing cached
data and then watches that range for further changes.
Also: remove the .reverse() before .sortBy('name') on the session query —
.sortBy() always re-sorts so .reverse() before it was a no-op.
Onsite operators may have the sys menu locked/unavailable. This button
in the always-visible launcher footer gives them direct A / A+ / A−
control that cycles $ae_loc.font_size_mode, which the root layout DOM
effect picks up and applies as html.font-size-* class.
- event_page_menu: set events__session_search=false — the Session Search nav
link was redundantly appearing on the Session Search page itself
- element_manage_event_file_li: replace hardcoded gray hover colors with
theme-aware surface tokens (hover:bg-surface-100-900, border-surface-200-800)
and add transition-colors; fixes light-on-light in dark mode for the file
list table rows and Event File Purpose select element
- font size cycler (default → larger → smaller → default):
- ae_stores: add font_size_mode: 'default' to ae_loc defaults
- app.css: html.font-size-larger (112.5%) and html.font-size-smaller (87.5%)
- +layout.svelte: DOM effect applies/removes font-size-* class on <html>
- e_app_sys_menu: compact A / A+ / A− button cycles the mode
Root cause: flex-row flex-wrap on the session header caused the datetime
and name to compete for the same row. Long session names (up to 300 chars)
wrapped onto 2-3 lines while short names stayed 1 line, making the header
jump in height every time the operator switched sessions.
Fix:
- header: flex-row flex-wrap -> flex-col; datetime and name are now
always on separate rows, header height is predictable in both cases
- h2 name: shrink -> grow line-clamp-2 min-w-0; height is always exactly
2 lines, never less, never more; full text accessible via title attribute
- code badge: added shrink-0 so it is never squeezed by a long name
- removed justify-between/justify-end conditional classes (no longer relevant)
- Section 508: title attribute on h2 provides full text for screen readers
- Remove dead .field_editing_wrapper CSS rules from element_ae_crud_v2.svelte
(template migrated to field_editing_wrapper_v2 with Tailwind)
- Fix TS error: use optional chaining on person_obj key in ae_comp__person_obj_tbl.svelte
- Fix state_referenced_locally: wrap data.user init with untrack() in users/[user_id]/+page.svelte
- Replace misused <label> with <span> for visual section headings (a11y) in:
launcher_cfg_native_os.svelte, launcher_cfg_health.svelte,
launcher_cfg_local_actions.svelte, launcher_cfg_template.svelte
Fixed all 27 remaining instances across 19 files. Keys used:
- Object ID fields where available (e.g. account_id_random, event_file_id)
- index for logger lists with no reliable unique key
- Property name for Object.entries() loops
Miscellaneous small changes to events (badges, launcher, leads, pres_mgmt,
settings), journals, reusable elements (crud, field editor), app components,
core components, and test README. Mostly 1-2 line changes per file.
- ae_idaa_stores.ts: update default novi_admin_li UUID; add staff UUID to
novi_trusted_li hardcoded defaults.
- +layout.svelte (idaa): only overwrite admin/trusted lists from site_cfg_json
when the list is non-empty, so hardcoded defaults are never silently cleared.
Remove $ae_loc.iframe requirement for 'authenticated' access level — the
presence of a valid Novi UUID in the URL is sufficient proof regardless of
whether the iframe flag is set yet.
- app.css: add @custom-variant dark so Tailwind v4 respects .dark class
on <html> instead of always following OS prefers-color-scheme.
- app.html: remove hardcoded class="light" (now set dynamically).
- +layout.svelte: toggle .dark/.light on <html> when ae_loc.theme_mode changes.
- e_app_theme.svelte: related theme toggle changes.
- Fix site_domain mock to return array { data: [mock_site_domain] } so
ae_api.headers['x-account-id'] gets a valid 11-char account_id.
Previously returned { data: {} } causing layout to fall back to 'ghost'
(5 chars) and the real API PATCH rejected the request with 422.
- Add integration test describe block (Real Backend Save) with
pass_through_event_patch option to let PATCH reach the real API.
- Extract localStorage injection into setup_idaa_auth() helper.
- Fix test name: 'sends PUT' → 'sends PATCH' (the API uses PATCH).
- All 14 tests pass.
13 tests covering: form render, form sections, field names/types,
weekday checkboxes, timing inputs, contact fields, address fieldset
visibility, virtual checkbox, text input, and PATCH API submission.
All tests pass (13/13). Fully mocked — no real backend required.
Fixed three issues in ae_comp__badge_obj_view.svelte:
1. Bug: {#await} block used src={qr_data_url} (the Promise) instead of src={result}
(the resolved data URL) — QR image never displayed correctly on badge front.
2. Layout: .special div had no flex context; added flex-row + items-end so the QR
sits at the bottom-right of the body section via ml-auto.
3. Cleanup: removed dead second QR block that awaited event_badge_qr_id_get_promise
(always null), which could never render.
Also marks CRUD v2 refactor as complete in TODO__Agents.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously buttons were opacity-0 until hover — invisible even in edit mode.
Changed to opacity-20 base so users can see which fields are editable, opacity-100 on hover.
Matches the behavior in element_data_store_v3.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix layout shift on edit_mode toggle: always render the edit button
(using invisible/pointer-events-none) so the flex container doesn't
reflow when edit_mode is toggled on/off.
- Fix 'store.set is not a function' crash: remove $bindable() from
current_value. The component is SWR-first; after a successful PATCH
liveQuery updates the prop from Dexie. Trying to write back to a
readonly liveQuery-derived prop caused the crash.
- Fix stale display after save: add has_optimistic flag + display_value
derived. After a successful PATCH, display_value shows draft_value
immediately without waiting for liveQuery. Cleared automatically when
current_value catches up, or on cancel/re-open of edit mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. launcher_presentation_view: accept session_type prop from parent
instead of relying on event_session_type_code on file objects (which
is not reliably populated in Dexie). Use session_type to correctly
set hide_meta, session_type, and open_method on the file container.
2. launcher_session_view: pass session_type={type_code} to
Launcher_presentation_view; restore Launcher_presenter_view_posters
in the presenter list for poster mode (hide_name=true). Some events
store poster files at the PRESENTER level (for_id=event_presenter_id)
— particularly group/company presenters — so both paths must render.
3. launcher/+layout.svelte: fix poster modal image 403. The img src was
using the event_file download endpoint which requires auth headers a
plain img tag cannot send. Switched to the hosted_file endpoint with
key=account_id, which is browser-compatible. Also guarded on
modal__event_file_obj.hosted_file_id for safer access.
4. launcher_cfg_screen_saver: rename section title to 'Poster Screen Saver'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues in poster session view:
1. 'No files for this presenter' message always showed because
file_count on the presenter is 0 — poster files are attached to
the presentation, not the presenter. Removed the misleading message
from launcher_presenter_view_posters.svelte with an explanation.
2. launcher_presentation_view.svelte was not passing hide_meta, so
the OS toggle, date badge, and file size always rendered below the
poster button. Added hide_meta={true} for poster files to match the
clean display already used in launcher_presenter_view_posters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
native_device can be undefined during initial load (before device config
is fetched). The select bind:value was doing a hard property access that
threw a TypeError crash when native_device was null/undefined. Wrapped
in {#if $ae_loc.native_device} to defer rendering until it's available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The $effect in launcher_presentation_view.svelte was calling
load_ae_obj_id__event_presentation() on every prop update. The SWR
pattern in that function always fires a background Dexie write, which
triggered the upstream liveQuery, which updated the prop, which
re-ran the $effect — creating an infinite loop that saturated the API
and crashed the browser tab within 30-60 seconds.
Fix: guard on last_loaded_id so the API call only fires when the
presentation ID actually changes, not on every downstream re-render.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SWR pattern always fires a background API call on cache hits, so the
15s session interval created a continuous API stream even when data was
fresh. Increased all defaults: session 15s->60s, location/device 30s->60s,
presentation/presenter 45-60s->120s.
Added sync_paused guard to all six refresh functions and a Pause/Resume
toggle in the Sync Timers config section (visible without edit mode) so
testing and troubleshooting don't require a full reload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
type_code was a prop in launcher_session_view that was never passed by
the parent layout, silently killing all poster-specific code paths
(sort order, section labels, poster presenter component, inline
presenter display). Fixed by deriving type_code directly from the
session LiveQuery object already present in the component.
Also adds a poster icon badge to the session list so poster sessions
are visually distinct from oral sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Disabled items are treated as functionally deleted for all end clients
(including Trusted Access staff). Only Manager + Edit Mode should see
Show/Hide Disabled controls — previously using administrator_access and
missing edit_mode gate in BB and archives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add keyed {#each} to recovery meeting list and exhibit tracking list
to satisfy Svelte's each-block-key lint rule and ensure correct DOM
reconciliation on list updates
- Gate Zoom/Jitsi copy-to-clipboard buttons behind manager_access in
both the recovery meeting list view and single meeting detail view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- e_app_access_type: reset checked_passcode on clear so same passcode
can be re-entered without a page refresh (guard was blocking re-entry)
- element_data_store_v3: wire display prop to wrapper CSS style;
gate "not found" diagnostic to administrator+ or trusted+edit_mode;
public/anonymous visitors no longer see missing block warnings
- +page.svelte: add manager_access exception to header/content class_li
so managers can see "not found" diagnostics (matches footer pattern)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migrate event_exhibit and event_exhibit_tracking CRUD to V3 API (parent_type/child_type params).
- Implement Element_qr_scanner_v3.svelte: A Svelte 5 / Runes component using html5-qrcode with auto-start and unique viewfinder IDs.
- Integrate QR Scanner v3 into ae_comp__badge_search.svelte and lead capture.
- Refactor Exhibitor Leads UI:
- Add 'Rapid Scan' vs 'Qualify Mode' toggles for efficient lead capture.
- Upgrade ae_comp__lead_detail_form.svelte to support new question/response schema with backward compatibility.
- Implement 'Sign Out of Booth' functionality in exhibit management.
- Optimize lead detail layout for mobile readability and high information density.
- Fix component prop sync for event_id and exhibit_id.
- UI/UX refinements: standardizing icons (SquarePen), cleaning up unused imports, and improving responsive states.
- Create ae_comp__badge_print_controls.svelte: A fixed-right-edge panel for per-field accordion controls, font size adjustments, and inline editing.
- Refactor print/+page.svelte to integrate the new controls panel and standardize font size state management via $bindable() props.
- Update ae_comp__badge_obj_view.svelte and ae_comp__badge_review_form.svelte to correctly sync badge_type_code_override and badge_type_override.
- Improve badge_type_name derivation logic to prioritize staff overrides.
- Hide unused receipt/ticket sections in badge view pending future redesign.
- Update documentation (PROJECT and TODO) to reflect completion of Task 3.
Two compiled layout CSS files in src/lib/ae_events/badges/css/:
- badge_layout_epson_4x5_fanfold.css — 4"×5" per side, Epson ColorWorks C3500
fanfold duplex; preferred for general conference use (ISHLT, demos)
- badge_layout_zebra_zc10l_pvc.css — 3.5"×5.5" PVC card, Zebra ZC10L,
single-sided (pair with duplex=0 on template)
Rules scoped under [data-layout="..."] on the wrapper — beats Tailwind utility
class specificity without !important. Vite hot-reloads these in dev.
Badge component: add data-layout attribute from template.layout field; import
both layout CSS files (falls back to Tailwind 4"×6" defaults if layout unset).
Print page svelte:head: inject @page paper-size rule per layout code
(@page cannot use attribute selectors so it must be dynamic). Also wire
style_href as a <link> for per-event external client branding CSS.
db_events.ts Badge_template interface: add style_href and duplex fields.
MODULE doc: update layout codes table with badge_4x5_fanfold entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add duplex + style_href to properties_to_save so they're cached in IDB
- Derive show_badge_back: true when duplex is null/unset (safe default for
existing templates) or 1; false when duplex=0 (single-sided, e.g. Axonius
NYC using Zebra ZC10L PVC cards)
- Wrap entire badge_back section in {#if show_badge_back} so the DOM node
is fully removed rather than just hidden — cleaner than a CSS class approach
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static `badge_type_code_li` array (hardcoded ISHLT 2024 types) with
`$derived.by()` that parses `$lq__event_badge_template_obj.badge_type_list` JSON.
Each event's template defines its own badge type set — the component now reflects
that correctly. Falls back to `[]` on missing/invalid JSON (hides the type select).
Also add MODULE__AE_Events_Badge_Templates.md documenting template field reference,
external CSS approach, layout codes, duplex field plan, and standard template setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ae_comp__badge_review_form.svelte:
- Full implementation of the badge review form (Task 1)
- Editable fields gated by access level (attendee / trusted / admin)
- Save/cancel with change detection, override revert buttons
- QR code display (hover zoom + click expand)
- Print status section, options/tickets, T&C block
- HTML rendering for name/title/affiliations/location fields
- Accessibility font-size toggle (text-2xl ↔ text-4xl)
- Help modal (Flowbite) with 6 sections
- Local edit mode — never writes to $ae_loc.edit_mode
ae_events__event_badge.ts:
- Add missing fields to properties_to_save so they are persisted to IDB:
pronouns_override, phone, phone_override, registration_type(_code/_override),
allow_tracking, agree_to_tc, other_1-8_code, ticket_1-8_code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PROJECT doc: Task 2 status → complete (v1); added implementation
details, default px values table, and note on future mm/inch iteration
- PROJECT doc: noted the review page field list bug fix (commit 011fc19a)
- MODULE doc: added "Recently Completed" section above Still Needed;
cleared Badge Review Form and Print Font Controls from HIGH PRIORITY list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add optional font_size_{name,title,affiliations,location} px props to
ae_comp__badge_obj_view; when set, replaces auto inch-based sizing with
an inline style so the caller controls the value
- Add screen-only (print:hidden) font size control panel to print page:
four [−] [value] [+] [↺] rows, step 2px, null = auto (default)
First + click activates at a sensible default that approximates the
auto-sized inch values; ↺ button resets back to auto mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
default_authenticated_fields was missing: pronouns_override, phone_override,
allow_tracking, agree_to_tc — so attendees couldn't edit those fields even though
the form rendered inputs for them.
default_trusted_fields had 'email' and 'badge_type_code' instead of 'email_override'
and 'badge_type_code_override', causing the can_edit() check in handle_save() to
silently drop those changes. Also added the full trusted field set: registration_type,
other_1-8_code, ticket_1-8_code, hide, priority, notes.
Also removed unused lucide imports (ShieldCheck, User, UserCheck) left over from
the removed access level indicator block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Badge search results list (ae_comp__badge_obj_li):
- 4 action buttons per row: Print, Review (nav link), Copy Link (clipboard), Email Link
- Visibility rules: unprinted-only for non-edit mode; all non-hidden for trusted+edit
- Plain name display (User/EyeOff icon) — name is no longer a print link
- Obscured email for non-trusted users
- Debug row (ID, CR, UP, PC, FP, LP) in edit mode
- All icons converted to Lucide (Font Awesome removed)
Badge print page (/print):
- 3 header action buttons: Print Now, Review (nav), Email Link
- Removed old [badge_id]/+page.svelte placeholder (moved to trash)
- Added is_trusted, is_edit_mode, print state derived vars
- "Already printed Nx — last [timestamp]" warning inline with name
- Removed unused imports (browser, onMount, events_slct)
Badge review page (/review):
- 3 header action buttons: Print (nav), Copy Link (clipboard), Email Link
- Added events_loc for email placeholder + title event name
- Added is_edit_mode, print_count, is_printed, copy_status
- FA icons replaced with Lucide (ShieldCheck, UserCheck, User)
- Title now includes event name (was missing)
Infrastructure:
- print/+page.ts and review/+page.ts added (non-blocking badge loaders)
- ae_comp__badge_review_form.svelte stub created (fields pending)
- Fixed: components no longer write to $ae_loc.edit_mode (critical bug)
Docs:
- NEW: AE__Permissions_and_Security.md — full permissions hierarchy reference
- NEW: PROJECT__AE_Events_Badges_Review_Print.md — agent task brief for review form + print font controls
- UPDATED: MODULE__AE_Events_Badges.md rev 5 — field permissions spec, header buttons, still-needed list by priority
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLIENT__IDAA_and_customized_mods.md: New comprehensive doc covering IDAA
architecture, all 4 submodules (Archives, BB, Recovery Meetings, Jitsi),
Novi UUID auth system, permission levels, state stores, iframe integration,
and testing requirements. Reverse-engineered from source 2026-02-26.
- MODULE__AE_Events_Badges.md: trailing whitespace only
- tests/README.md: blank line only
Describes which object types are always-flat (never nested in URL) for
ALL operations, vs event sub-objects which use nested paths for mutations
but flat paths for all reads (GET, list, search, delete).
Always-flat objects:
- Core: account, activity_log, address, contact, hosted_file, organization,
page, person, site, user
- Other: archive, event, journal, post
Event sub-objects (event_badge, event_session, etc.) use nested
create_nested_obj_v3 / update_nested_obj_v3 for POST/PATCH, but flat
paths for everything else.
Includes Playwright mock URL patterns for each operation type.
- AE__Architecture.md: Add section 7 -- Runtime Environment: Browser vs Electron.
Electron is ONLY for Events Pres Mgmt Launcher. Badge printing (and everything
else) works via standard browser window.print(), no Electron needed.
- MODULE__AE_Events_Badges.md rev 3: Update print section to browser-first approach,
remove Electron from print workflow, add two new test lessons (flat API URLs,
CSS attribute vs DOM property), mark all tests passing.
- TODO__Agents.md: Add completed data integrity test fixes item, add window.print()
wiring to upcoming tasks, expand input field audit note.
- Badge search mock: was checking nested URL /v3/crud/event/{id}/event_badge/search
but search_ae_obj_v3 uses flat path /v3/crud/event_badge/search
- Template list mock: was checking nested path, fixed to flat /v3/crud/event_badge_template/
with for_obj_id query param (matches get_ae_obj_li_v3 behavior)
- Badge objects: add _random ID fields (event_badge_id_random, id_random, event_id_random)
required for Dexie IDB processing
- Template edit assertion: input[value*=...] CSS checks HTML attribute not DOM property;
Svelte bind:value sets DOM property only — fix to use getByLabel('Template Name')
- Relax null body check: the debug panel footer can contain 'null' legitimately
- Add data-testid to badge edit/save/cancel/print buttons and professional title input
- Fix API mock routes (badge GET uses /v3/crud/event_badge/{id}, not nested)
- Add badge_template mock so badge view renders
- Add event_badge_id_random and id_random fields to all mock responses
- Split workflow test into its own test() instead of being inline in beforeEach
- Use data-testid selectors throughout for stability
- Split search query by spaces
- Apply AND logic: all words must match
- Single word: LIKE '%word%'
- Multi-word: LIKE '%word1%' AND LIKE '%word2%'
Example: 'scott idem' now searches for both 'scott' AND 'idem'
Previously searched for literal 'scott idem' phrase which failed.
Fixes search bug discovered during Playwright test development.
- Simulates complete check-in: navigate → search → view → edit → print → return
- Mocks V3 API for event, badge search, and PATCH operations
- Tests override field editing (professional_title_override)
- Documents future attendee review feature (email link workflow)
**STATUS: WIP** - Search results not displaying in test environment
- API mocks configured correctly
- IDB cleared properly, schema managed by app
- Store initialization includes qry__remote_first flag
- Issue: Badge list not rendering after search (timing or store issue)
- Screenshot saved to test-results/ for debugging
- Added testing_site_id, testing_site_domain_id, testing_fqdn, testing_person_id
- Created mock_site_domain object matching +layout.ts expectations
- Updated minimal_v3_mocks.ts to use mock_site_domain
- Fixes 'Domain Not Registered' overlay in Playwright tests
- All test IDs now documented with comments from tests/README.md
CRITICAL FIX (same pattern as event_file fix):
- search__event_badge(): Now returns processed_obj_li instead of unprocessed result_li
- load_ae_obj_id__event_badge(): Returns processed object after IDB save
- load_ae_obj_li__event_badge(): Returns processed list after IDB save
- create_ae_obj__event_badge(): Returns processed object after IDB save
- update_ae_obj__event_badge(): Returns processed object after IDB save
Pattern: All functions now return what was actually saved to IDB (processed data)
This ensures consistency between API return values and IDB cached data.
TEST IMPROVEMENTS:
- Add full_name field to badge mock data (not just full_name_override)
- Ensures mock data matches real API structure
- Fix session detail page params not being passed to component (causing undefined session_id)
- Fix wrapper components discarding API enriched data by re-fetching from IDB
- Fix event_file, event_session, event_presenter wrappers to preserve API data
- Add optional chaining for hash_sha256 field to prevent undefined crashes
- Fix search__event_file to always process API results before returning
- Ensures hosted_file_size -> file_size field mapping for reports
- All pres mgmt reports (files, sessions, presenters) now work on cold-start
Project-specific context file for Claude Code containing:
- Critical privacy/business rules (IDAA, Journals)
- Mandatory workflows and tech stack
- API patterns and key documentation references
- Active issues tracking
Complements global ~/.claude/CLAUDE.md with SvelteKit-specific guidance.
New test validates that presentations AND presenters render on first
navigation when IndexedDB is empty (cold-start scenario).
Test Configuration:
- Demo Session: DOW3h7v6H42 (703) 'How To Do Things'
- Demo Presentation: 7U2eXSjR6H4 (1670) 'Build a House'
- Demo Presenter: gT-hxnifb-0 (2202) 'Bob The Builder'
Tests:
1. UI visibility: Session name, presentation, and presenter all visible
2. IDB integrity: Verifies all nested data written to IndexedDB
Both tests passing - confirms fix works correctly.
- Updated Dexie/liveQuery guide with detailed explanation of the
try_cache + microtask yield bug pattern
- Marked session view refactor project as RESOLVED (2026-02-26)
- Added inline code comments to all three fixed loader functions
explaining the critical fixes
Documents the 'refresh twice' bug resolution for future reference.
Added await Promise.resolve() yields after IndexedDB writes to ensure
Dexie observers fire before function returns. Aligns with pattern
established in event loaders fix.
Journals module was already working correctly (preserved try_cache),
but adding yields ensures consistent timing behavior across all
nested data loading patterns.
Adds the first successful, stable Playwright integration test for the SvelteKit frontend. This test serves as a template for future UI/UX and integration tests.
Key outcomes and learnings captured in this test:
- Establishes a pattern for running tests against the high-speed 'npm run dev' server.
- Verifies critical API security header logic, including the unauthenticated bypass for lookups and account ID scavenging from localStorage.
- Solves application boot crashes in a test environment by injecting a complete, default 'ae_loc' state object into localStorage before the app hydrates.
- Demonstrates the correct, race-condition-free pattern for waiting on network requests that are handled by API mocks.
This commit also removes the old, unreliable Node.js-based scripts ('verify_jwt_logic.js', 'verify_jwt_sync.js') that these new tests replace.
- Sequenced sections as Country, Subdivisions, then Time Zones.
- Implemented a reactive toggle for 'only_priority' in the Time Zone section to support high-priority filtering.
- Switched to a full-width card layout for improved readability of large datasets.
- Updated V3 lookup API and core timezone loader to support the 'only_priority' flag.
- Enabled high-priority timezone filtering in IDAA Recovery Meeting and Archive editors to streamline selection lists.
- Created 'src/lib/ae_api/api_get__lookup_v3.ts' to handle the new '/v3/lookup/{lu_type}/list' endpoint.
- Refactored 'get_ae_obj_li_for_lu' in 'api.ts' to prioritize the V3 system for countries, subdivisions, and time zones, with V2 fallback for legacy types.
- Ensured lookups use the 'x-no-account-id' bypass for unauthenticated site bootstrapping.
- Updated core country, subdivision, and time zone loaders to use the refined API interface.
Implemented explicit 'enabled' and 'hidden' parameter support in the Event data layer ('load_ae_obj_id__event').
Updated the Events Launcher layout and background sync engine to proactively fetch all enabled locations (including hidden ones), ensuring the room select list is complete and stays updated.
Refined 'launcher_file_cont.svelte' to only display native-specific file opening instructions when 'app_mode' is 'native'.
Updated AGENT_TODO.md to reflect task completion and new priorities.
Updated the Event File data layer to support the 'inc_hosted_file' flag in load and search functions, enabling on-demand retrieval of joined Hosted File metadata.
Refined the data mapping in 'process_ae_obj__event_file_props' to include 'content_type' and strictly controlled which properties are persisted to IndexedDB, adhering to the 'Bite-Sized Data' principle by excluding unneeded backend fields like subdirectory paths.
Enhanced 'get_ae_obj_li_v3' to support generic parameter pass-through for V3 CRUD operations.
Mapped prefixed backend fields 'hosted_file_hash_sha256' and 'hosted_file_size' to flat 'hash_sha256' and 'file_size' properties in the Event File data layer. This resolves component crashes (TypeError: slice on null) by ensuring IndexedDB is populated with valid hash strings.
Updated AGENT_TODO.md to reflect recently completed tasks.
Implemented Svelte 5 callback props (onsuccess, oncancel) for Badge create and upload forms, replacing legacy dispatchers.
Updated the AE Field Editor to accept an optional 'id' prop, resolving property mismatch errors.
Updated the Event Settings page to use the new callback prop interface, clearing type assignment errors reported by 'npm run check'.
Standardized access level hierarchy (super > manager > administrator > trusted) and added hierarchical comparison utilities to 'ae_util'.
Refactored IDAA layout to use an 'Upgrade-Only' permission strategy, preventing context-specific identifications from downgrading global Manager privileges.
Implemented strict gated filtering in the Journal Entry list: hidden and disabled items now correctly require both the appropriate hierarchical role (Trusted/Admin) AND active Edit Mode.
Added 'activity_log' table to Dexie 'ae_core_db' (v4) to support local caching of tracking data.
Implemented 'process_ae_obj__activity_log_props' with robust timestamp fallbacks.
Refactored 'qry__jitsi_report' to follow the Frontier module pattern, ensuring consistent V3 search and local cache synchronization.
Implemented 'Meeting Name (A-Z)' and 'Meeting Name (Z-A)' sorting for Recovery Meetings, including UI dropdown updates and client-side re-sorting logic.
Updated documentation/GUIDE__DEVELOPMENT.md to v1.1 with clarified verification steps and inter-agent coordination protocols.
Minor label cleanup in Journal editor.
Updated '_process_generic_props' in multiple libraries to ensure 'updated' timestamp always falls back to 'created_on' or epoch start, preventing null values from breaking newest-first ordering in IndexedDB.
Aligned Recovery Meetings and Archives list views to use these pre-computed sort keys for consistent UI behavior even when 'updated_on' is null.
Implemented critical security and architectural fixes to align the frontend with the Aether API V3 standard and resolve 403 Forbidden race conditions.
- Unified CRUD Helpers: Updated get, create, update, and delete helpers to use the standard /v3/crud/{obj_type}/{id} paths, ensuring correct backend isolation context.
- Auth Scavenging: Implemented direct localStorage scavenging for 'x-account-id' in core fetch helpers to prevent hydration race conditions in Svelte 5.
- Header Cleanup: Purged redundant 'x-aether-api-token' and fixed misplaced protocol headers in global stores.
- Reliability: Fixed 'Content-Type' typos and standardized kebab-case header normalization.
- Created consolidated AE_Obj_Field_Editor_V3 component using Svelte 5 Runes.
- Standardized on V3 CRUD API (PATCH /v3/crud/{obj_type}/{obj_id}).
- Implemented local state guards to prevent reactivity loops.
- Added support for multiple field types (text, textarea, select, checkbox, tiptap).
- Created a dedicated testing playground with real Demo account data.
- Updated the PROJECT_AE_OBJECT_FIELD_EDITOR_V3_UPGRADE.md plan.
Aligned the Events Badges, Templates, and IDAA Bulletin Board modules with
Aether UI/UX v3 standards by prioritizing semantic String IDs (e.g., event_id,
badge_id) over legacy '_random' variants in API helpers, Dexie processors,
and UI components.
- Refactored badge and template loaders to use clean ID field names.
- Updated search and LiveQuery logic in badge management views.
- Cleaned up unused imports and legacy code in +layout and +page components.
- Standardized ID mapping in generic object processors.
- Improved IDAA post creation UX with autofocus and empty title defaults.
- Updated 'handle_open_file' in launcher_file_cont.svelte to correctly pass filename to API.
- Fixed WebSocket ae_download command in launcher layout to include filename.
- Implemented a safety net in api_get_object.ts to extract filename from params if missing.
- Added 'download' attribute to Hosted Files download button for direct links.
- Refactored launcher menu components to use Svelte 5 global 'page' state instead of obsolete 'data_url' prop.
- Optimized session navigation by switching from static data_url to reactive $page.url.\n- Fixed selection hang when entering the Launcher without an initial session ID.\n- Synchronized global header and idle logic with derived layout observables.\n- Streamlined prop passing to ensure clean state transitions during session switching.
- Optimized background API refreshes to focus on the selected session instead of the entire room.\n- Eliminated redundant bulk database writes that were causing UI flickering during refresh cycles.\n- Staggered initial data fetches to prevent API request storms.\n- Added detailed logging for easier tracing of background sync operations.
- Standardized default view to 'alt' for session loading to ensure persistent file counts.\n- Hardened 'No files' warning logic to prevent false positives during background refreshes.\n- Restored reactive session observable in layout for global header and idle logic.\n- Documented overwrite pitfalls in SVELTE_DEXIE_GUIDE.md.
- Migrated background timers to persistent 'sync_intervals' store.\n- Updated Sync Monitor and Config UI to display and edit all six polling loops.\n- Ensured timer settings are applied and persisted regardless of native mode status.\n- Refined initialization logic to prioritize user configuration with robust fallbacks.
- Added background API refresh loops for room-level presentation and presenter metadata.\n- Updated Sync Monitor UI to display all six loop intervals.\n- Added UI controls in Launcher Config for fine-tuning polling periods.\n- Staggered initial background fetches to prevent API connection flooding.\n- Corrected timer assignments to separate structural metadata from room content.
- Added dedicated background timers for room-level presentation and presenter metadata.\n- Staggered initial data fetches to prevent request storms during room entry.\n- Cleaned up redundant timer assignments and separated API refresh from native file sync.\n- Optimized loop frequencies for better balance between freshness and performance.
- Silenced 'Heartbeat skipped' warnings when not running in native Electron mode.\n- Hardened device ID retrieval using String-Only ID prioritization logic.\n- Improved reliability of background sync cycles during session transitions.
- Moved session observable to child component to ensure reactive header updates.\n- Implemented deferred loading for presenters and files to prevent request storms.\n- Fixed WebSocket navigation paths to use store-backed IDs.\n- Streamlined Svelte 5 component lifecycle for better performance and SWR responsiveness.
- Optimized session list load to be shell-only, preventing initial request storms.\n- Moved deep data fetching (presenters/files) into the Presentation component level using Svelte effects.\n- Deferred child collection lookups until components are rendered in the DOM.\n- Fixed ae_api import in launcher_presentation_view.\n- Hardened background refresh logic to respect requested views.
- Initialized event_session_id to null in store template to prevent Svelte 5 binding errors.\n- Fixed invalid bind expressions in Launcher layout.\n- Corrected view: alt propagation in session list background refresh to ensure file counts are retrieved.\n- Hardened duplicate launch protection with verify_hash enabled by default.\n- Updated SVELTE_DEXIE_GUIDE.md with binding pitfall documentation.
- Implemented SHA-256 integrity checks and stale temp purge in native download logic.\n- Enabled strict hash verification in background sync cycle.\n- Made CPU load gauge dynamic using real-time loadavg metadata.\n- Consolidated native app documentation into single master manual.\n- Marked legacy electron_native.js as deprecated.\n- Updated roadmap with temp cleanup and launch protection tasks.
- Convert decorative labels to spans to fix a11y warnings in Accounts and Sites pages
- Add rel="noopener noreferrer" to external links for security
- Use untrack for Svelte 5 state initialization from props in e_app_sign_in_out.svelte
- Make TipTap editor styles global to fix scoping warnings for dynamic content
- Integrated TipTap rich text editor for Booth Descriptions and Exhibitor Notes.
- Implemented strip_html utility for clean search/preview of rich text fields.
- Renamed exhibit loading functions to follow load_ae_obj_id__event_exhibit* pattern.
- Hardened property processors with 'undefined' string guards and automatic reload triggers.
- Resolved type mismatches and naming inconsistencies across the Leads module.
- Verified zero-error state via svelte-check.
- Hardened 'Comp_exhibit_license_list' to handle both raw JSON and IDB object formats.
- Updated all labels to 'Licensed Leads User' or 'Licensee' for consistency.
- Verified Admin-only restriction for the licensee management section.
- Fixed silent parsing failures that caused empty staff lists.
- Added real-time checking for existing leads in manual search.
- Implemented 'Already Captured' status for QR scanner with direct 'View Lead' link.
- Resolved all remaining TypeScript typing issues in capture components.
- Optimized Dexie lookups for existing lead detection.
- Added 'Comp_lead_detail_form' for editing licensee responses.
- Implemented reactive form generation based on Exhibit question definitions.
- Wired up 'Edit Mode' toggle in Lead Detail page.
- Added CRUD editors for lead notes, priority, and visibility status.
- Refactored Lead List to use direct observable subscription for better reactivity.
- Implemented in-memory 'Hard Guard' filter to ensure licensee selection is strictly enforced.
- Fixed 'TypeError: s.subscribe is not a function' by removing legacy $ prefix from resolved props.
- Resolved TypeScript typing errors in Lead Detail and Search components.
- Migrated Badge Search icons to Lucide.
- Implemented dual-format parsing for 'license_li_json' (handles raw string and IDB objects).
- Added fallback lookup by 'event_exhibit_id_random' for robust record resolution.
- Fixed reactivity for licensee dropdown population.
- Hardened default selection logic based on Aether access levels.
- Renamed all staff-related fields and variables to 'licensee'.
- Implemented correct filtering logic for Aether Admins (default All, hide My).
- Implemented correct filtering for booth users (default My, show colleagues).
- Populated dropdown labels with Full Names from license_li_json.
- Removed 'Shared Passcode' from the Lead List filter.
- Disabled redundant comment loading in BB Post list to eliminate '1+N' API request overhead.
- Fixed missing 'untrack' import in BB Post page.
- Refactored IDAA Archive and BB pages to use 'page.params' for robust Svelte 5 reactivity.
- Finalized root layout hydration guards to prevent infinite store synchronization loops.
- Forced priority archives to the top of the list.
- Implemented descending (DESC) sort by the 'sort' field within groups.
- Added a sort selector to the archive list component.
- Centralized sorting logic in-memory within LiveQuery for immediate reactivity.
- Resolved 'untrack is not defined' ReferenceError in Badge Detail page.
- Transitioned Badge, IDAA Archive, and Journal Entry pages to use page.params for robust reactivity.
- Implemented fallback semantic ID lookup for Badges to handle mixed ID formats in URLs.
- Added proper loading states to detail views to prevent 'No IDB record' flashes during hydration.
- Systematically audited and cleaned up duplicate 'untrack' imports across all Svelte files.
- Refactored layouts to derive account data from stable props instead of reactive stores.
- Wrapped store updates in untrack() with deep equality guards to prevent infinite re-renders.
- Resolved duplicate untrack declarations and missing imports across the project.
- Added fetch safeguards to Element_data_store to prevent redundant API calls.
- Standardized hydration patterns to break circular dependencies during initial load.
- Fixed 'Captured initial value' warnings in 65+ components by implementing
proper sync effects with 'untrack' and derived states.
- Hardened Event Settings JSON editors using a temporary string-buffer pattern
to safely decouple object-based data from CodeMirror's string requirements.
- Resolved strict TypeScript mismatches across core routes (Accounts, Sites, etc.)
and improved property indexing safety in views.
- Patched Flowbite-Svelte Drawer transitions for Svelte 5 compatibility using
prop spreading.
- Added comprehensive safety comments to high-risk reactivity blocks.
- Synchronized 'ae_types.ts' with V3 backend models.
- Simplified Tab 2 interface by replacing segmented control with a single toggle button.
- Updated button labels and icons to indicate the available switch action.
- Implemented sticky tab persistence using local storage via events_loc store.
- Aligned Manual Search form styling with Lead List search for UI consistency.
- Updated tab switching logic to support historical navigation within exhibits.
- Center-aligned and stabilized Add Lead content area.
- Added surgical console logging in '_refresh_file_li_background' to track raw API data vs processed records.
- Refined the ID safety net to only inject missing keys, preventing accidental overwrites of existing relationships.
- Hardened '_process_generic_props' to prevent 'null' random IDs from clobbering clean V3 IDs.
- Restored specific object ID fields to 'properties_to_save' for full IndexedDB synchronization.
- Implemented a safety net in '_refresh_file_li_background' to inject missing 'for_id' and 'for_type' from query context.
- Fixed a bug in '_process_generic_props' where 'null' random IDs could overwrite clean IDs.
- Enabled initial debugging logs for event file processing.
- Updated '_refresh_file_li_background' to manually inject 'for_id', 'for_type', and specific object IDs if they are missing from the API response.
- This ensures robust indexing and retrieval from Dexie even when the V3 backend fails to populate linking fields.
- Verified that these 'fixed' objects are correctly processed and saved to the local cache.
- Reverted 'element_manage_event_file_li_direct.svelte' to use 'for_type' and 'for_id' for cache-aware filtering.
- Enhanced 'element_manage_event_file_li_all.svelte' with a combined filter for specific and generic IDs.
- Verified that ID synchronization in 'process_ae_obj__event_file_props' supports these retrieval patterns.
- Updated 'process_ae_obj__event_file_props' to synchronize generic 'for_id' with specific object IDs (e.g., 'event_presenter_id').
- Standardized 'element_manage_event_file_li_direct.svelte' to use specific ID filtering.
- Fixed missing 'prevent_default' in 'ae_comp__hosted_files_clip_video.svelte'.
- Resolved miscellaneous type and syntax errors identified by svelte-check.
- Updated 'create_event_file_obj_from_hosted_file_async' to use the modern V3 action endpoint.
- Standardized 'prevent_default' helper names in root Event and Archive components.
- Applied batch formatting (printWidth: 80) across the settings and events modules.
- Standardized props and UI using Lucide icons and Element_input_files_tbl.
- Migrated state to Svelte 5 runes ($state, $bindable).
- Updated upload logic to handle sequential processing and event_file creation.
- Improved revalidation logic by clearing Dexie cache before refreshing.
- Corrected native 'event.preventDefault()' calls in form agreement components.
- Applied batch formatting (printWidth: 80) across the pres_mgmt module.
- Applied snake_case helper standardization and formatting to element_input_file, hosted_files_clip_video, and websocket_v2.
- Cleaned up deprecated legacy imports.
- Batch formatted all Recovery Meetings module files using Prettier with printWidth: 80.
- Refactored preventDefault to prevent_default in editor and search components.
- Standardized line breaks for long attribute lists and Svelte tags for better readability.
- Ensured consistent snake_case naming for internal identifiers.
- Batch formatted all Journals module files using Prettier with printWidth: 80.
- Refactored preventDefault to prevent_default across all Svelte components.
- Standardized line breaks for imports and long attribute lists for better readability.
- Ensured consistent snake_case naming for internal identifiers.
- Systematically migrated from *_id_random to clean *_id fields in BB, People, and Posts modules.
- Synchronized Post_Comment interface with account_id for multi-tenant isolation.
- Applied optional chaining and local reactive state to harden async UI initialization.
- Refactored common helpers to follow snake_case naming conventions.
- Resolved various minor stability and logic issues across IDAA Bulletin Board.
- Refactored lq__post_obj_li to perform in-memory descending sort by updated_on/created_on.
- Ensured newest and recently modified posts appear at the top of the list.
- Hardened reactivity for Svelte 5 stability.
- Fixed 'post_id' missing error in comment creation by mapping to 'post_id_random'.
- Resolved infinite request loop in post view via untrack() optimization.
- Hardened all property accesses with optional chaining to prevent TypeErrors.
- Migrated comment editor to local reactive state for Svelte 5 stability.
- Refactored post visibility layer to use derived reactive filtering.
- Standardized ID mapping patterns across all BB components.
- Renamed 'comment' table to 'post_comment' in db_posts.ts.
- Bumped Dexie database version to 2.
- Resolved InvalidTableError: Table post_comment does not exist.
- Standardized ID mapping for local storage consistency.
- Added optional chaining to 'Cancel Edit' button title attributes.
- Resolved persistent TypeError: can't access property 'full_name' on null.
- Ensured total UI stability during new comment initialization.
- Added optional chaining to all post_comment_obj property accesses.
- Resolved TypeError when clicking 'New Comment' button.
- Hardened UI stability for asynchronous data loading.
- Added optional chaining to all post_comment_obj property accesses.
- Resolved TypeError when clicking 'New Comment' button.
- Hardened UI stability for asynchronous data loading.
- Deleted redundant reactive trigger in post view component.
- Removed state-syncing side-effects from liveQuery derivation.
- Standardized log_lvl prop to resolve initialization crash.
- Optimized performance for single post loading.
- Added 'account_id_random' to persistent property list to fix local search isolation.
- Standardized search body and helpers to support mandatory x-account-id headers.
- Refactored LiveQuery to use synchronous dependency tracking ($derived.by) for reliable search updates.
- Broadened server-side search to handle inclusive OR logic on the client, preventing disappearing results.
- Updated task list with IDAA Recovery Meeting testing status.
- Resolved 400 Bad Request by whitelisting 'physical', 'virtual', and 'external_person_id' fields in backend searchable_fields.
- Broadened server-side search by removing restrictive AND filters for location types, moving the inclusive OR logic to the client-side filter layer.
- Hardened handle_search_refresh error handling to clear results on category filter changes (Type, Physical, Virtual) while maintaining flicker protection strictly for text search typing.
- Restored permissive visibility for Trusted users, allowing them to see hidden/disabled items regardless of Edit Mode.
- Optimized plural object loading in Archives and Events modules using concurrent Promise.all.
- Standardized ID handling across the search flow to prevent Dexie lookup failures.
- Finalized well-commented code across IDAA modules to document search strategies and workarounds.
- Hardened 'JournalEntry' and 'Sponsorship' object processors to handle null sort values.
- Disabled aggressive pre-loading of archive content in main archives list to improve performance.
- Hardened object processors in Archives and Posts modules to safely handle null 'sort' values, preventing runtime TypeErrors during data synchronization.
- Fixed inconsistent sorting in Archive Content list by correctly implementing descending order (sort then reverse) and adding a configuration loading guard to the liveQuery.
- Standardized safe data processing patterns in SVELTE_DEXIE_GUIDE.md.
- Performed minor cleanup and visibility logic hardening in Recovery Meetings module.
- Restored Event Session search by standardizing on 'event_id' for Dexie queries and implementing dual-layer filtering (local + API guard) to prevent broad results from clobbering filtered views.
- Advanced String-Only ID Standardization (Phase 2) by updating generic object processors across all event library modules to support both base IDs and legacy '_random' variants.
- Refactored Event Presenter and Presentation components to support standardized '_id_li' props while maintaining backward compatibility.
- Standardized common helper identifiers to snake_case (e.g., 'prevent_default') in the Events module.
- Verified Staff and Poster email notification logic in the Bulletin Board module.
- Updated .gitignore and cleaned up test artifacts.
- Updated 'method' parameter types in IDAA edit components to match the expected union types in the V3 API helpers.
- Applied type casting to 'liveQuery' results in the Archives page to ensure 'topic_name' and other convenience fields are recognized by the compiler.
- Fixed casting for 'method' variable in delete operations across Archives, Bulletin Board, and Recovery Meetings.
- Harden 'ae_events__event_presenter.ts' to ensure both ID and ID_random fields are synced during refreshes.
- Update presenter list wrapper to use '_id_random' for reliable Dexie queries.
- Enable 'inc_all_file_li' and 'inc_file_li' in session loaders to pre-cache nested files.
- Introduce 'Launcher_presentation_view.svelte' to explicitly display files linked to presentations.
- Refactor presenter views to focus exclusively on speaker-specific files, avoiding UI duplication.
- Roll out platform-wide standard for unauthenticated binary access using '?key=[account_id]' query parameter.
- Update API helpers (get, post, patch) to recognize 'key' bypass and strip account context headers accordingly.
- Refactor IDAA Bulletin Board to restore inline image rendering and edit-mode previews.
- Modernize Events Launcher (Layout, Sync, Session View) to use V3 Action URLs with verified auth.
- Update HTML generators in 'ae_utils.ts' to support the new authenticated URL structure.
- Harden 'ae_comp__event_file_obj_tbl' CSV export and clipboard links with V3 standard patterns.
- Add 'show_direct_download' prop to support native browser downloads via V3 Action paths (/v3/action/...).
- Integrate 'shorten_filename' logic directly into the component via 'max_filename' prop.
- Refactor internal UI to snippets for consistent rendering across button and anchor modes.
- Update 'ae_HostedFile' type with new SQL view fields: 'filename_no_ext' and 'filename_w_ext'.
- Add direct download toggle and edit mode indicator to testing dashboard.
- Simplify 'ae_comp__event_file_obj_tbl' by removing manual snippets in favor of standardized component logic.
- Created AE_Comp_Hosted_Files_Download_Button using Svelte 5 and Lucide icons.
- Added file_extension_icon_lucide utility for direct Lucide icon mapping.
- Refactored download logic to core__hosted_files.ts using V3 Action endpoint (/v3/action/hosted_file/.../download).
- Integrated new component into Event File Object Table.
- Cleaned up legacy window.postMessage calls in several file management views.
NOTE: The new download component is currently in development and may not be fully functional.
- Refactored 'event_session', 'event_presentation', 'event_file', 'event_location', and 'event_presenter' libraries to follow a Stale-While-Revalidate pattern.
- Implemented non-blocking background refreshes to allow instant UI rendering from IndexedDB cache.
- Parallelized nested collection loads (files, presentations) during background tasks to eliminate UI stalls.
- Standardized ID lookups using specific indices (event_location_id, event_id) for reliable local data retrieval.
- Resolved regression where aggressive ID overwriting caused relationship inconsistencies in background loads.
- Fixed reactive access to live queries in 'session_view.svelte' using proper Svelte 5 prefixing.
- Introduced 'Launcher_Cfg_Section' with 3-way state support (collapsed, auto, pinned).
- Implemented 'handle_section_expand' coordinator in 'launcher_cfg.svelte' for single-active-section behavior.
- Overhauled all configuration sub-components to participate in the auto-collapse logic.
- Updated 'ae_events_stores.ts' with new persistent section states.
- Synchronized 'launcher_cfg_template.svelte' with the new pattern for future extensions.
- Expose new native handlers in electron_relay.ts (Wallpaper, Updates, Window Control, Power).
- Overhaul Native OS Management UI with controls for window states, display layouts, and power.
- Add Application Updates component with support for local and web sources.
- Include confirmation modal for dangerous system actions (shutdown/reboot).
- Update TODO.md to mark Phase 5 integration as completed.
Update layout.ts to clean raw data from the native bridge. Initialize events_slct.event_device_id in launcher layout. Resort device_id prioritization in launcher_background_sync.svelte.
- Synchronize Functional Spec with new system handlers (Wallpaper, Power, Recording, Displays).
- Update Automation Scripts with finalized AppleScript handlers for PowerPoint and Keynote.
- Mark IDAA and Phase 5 automation tasks as completed in TODO.md.
- Refactor Jitsi reports to use SvelteKit streaming with a skeleton loader.
- Add conference lifecycle event listeners (left, close) to video conference page.
- Implement manual Novi data re-sync and improve initialization robustness.
- Fix skeleton visibility by using standard Tailwind colors.
- Fix infinite loops in '+layout.svelte' by using 'untrack' for store synchronization effects.
- Correctly define 'ae_acct' as a derived rune to resolve ReferenceErrors and ensure site styles hydrate properly.
- Modernize 'ae_comp__badge_template_form.svelte' with Svelte 5 Runes and callback props (onsuccess, onerror, oncancel).
- Fix illegal async in badge form by moving logic to a dedicated load function untracked by the effect.
- Add Presentation Management Reports to TODO list for tracking.
- Standardize 'ae_BaseObj' and event types in 'ae_types.ts' to handle nullable fields from V3 API/Dexie.
- Modernize Person, Address, and Contact forms with Svelte 5 Runes and reactive synchronization.
- Refactor Event Settings and its sub-components to use the 'onsave' callback pattern, removing deprecated dispatchers.
- Hardened 'element_data_store_v2' with safe initialization and localStorage caching logic.
- Clean up unused 'element_data_store.svelte' (V1) and suppress Electron environment type errors in 'tmp_shell_handlers.ts'.
- Update documentation and workspace settings to reflect Phase 5 reactive patterns.
- Implemented V3-style reactive search (Local Cache -> Remote Revalidation) for exhibitors.
- Standardized search fields to 'name' for Exhibits and 'event_badge_full_name' for Lead Tracking.
- Refactored Leads UI with standardized search components and grid layout.
- Updated event routing to exclusively use string-based IDs (Triple-ID pattern).
- Hardened 'ae_EventSession' type definitions to handle null values from V3 API/Dexie.
- Migrated Session search to the debounced pattern with Search Guards and shared observables.
- Implemented 'Remote First' toggle support (Edit Mode only) for background-only revalidation.
- Resolved 'each_key_duplicate' crash and fixed icon scaling issues in the session list.
- Restored missing session alert visibility for managers.
- Standardized store initialization to prevent reactivity loops.
- Finalized the rename of 'default_qry_string' to 'default_qry_str' across the Badge library, Dexie schema, and API documentation.
- Synchronized all reactive filtering in '+page.svelte' with the standardized field names to restore full search functionality.
- Verified that text search, affiliations, and types now correctly map to whitelisted backend fields.
- Renamed 'default_qry_string' to 'default_qry_str' across the Badge interface, Dexie schema, and API logic to resolve 400 'Unauthorized search field' errors.
- Synchronized local Fast Path filtering with the correct database field names.
- Hardened the reactive search pattern in '+page.svelte' to ensure end-to-sync consistency between local and background results.
- Implemented Search Guard and store initialization in '+page.svelte' to fix loops and filtering.
- Updated 'ae_comp__badge_search.svelte' to act as a pure UI component triggering versioned searches.
- Refactored 'ae_comp__badge_obj_li.svelte' to use shared data streams and fixed icon build errors.
- Corrected 'search__event_badge' to use the valid 'default_qry_str' field name, resolving 400 errors during text search.
- Re-implemented the standardized debounced search pattern in '+page.svelte' with a robust Search Guard to eliminate loops.
- Hardened Fast Path local filtering to match the 'Badge' schema and synchronized result sorting with API revalidation.
- Updated 'ae_comp__badge_search.svelte' and 'ae_comp__badge_obj_li.svelte' to align with the shared data pattern and fixed Lucide icon imports.
- Ensured 'events_loc' store initialization for new search-related fields.
- Implemented a 'Search Guard' pattern in '+page.svelte' that snapshots search criteria and bails out of redundant executions.
- Stabilized reactivity by removing immediate list clearing in Remote First mode, ensuring a consistent data stream.
- Isolated all search-driven state updates with 'untrack' to prevent circular dependency triggers.
- Hardened the 'lq__journal_entry_obj_li' observable to ensure stable result emission.
- Refactored 'src/routes/journals/[journal_id]/+page.svelte' to use a singleton 'liveQuery' observable, eliminating the search subscription loop.
- Synchronized entry counts in 'ae_comp__journal_obj_id_view.svelte' with the shared entries observable, ensuring accurate header feedback.
- Cleaned up 'ae_comp__journal_entry_obj_li.svelte' by removing redundant script blocks and hardening visibility filters.
- Restricted the result count badge to Edit Mode for a cleaner standard user interface.
- Improved initial load UX with localized spinners and proper undefined-state guards.
- Implemented the loop-proof search pattern in 'src/routes/journals/[journal_id]/+page.svelte' using versioned triggers and untracked dependency isolation.
- Stabilized data loading by introducing 'ae_comp__journal_entry_obj_li_wrapper.svelte', ensuring smooth SWR transitions between local and API results.
- Synchronized result counts with the rendered list in 'ae_comp__journal_entry_obj_li.svelte' and implemented permissive visibility defaults.
- Added 'Remote First' toggle support (Edit Mode only) and laid groundwork for Global Search in 'ae_comp__journal_entry_obj_qry.svelte'.
- Updated Dexie schema and API helpers to support future person-level cross-journal searching.
- Stabilized the 'liveQuery' observable in 'ae_idaa_comp__event_obj_li_wrapper.svelte' by re-wrapping it in '', ensuring smooth UI updates when switching between local and API results.
- Reduced search debounce time to 250ms in '+page.svelte' for a more instantaneous user experience.
- Hardened 'Fast Path' local filtering to be more permissive and added debug logging to help diagnose IndexedDB sync issues.
- Restricted the visibility of the 'Remote First' toggle to Edit Mode (.edit_mode) for a cleaner standard user interface.
- Replaced boolean search triggers with a versioned integer pattern to break reactivity loops in '+page.svelte'.
- Resolved 'behind by one' search lag by ensuring the UI immediately reflects local results, even if empty.
- Corrected subscription syntax for 'liveQuery' observables in 'ae_idaa_comp__event_obj_li.svelte'.
- Implemented a robust 'Remote First' toggle support in search logic while maintaining fast-path local feedback.
- Hardened Fast Path filtering to match server-side logic more accurately for improved cache reliability.
- Standardized the search pattern using Svelte 5 debounced $effects in Recovery Meetings, Badge Search, and Journals to eliminate manual triggers and stuttering.
- Fixed a bug in 'ae_idaa_comp__event_obj_li.svelte' where the Results count was inconsistent with the displayed list by implementing a derived 'visible_event_obj_li' state.
- Hardened 'ae_idaa_comp__event_obj_li_wrapper.svelte' to filter out 'undefined' entries from database 'bulkGet' calls.
- Cleaned up legacy search handling code in 'ae_idaa_comp__event_obj_qry.svelte'.
- Updated 'TODO.md' and 'GEMINI.md' to reflect search logic hardening accomplishments.
- Documented the 'Non-Blocking Load Pattern' (SWR) in 'documentation/PERFORMANCE_GUIDELINES.md' to prevent future performance regressions.
- Refactored 'src/routes/core/people/[person_id]/+page.ts' to be non-blocking, improving perceived speed for person details.
- Updated 'GEMINI.md' standards and 'TODO.md' tasks to reflect system-wide performance hardening.
- Eliminated blocking 'await' calls in '+page.ts' for main Events, Session, Location, and Presenter views.
- Implemented Stale-While-Revalidate (SWR) pattern: pages now render instantly using cached IndexedDB data while API refreshes run in the background.
- Removed expensive sequential loops in load functions, allowing the reactive UI (via LiveQuery) to handle data arrival as background tasks complete.
- Standardized Event module logic files to strictly use random string IDs, ensuring frontend consistency and avoiding 'Integer Trap' 404s.
- Refactored 'ae_comp__event_session_alert.svelte' to accept a plain object instead of a store/observable, resolving the 'store_invalid_shape' error in list loops.
- Updated all callers of the alert component to pass unwrapped session objects.
- Cleaned up event-related UI components to remove redundant '_random' field lookups and align with V3 CRUD patterns.
- Updated project metadata (GEMINI.md, TODO.md) to reflect the new memory structure and latest hardened state.
- Implemented 'Details' toggle in Session lists to defer expensive presenter/file sub-component renders, improving initial hydration speed.
- Fixed line-wrapping for long lines in Journals Editor (both plain textarea and CodeMirror).
- Updated TODO.md and GEMINI.md to reflect Journals standardization and performance optimizations.
- Renamed all Journal components to follow the ae_comp__* snake_case convention.
- Normalized all custom event handler props from PascalCase (onSave) to snake_case (on_save) across the module.
- Migrated all icon imports from @lucide/svelte to lucide-svelte for consistency.
- Resolved ReferenceErrors and file corruption issues in Journals config and entry views.
- Updated qry__journal_entry logic to support category filtering.
- Verified module integrity and component interop.
- Fixed invalid JavaScript-style comments inside HTML tags.
- Restored intentional '1 == 3' business logic pattern.
- Switched to 'event.currentTarget' for FormData instantiation.
- Resolved type errors in delete method calls via explicit casting.
- Cleaned up unused props and resolved property access issues in Archives and Bulletin Board components.
- Verified final state with npm run check.
- Updated ae_types.ts with missing IDAA-specific fields for Archives and Events (topic_name, archive_on, contact_li_json, etc.) using snake_case.
- Refactored bulletin board post filter to safely handle null archive_on dates.
- Fixed missing 'data' prop assignment in bulletin board list component to resolve type error.
- Corrected core_func.download_export__obj_type method name in recovery meetings export.
- Hardened safety checks for contact_li_json in recovery meetings view logic to prevent null property access.
- Mapped Jitsi meeting event data to internal snake_case variables and fixed input type assignments.
- Updated project documentation (TODO, GEMINI.md, .ae_brief) to reflect IDAA hardening progress.
- Re-enabled automatic service worker registration in svelte.config.js.
- Implemented a production-ready service-worker.js using SvelteKit metadata (-worker).
- Added intelligent caching for build artifacts, static files, and network fallbacks.
- This ensures the UI shell and assets load instantly from disk, completing the hydration performance workstream.
- Added visual progress bars for RAM usage based on native device metadata.
- Implemented color-coded status indicators (success/warning/error) for memory usage.
- Enhanced layout with animated pulses for heartbeat and sync status.
- Added detailed device info display including hostname and IP addresses.
- Refactored Events root layout to fire load_ae_obj_id__event in the background.
- Refactored Journals list page to fire load_ae_obj_li__journal in the background.
- Combined with SWR module logic, this ensures near-instant UI rendering from Dexie cache while refreshes occur asynchronously.
- Implemented SWR pattern for Journal and Site Domain lookups.
- Refactored +layout.ts across modules to fire background refreshes instead of blocking.
- Updated +layout.svelte to render the layout shell immediately while hydrating.
- Silenced 'AbortError' and 'NetworkError' in api_get_object.ts and api_post_object.ts for log_lvl 0.
- Resolved duplicate export errors in ae_journals__journal.ts.
- Implemented SWR pattern for session loading in ae_events__event_session.ts.
- Refactored Launcher layouts (+layout.ts and +page.ts) to fire background refreshes instead of awaiting API calls.
- Removed redundant blocking logic to ensure instant UI rendering from Dexie cache.
- Refactored lookup_site_domain_v3 to be cache-first, unblocking root layout.
- Implemented Stale-While-Revalidate (SWR) pattern for load_ae_obj_id__event.
- Added global hydration overlay and loading spinner to +layout.svelte.
- Updated site domain whitelist to persist critical account/styling metadata in Dexie.
- Refactored root load function to return immediately if cached site data is found.
- Broke down the massive launcher_cfg.svelte into 7 modular sub-components.
- Updated electron_relay.ts with Phase 5 presentation controls and manifest tools.
- Updated architecture documentation to reflect the new TypeScript-based native bridge.
- Upgraded LauncherBackgroundSync to force-hydrate OS metadata (home/tmp) on mount.
- Hardened electron_relay.ts with robust placeholder resolution and global regex.
- Restored safe handover by making native.launch_from_cache presentation-aware.
- Integrated heartbeat and sync status into the formal Launcher Config UI.
- Added comprehensive technical documentation for the 2026 native architecture.
- Reverted handle_open_file to use native.launch_from_cache for atomic copy support.
- Upgraded Electron main process to handle specialized launcher logic (LibreOffice/AppleScript) internally after the copy operation.
- Ensures absolute paths and file existence before triggering OS apps.
- Removed detailed OS path hydration from root +layout.ts to keep it lean.
- Moved home/tmp directory hydration to LauncherBackgroundSync onMount.
- Fixed regex pattern bug in electron_relay.ts (/\[home\]/g).
- Ensures absolute paths are correctly generated within the Launcher module.
- Officially marked Phase 4 (Heartbeat/Sync) as complete.
- Updated GEMINI.md development history and session context.
- Set Phase 5 (AppleScript/Office) as the next active workstream.
- Added logic to automatically JSON.stringify any field ending in '_json' in V3 API helpers.
- Added final payload logging to create_ae_obj_v3 for better debugging.
- Resolves 'str type expected' validation errors (HTTP 400) when sending objects to V3 CRUD endpoints.
- Removed manual 'Z' suffix addition in device processing to fix incorrect offsets.
- Switched heartbeat payload to standard UTC ISO strings.
- Enabled background timers in non-native environments for easier verification.
- Hardened telemetry gathering to skip Electron-only APIs when running in a browser.
- Explicitly added the 'x-account-id' header to resolve HTTP 403 error when fetching Jitsi activity logs.
- Ensures the server-side scope check passes for these report requests.
- Added Array.isArray check to prevent 'not iterable' error on API failure responses.
- Restored params_json to narrow the activity_log query and prevent potential timeouts.
- Cleaned up minor whitespace in sort logic.
- Hardened 'find_object_id' in Dexie to support 'event_' prefixed IDs.
- Implemented singleton 'LauncherBackgroundSync' in root launcher layout.
- Added V3-compliant heartbeat and room refresh cycles.
- Fixed redundant component imports and Skeleton UI prop errors.
- Added 'inc_file_li' support to event API loader.
- Improved manual OS command input with better binding and extraction logic.
- Resolved '[object Object]' issue by correctly displaying stdout/stderr/error from structured return.
- Polished terminal output UI with better styling and clear functionality.
- Renamed section to 'Native OS Handlers & Folders' for better clarity.
- Updated electron_relay.ts with full Phase 3 command set (run_cmd_sync, kill_processes, get_device_info).
- Fixed 500 error in Event Location by standardizing enabled/hidden flags.
- Hardened native environment detection in root layout.
- Added Manual OS Command input to Launcher Configuration for Phase 3 testing.
- Updated load functions to pass inc_session_li and inc_all_file_li.
- Enhanced _handle_nested_loads to recursively fetch sessions and files.
- Ensured consistency with Triple-ID pattern and V3 API standards.
- Implemented aggressive room-wide background caching engine in LauncherBackgroundSync.svelte.
- Added inc_file_li and inc_all_file_li support to Event and Event Location object loaders for total room coverage.
- Switched to proven V1 download path (/hosted_file/) to resolve V3 CRUD binary delivery issues.
- Optimized Electron bridge with download locking and flat-hash storage matching legacy 'perfect' logic.
- Standardized all Electron IPC methods and parameters to snake_case.
- Added visual sync progress indicator for room caching status.
- Implemented Default, Onsite, and Native launcher modes in launcher_file_cont.svelte.
- Restored background pre-caching logic with configurable timers in new LauncherBackgroundSync component.
- Fixed standard browser download regression for regular website mode.
- Modernized electron_relay to TypeScript and standardized bridge detection in layout.
- Detailed startup and background sync technical flow in documentation.
- Added Query Limit configuration for Journals and Entries in modal settings.
- Refactored +page.svelte to delegate empty-state handling to the entry list component.
- Improved "no results" messaging in ae_comp__journal_entry_obj_li.svelte.
- Consolidated LiveQuery logic into [journal_id]/+page.svelte and removed redundancy from layout.
- Added automatic load trigger on journal_id change to ensure data freshness.
- Hardened Enabled/Hidden filters to correctly include NULL/undefined values in default views.
- Refined search behavior to distinguish between "default view" (null) and "no results found" ([]).
- Updated modal_journals_config.svelte with standardized module-level settings.
- Robust ID handling in bulk retrieval to handle varying property names (id, random_id).
- Simplified limit logic in search__event and qry_ae_obj_li__event_v2 to use the passed value directly.
- Added .slice(0, limit) to filtered results in Event module to handle over-fetching correctly.
- Explicitly passed the qry__limit from idaa_loc to the meeting list components.
- Applied .limit() to the Dexie liveQuery in the meeting list wrapper.
- Updated the UI result count and loop to strictly adhere to the requested limit.
- Restored legacy query aliases (qry_ae_obj_li__event, qry__event_file, etc.) across Event modules to fix build errors.
- Fixed Hosted File deletion logic in core__hosted_files.ts to use specialized /hosted_file/{id} endpoint with fake_delete support.
- Standardized ID field usage (hosted_file_id vs hosted_file_id_random) in Archive Content and Event File upload components.
- Updated TODO.md to reflect completed search restoration tasks.
- Backend: Updated `qry__journal_entry` in `ae_journals__journal_entry.ts`.
- Logic: Switched `default_qry_str` operator to `like` (from `match`) for V3 API compatibility.
- Fix: Corrected filter field names from `enabled`/`hidden` to `enable`/`hide`.
- Resilience: Added robust unwrapping of V3 API response envelopes (handling `{ data: [] }` vs array) to prevent iteration errors.
- Backend: Updated `search__event` in `ae_events__event.ts` to populate `params['lk_qry']`.
- Logic: Used `like` operator with wildcards for `default_qry_str` in `search_query` to match `event_session` pattern.
- Context: This fixes the text search in IDAA Recovery Meetings by ensuring the backend receives the expected V3 search parameters.
- Backend: Added V3-compliant `search__event` to `ae_events__event.ts` with support for `default_qry_str`.
- Backend: Removed accidental duplicate `qry_ae_obj_li__event_v2` implementation.
- API: Exported `search__event` from `ae_events_functions.ts`.
- Frontend: Updated `recovery_meetings/+page.svelte` to use the new `search__event` function.
- Mitigation: Disabled `serviceWorker.register` in `svelte.config.js` to stop the loop of `TypeError` during script evaluation.
- Debug Tool: Preserved the `fix-sw` cache clearing utility by moving it to `src/routes/testing/fix-sw/+page.svelte` for future investigation.
- Service Worker: Simplified `src/service-worker.js` to a minimal "Hello World" state and removed the problematic `.ts` version.
- Cleanup: Minor formatting adjustment in `ae_events_functions.ts`.
- Docs: Updated `TODO.md` and `GEMINI.md` to reflect the mitigation and planned follow-up.
- API: Updated `search_ae_obj_v3` to correctly serialize complex URL parameters (JSON).
- Events: Restored "sacred" business logic for Event Badge and Session searches using `ft_qry` and `lk_qry`.
- PWA: Fixed manifest path in `app.html` to resolve 404 errors.
- Documentation: Updated `GEMINI.md` and `TODO.md` with recent search hardening accomplishments.
- API Migration: Refactored core__hosted_files.ts and ae_events__event_file.ts to use AE API CRUD V3.
- Logic Hardening: Unified property processing via '_process_generic_props' and ensured 'id_random' string identifier consistency.
- Reliability: Hardened orphan cleanup logic in delete_ae_obj_id__hosted_file and aligned with V3 generic delete patterns.
- API: Correctly exported 'get_ae_obj_li_for_lu' and reverted to stable V2 endpoints (/v2/crud/lu/.../list) to resolve V3 500 errors.
- Auth: Injected 'x-no-account-id' bypass header for unauthenticated global lookup access.
- UI: Updated lookups page to support 'english_short_name' fallback for countries and added Country Subdivisions card.
- Debug: Added transient console logging for verification.
- Search Hardening: Implemented 'Inclusive OR' logic for physical/virtual filters and restored robust full-text search using 'default_qry_str' with wildcards.
- Crash Fix: Added null checks for 'contact_li_json' in meeting detail view to prevent TypeError.
- ID Stability: Enhanced 'core__idb_dexie.ts' to support 'id_random' mapping and fixed misleading save logging.
- Reliability: Both V2 and V3 search paths now consistently return processed objects with standardized IDs.
- IDAA Isolation: Created using legacy V2 endpoints and for Recovery Meetings stability.
- V3 Refinement: Implemented 'Body + Header' injection in to fix 'Integer Trap' while maintaining Auth scope.
- API Upgrade: Enhanced to support custom headers.
- Docs: Updated migration guide and development history with final isolation strategy.
Restored the use of 'for_obj_type' and 'for_obj_id' URL parameters for the 'event/search' endpoint. The backend permission middleware requires these URL parameters to validate the API Key scope for the Event object, whereas body-only injection (used in Archives/Posts) was causing a 403 Forbidden error for Events.
- Implemented Structured Error Handling across GET/POST/PATCH helpers to extract rich V3 error metadata.
- Added direct localStorage fallback for JWT detection to resolve race conditions during initial page load.
- Fixed async race condition in Archives leading to 'archive_content_li is undefined' crash.
- Hardened generic object processor to handle non-array API responses gracefully.
- Resolved zero-result bug in Event Search by using raw 'account_id_random' to bypass backend mapping conflicts.
- Isolated bootstrap headers in +layout.ts and removed invalid response headers from request config.
- Enhanced /testing dashboard with live header inspection and V3 hardening audits.
- Updated api_get_object and api_post_object to extract rich metadata (meta.details) from 400/500 responses.
- Enables frontend to bubble up specific DB schema, validation, and constraint errors for better debugging.
- Added 'V3 Hardening' section to /testing dashboard with automated tests for Permissive Mode and Structured Error extraction.
- Hardened 'Bootstrap Paradox' bypass logic in GET/POST helpers to only strip account ID if an intentional bypass value is provided.
- Enabled 'Permissive Update Mode' (x-ae-ignore-extra-fields: true) by default to improve frontend state synchronization.
- Fixed loader hydration bug where isolated API headers were being overwritten by stale global defaults.
- Ensured correctly resolved account names persist in local state instead of defaulting to 'Ghost Account'.
- Added Environment & Bridge diagnostics section to the testing dashboard for easier runtime verification.
- Resolved 'Ghost Account' warning by updating layout hydration to align with V3 ID Vision (account_id vs account_id_random).
- Improved site lookup reliability using Agent API Key and structured EQ filters for exact FQDN matching (including ports).
- Modernized PWA manifest with maskable icons (PNG/WebP), app shortcuts, and unique installation IDs.
- Implemented automatic Electron 'Native' mode detection in root layout.
- Fixed stale API URLs in Launcher native file download logic.
- Added V3 migration documentation and JWT verification test scripts.
- Incorporated comprehensive icon list from dgr.oneskyit.com reference.
- Updated dynamic naming logic to 'One Sky IT - {Account} Aether PWA'.
- Set display mode to 'fullscreen' per reference standard.
- Added verification tool to the Testing Dashboard.
- Added /manifest.webmanifest server-side route.
- Implemented hostname-based branding lookup using Agent API Key.
- Updated app.html to use the dynamic manifest route.
- Added manifest verification tool to the System Testing dashboard.
- Standardized ID usage: Migrated multiple components from '_random' variants to standard 'id' properties to align with V3 backend and Dexie patterns.
- Electron Integration: Added global 'native_app' declaration and Window interface augmentation in app.d.ts to resolve TypeScript errors.
- Type Safety: Enhanced ae_EventSession interface with missing 'event_file_li' and added runtime null checks in session loading logic.
- UI/UX: Restored missing launcher components in location-specific pages and fixed LiveQuery filter logic for session lists.
- Bug Fixes: Resolved indexing errors in location list and corrected store update patterns in layout.
- API: Standardized 'order_by_li' types in event and archive modules.
- API: Corrected 'enabled'/'hidden' parameter types in event exhibit and device search/list functions.
- Type Safety: Addressed generic type casting issues in _process_generic_props across event modules.
- Data Handling: Resolved return type consistency in journal creation and DB save operations.
- Parameter Management: Fixed missing 'try_cache' parameters in event exhibit functions.
- Core Logic: Ensured correct object properties in DB put operations for hosted files.
- Core: Use FormData in video clip components to fix EventTarget errors.
- DB: Rename 'File' to 'ae_LocalFile' in db_core to prevent DOM type shadowing.
- API: Strictly type 'order_by_li' across archives, events, and journals to match API definition.
- API: Fix 'enabled'/'hidden' parameter types in search functions.
- Generics: Add 'any' cast to 'processed_obj' in generic processors to fix indexing errors.
- Journals: Update journal_entry creation to return null on failure, fix missing ID fields in DB save.
- Update ae_types.ts with joined fields for deep layout loading
- Fix OrderBy vs OrderBy[] type mismatch in API v2/v3 and generic CRUD
- Apply 'as const' to order_by_li defaults in core/event libraries
- Resolve type errors in reports_presenters and reports_files Svelte components
- Resolved Svelte 5 $bindable() prop errors in Analytics and Person Table components.
- Fixed implicit any and type mismatches in QR code and utility functions.
- Corrected OrderBy type mismatch pattern in systemic loaders.
- Improved type safety for BlobPart and ArrayBuffer handling.
- Down to 700 errors from ~731.
- Added missing Addresses and Contacts links to Core Management main page.
- Modernized list pages for Accounts, Sites, Activity Logs, and Lookups.
- Standardized headers, iconography, and search layouts across all core list views.
- Improved layout responsiveness and visual hierarchy.
- Rebuilt Users and People search from scratch using robust flexbox (grow, min-w-100px, whitespace-nowrap).
- Added search functionality and card-grid layout to Address and Contact management lists.
- Modernized detail pages for Addresses and Contacts with standardized headers and iconography.
- Unified form padding (p-3 for inputs, p-2 for selects) across all Core modules.
- Fixed variable hoisting and missing icon imports in Address components.
- Standardized User Management search to match People Management style.
- Added 'p-3' padding to main search inputs and 'p-2' to selects for better visual clarity.
- Improved search icon spacing with enhanced shim padding (!px-4).
- Applied consistent padding enhancements to Account, Site, and User detail pages.
- Standardized 'Go' button prominence across search sections.
- Updated Person, Address, Contact forms with consistent Skeleton UI styling.
- Standardized Account, Site, and User detail pages.
- Modernized People, Address, and Contact list pages with improved headers and action buttons.
- Applied 'variant-filled-surface' and 'rounded-lg' patterns from Journals module for UI consistency.
- Updated load_ae_obj_li__user to handle 'All' scope by fetching account and global users separately.
- Ensured Search API uses authorized account_id_random field with op: 'eq' for null.
- Fixed 404 for global lookups by avoiding [NULL] in standard List API.
- Switched from 'op: is' to 'op: eq' for NULL values, as 'is' was returning 400.
- Refined search query structure to ensure compatibility with user search endpoint.
- Refined load_ae_obj_li__user to use List API for simple account filtering and Search API for complex cases.
- Fixed 'Global Only' scope returning all users by ensuring correct NULL filtering.
- Improved search query structure with top-level 'or' for inclusive global support.
- Added detailed logging to load_ae_obj_li__user for easier debugging.
- Updated load_ae_obj_li__user to use search_ae_obj_v3 for complex account OR global filtering.
- Fixed zero-results issue by defaulting to 'All' scope (Current Account + Global).
- Added visual 'Account' vs 'Global' badges to user cards.
- Added 'Scope' selector to User Management page.
- Updated load_ae_obj_li__user to support for_obj_id filtering.
- Updated create_ae_obj__user to support account_id for new users.
- Updated User Management page to default to current account_id for listing and creation.
- Improved Person table with 'Not Linked' badge for better visibility.
- Updated Person form with 'prefix' field and direct User account linking.
- Standardized Address and Contact list views to show all records (enabled/hidden).
- Amending previous refactor to include all verified import and type fixes.
- Unified Person and User logic into ae_core__* counterparts and marked legacy files.
- Renamed Activity Log to ae_core__activity_log.ts for naming consistency.
- Updated all core function imports across the identity, logs, and video conference modules.
- Fixed missing 'prefix' field in Person form payload and corrected return types in Activity Log.
- Updated project TODO to reflect completed core module refinements.
- Refactored authentication calls in core__user.ts to explicitly set x-account-id and remove x-no-account-id, ensuring correct account context for legacy endpoints.
- Updated emailed sign-in link logic to use the correct /user/{user_id}/email_auth_key_url endpoint and avoid 500 crashes caused by extraneous URL parameters.
- Fixed person search query field names (enable/hide) and broadened search scope to 'all' to ensure records are found regardless of status.
- Added safety checks and documentation to prevent UI crashes when API responses are empty or NULL.
- Implemented 'untrack' safeguards in all modal effect blocks to prevent reactivity loops.
- Standardized Module, Journal, and Entry configuration with unified tabbed UI and Aether Orange theme.
- Fixed critical 'journal is undefined' and ReferenceErrors in configuration modals.
- Restored missing button toggles and visibility options in journal-level settings.
- Documented Dexie liveQuery '$' prefix mandate in GEMINI.md and project documentation.
- Eliminated legacy JournalEntry_SettingsMenu in favor of ModalJournalEntryConfig.
- Refactored Module, Journal, and Entry configs to use consistent tabbed layouts.
- Adopted unified 'Aether Orange' aesthetic with Skeleton UI and Lucide icons.
- Replaced overcrowded inline settings menu with 'ModalJournalEntryConfig'.
- Cleaned up Header component logic and improved visual consistency.
- Added 'Force Reset to Plain Text' button in the editor for decryption failure states.
- Guarded reset functionality with '.edit_mode'.
- Refactored background sync logic to properly isolate 'untrack' calls and prevent content resets.
- Refactored effects to use selective tracking and 'untrack' for all store reads.
- Implemented manual 'deep_copy' to handle Svelte 5 reactive proxies safely.
- Added concurrency locks ('is_processing') to decryption and save workflows.
- Stabilized state synchronization to prevent circular reactivity loops.
- Refactored all effects to use 'untrack' for rigorous dependency isolation.
- Implemented 'deep_copy' helper to safely handle Svelte 5 reactive proxies.
- Added concurrency locks to 'update_journal_entry' and decryption workflows.
- Completed integration of 'JournalEntry_Metadata' component.
- Refactored effects to use 'untrack' for store isolation.
- Implemented content normalization in 'has_unsaved_changes' to avoid circular triggers.
- Ensured atomic state sync after successful PATCH updates.
- Added comprehensive flow diagnostics to track effect execution.
- Cleared 'content_encrypted' from local state upon successful decryption to update UI.
- Modified background sync effect to preserve decrypted content during active sessions.
- Enhanced decryption logging for better diagnostics.
- Extracted journal entry decryption workflow to 'ae_journals_decryption.ts' to decouple it from Svelte reactivity and address loop issues.
- Updated 'ae_comp__journal_entry_obj_id_view.svelte' to use the new decryption helper.
- Refactored 'Quick Add' to use the first line as the title and remove it from the content before saving.
- Added 'ae_journals_export_templates.ts' with Markdown, HTML, and JSON support
- Refactored 'ae_comp__modal_journal_export.svelte' to use the new template system
- Optimized bulk export with automated template selection based on Journal type
- Integrated 'Import Entries' action directly into the Journal menu
- Updated project documentation to reflect portability features and implementation status
- Marked Phase 2 (Rapid Entry) and Phase 3 (Content Portability) as complete
- Logged new Auto-Save functionality in Phase 4
- Updated TODO.md and module documentation to reflect current state
- Added 'auto_save' preference to journals store
- Added toggle and status indicators (Check/X/Loader) to JournalEntry_Header
- Implemented auto-save logic with debounce in Journal Entry view
- Added 'ae_journals_parsers.ts' with Standard, Personal Log, and Amazon Vine parsers (ported from Python)
- Created 'AeCompModalJournalImport' for file selection, preview, and API submission
- Integrated Import button into Journals list view
- Created AeCompJournalEntryQuickAdd for high-velocity note creation
- Extracted robust append/prepend logic from List View into AeCompModalJournalEntryAppend
- Unified List and Detail views to use the shared modal for content manipulation
- Added explicit Append/Prepend actions to Journal Entry settings menu
- Updated TODO.md and Journals module documentation
Binding to an undefined value when a $bindable prop has a default fallback causes a runtime error in Svelte 5 runes mode. Defaults are now applied within the component logic.
- Implement new Svelte 5 Person, Address, and Contact form components with surgical payload logic.
- Refactor core routes (People, Addresses, Contacts) to support unified Create/Edit workflows.
- Update ae_types.ts, db_core.ts, and db_journals.ts to align with V3 backend object models.
- Fix type safety issues in Journal history views and refine metadata display.
- Migrate person core functions to the newer ae_core__person module.
- Synchronized Lucide icon imports with template usage.
- Removed all redundant local state and legacy crypto comments.
- Verified stable integration of modular AITools, ObjectFlags, and MetadataFooter.
- Standardized Svelte 5 logic for better maintainability and token efficiency.
- Extracted complex View/Edit logic into standalone JournalEntry_Editor.svelte.
- Integrated modular editor with bindable state and editorView.
- Maintained support for CodeMirror, Plain Text, and Rendered HTML modes.
- Simplified main view by removing another 300+ lines of template logic.
- Added a dedicated gear button to open the AI modal directly to the Settings tab.
- Resolved the 'chicken and egg' problem where settings were inaccessible if the AI process failed.
- Improved layout consistency with inline-flex wrapper.
- Implemented 'Demo Mode' fallback when no API token is provided.
- Ensured the AI modal opens even on connection errors to display troubleshooting info.
- Improved user feedback with simulated loading states for demo data.
- Enhanced AE_AITools with a Settings tab for model, prompt, and parameter configuration.
- Connected AE_AITools to Journals state via two-way bindings for persistent configuration.
- Removed redundant AI settings from Journals config modal.
- Standardized Svelte 5 patterns for cross-module component configuration.
- Created AE_ObjectFlags.svelte for standardized flag management across all modules.
- Created AE_MetadataFooter.svelte for unified creation/update/original timestamp display.
- Modularized Journal Entry view by replacing manual flag and footer logic with generic elements.
- Improved code reusability and reduced main component complexity.
- Created reusable AE_AITools.svelte in src/lib/ae_elements for system-wide AI features.
- Refactored Journal Entry view to utilize the generic AI toolset.
- Cleaned up redundant module-specific AI logic and modal code.
- Standardized Svelte 5 patterns for AI summary results.
- Extracted AI summarization logic and Modal into standalone JournalEntry_AITools component.
- Applied Prettier formatting to main component for consistent structure.
- Removed further blocks of legacy script and template code.
- Re-verified stability of core features (decryption, saving, metadata).
- Restored accidentally removed article tag and corrected syntax errors.
- Cleaned up extensive blocks of redundant comments and legacy code in the script and template.
- Refined component structure to serve as a lean template for other Aether modules.
- Re-verified all core UI logic (edit mode, decryption, data sync).
- Cleaned up redundant imports, legacy script stubs, and dead code.
- Removed extensive legacy comments from history and AI summary template sections.
- Verified stability of core decryption and data sync logic.
- Standardized file structure to serve as a better template for other Aether modules.
- Extracted AI Tools and Metadata into dedicated Svelte 5 components (JournalEntry_AITools, JournalEntry_Metadata).
- Standardized iconography to Lucide across list and detail views.
- Refined sort order controls with improved styling and Lucide icons.
- Fixed 'OpenAI is not defined' ReferenceError by restoring necessary imports.
- Corrected component naming discrepancy for Journal_entry_obj_file_li.
- Hardened change detection using Svelte 5 .by for reliable Save button behavior.
- Restored sync_config__event_pres_mgmt in ae_events__event.ts to resolve import errors.
- Removed unused OpenAI import in journal entry view.
- Removed unused Html5QrcodeScannerState import in QR scanner component.
- Improved build consistency for staging and production environments.
- Implemented 3-state (View/Eye/Save) toggle in journal entry header with Lucide icons.
- Hardened change detection logic using Svelte 5 .by for reliable UI updates.
- Improved header responsiveness and scaling across device sizes.
- Commented out experimental CodeMirror toolbar to maintain stability while keeping the helper code for reference.
- Completed the unified type system in ae_types.ts covering all 42 active Aether objects.
- Scaffolded missing core logic modules for Organization, EventTrack, and Sponsorship with standardized return types.
- Updated project roadmap in TODO.md to reflect mission completion on foundational type parity.
- Replaced dangerous custom dispatch override with EditorView.updateListener.
- Resolved 'Uncaught TypeError: child is undefined' by allowing CodeMirror to manage its own update cycle.
- Improved Svelte state syncing for new_content.
- Added formatting toolbar to journal entry editor with support for bold, italic, headers, and lists.
- Standardized iconography to Lucide across Journals module, removing legacy FontAwesome.
- Improved responsiveness and dark mode compatibility in layout and list views.
- Refactored CodeMirror component to support external control via editorView binding.
- Hardened security by removing unnecessary @html tags in journal names.
- Added detailed fields for ae_EventPerson, ae_EventRegistration, ae_AccountCfg, and others to ae_types.ts.
- Replaced local interfaces in ae_journals__journal.ts with unified imports.
- Standardized Promise return types for all core data loading, creation, search, and update functions in Journal module.
- Finalized migration for primary Identity and Journal objects.
- Added ae_EventExhibit and ae_EventExhibitTracking interfaces to ae_types.ts.
- Replaced local interfaces in ae_events__exhibit.ts with unified imports.
- Standardized Promise return types for all core data loading, creation, and update functions in Exhibit and Tracking logic.
- Integrated triple-ID patterns for lead retrieval and booth management components.
- Integrated ae_Post, ae_PostComment, ae_Page, and ae_Archive types into logic files.
- Replaced local interfaces in ae_posts__post.ts and ae_posts__post_comment.ts.
- Standardized Promise return types for all CRUD and loading functions in Post and Presentation modules.
- Synchronized event presentation logic with V3 triple-ID patterns.
- Added ae_EventFile, ae_EventDevice, ae_EventAbstract, and ae_Organization to ae_types.ts.
- Replaced local interfaces in EventFile and EventDevice logic files with unified imports.
- Standardized Promise return types for all core data loading, creation, search, and update functions.
- Integrated triple-ID patterns for Dexie and V3 API parity across files and devices.
- Added ae_HostedFile, ae_HostedFileLink, ae_DataStore, ae_Post, ae_PostComment, ae_Page, ae_Archive, and ae_ArchiveContent to ae_types.ts.
- Replaced local interfaces in core__hosted_files.ts and core__data_store.ts with unified imports.
- Replaced local interfaces in ae_archives/*.ts with unified imports.
- Standardized Promise return types for all core data loading, creation, search, and update functions across migrated modules.
- Synchronized storage and archival modules with Backend V3 field naming and Triple-ID patterns.
- Added ae_EventLocation, ae_EventSession, ae_EventPresenter, and ae_EventTrack to ae_types.ts.
- Replaced local interfaces in logistics logic files with unified imports.
- Standardized Promise return types for all core data loading, creation, search, and update functions.
- Integrated triple-ID patterns for Dexie and V3 API parity across locations, sessions, and presenters.
- Added ae_Address and ae_Contact interfaces to ae_types.ts.
- Replaced local interfaces in Address and Contact logic files with unified imports.
- Standardized Promise return types for all core data loading, creation, and update functions in migrated modules.
- Refined field optionality to align with frontend CRUD requirements.
- Added ae_User and ae_ActivityLog interfaces to ae_types.ts.
- Replaced local interfaces in User and ActivityLog logic files with unified imports.
- Standardized Promise return types for all core data loading, creation, and update functions.
- Verified permission field alignment for User type to support hierarchical access logic.
- Updated ae_types.ts with ae_Event, ae_EventBadge, and ae_EventBadgeTemplate interfaces.
- Standardized return types (Promise<ae_Event[]>, etc.) for all core loading and CRUD functions in Events module.
- Integrated triple-ID patterns for Dexie and V3 API parity in event-related objects.
- Cleaned up local interface redundancies in favor of unified imports.
- Updated ae_types.ts with detailed fields for ae_Person, ae_Site, and ae_SiteDomain based on V3 backend exports.
- Replaced local interfaces in ae_core__person.ts and ae_core__site.ts with unified imports.
- Added explicit Promise return types to all core data loading and CRUD functions in Person and Site modules.
- Standardized triple-ID pattern and return signatures across core identity and configuration modules.
- Created src/lib/types/ae_types.ts to serve as the central repository for Aether object interfaces.
- Standardized interfaces to include the triple-ID pattern (id, [type]_id, [type]_id_random) for Dexie and V3 API compatibility.
- Migrated Account and JournalEntry modules to use ae_Account and ae_JournalEntry types.
- Added explicit Promise return types to all core data loading, creation, and update functions in migrated modules.
- Unified and hardened get, post, patch, and delete helpers with standardized retry logic, kebab-case headers, and V3 response envelope handling.
- Implemented robust 'Bootstrap Paradox' resolution logic across the API stack to handle unauthenticated site domain lookups safely.
- Enhanced API helpers to support custom fetch injection, enabling reliable server-side execution in SvelteKit.
- Upgraded /testing page into a comprehensive System Testing Dashboard for core helper and V3 search verification.
- Updated TODO.md and GEMINI.md with 2026-01-08 session learnings and 'Frontier Journals' vision.
- Changed default 'enabled' and 'hidden' filters to 'all' for activity logs
- Refactored qry__activity_log to avoid sending empty 'and' arrays
- Ensured consistent filtering between API function and UI
- Updated qry__activity_log to support filtering by person_id
- Created /core/activity_logs standalone page for monitoring system actions
- Enhanced Person detail page with 'Recent Activity' column showing real data
- Added 'Activity Logs' card to the Core Management dashboard
- Created .editable_fields.ts files for Journals, Events, and Sponsorships modules
- Standardized the whitelist pattern across the entire codebase
- Updated TODO.md and GEMINI.md with recent accomplishments and learnings
- Ensured 'account_id' is injected into post objects during processing to maintain IndexedDB filter consistency
- Resolved race condition by awaiting database clearing before refreshing posts
- Corrected 'archive_on' date comparison logic in BB component
- Exported 'qry__post' and enabled comment fetching during post search
- Updated GEMINI.md and TODO.md with project progress
- Resolved Svelte 5 / SvelteKit SSR errors by adding browser checks for window.postMessage and Dexie database operations
- Prevented side effects on global state during detail page preloading by refactoring people/[person_id]/+page.ts to use shallow copies
- Implemented full V3 CRUD support, detail pages, and editable_fields for Address and Contact modules
- Enhanced Event and Post search to support filtering by person_id, enabling real related data in the Person detail view
- Fixed missing onMount import in Person detail component
- Restored 'conference' search filter in ae_events__event.ts (now supported by backend)
- Maintained local filtering as a safety fallback
- Finalized Bulletin Board (Posts) V3 migration
- Added editable_fields for all remaining IDAA-related core objects
- Migrated Post and Post Comment modules to Aether API CRUD V3
- Added editable_fields for Posts and Post Comments
- Implemented local filtering for 'archive_on' in BB route as V3 workaround
- Verified IDAA Archives and Recovery Meetings are functional on V3
- Ensured all IDAA logic follows the API -> Processor -> DB Save pattern
- Implemented workaround for 'conference' field search restriction in V3 by using local filtering
- Optimized post_object to stop retrying on 4xx client errors (400, 401, 403)
- Migrated Archives and Event/Recovery Meeting modules to Aether API CRUD V3
- Added editable_fields definitions for Archive and Archive Content objects
- Implemented lookup_site_domain_v3 in ae_core__site.ts using Search API
- Refactored root +layout.ts to use the new V3 lookup logic
- Standardized top-level data initialization with V3 response metadata
- Improved security by transitioning to standardized V3 API patterns
- Fixed broken import path in /core dashboard
- Simplified core layout data requirements to prevent crashes
- Implemented V3 CRUD and Dexie tables for Addresses and Contacts
- Created placeholder management pages for /core/addresses and /core/contacts
- Added 'Linked Activity & Content' section to Person detail page to show related events and posts
- Added dynamic route for user details with manager-level access
- Implemented permission management UI (Super, Manager, Admin, Verified)
- Added account status toggles and timestamp visibility
- Integrated with V3 User CRUD logic and editable field whitelist
- Renamed person route to people for consistency
- Refactored /core dashboard to use standard <a> links
- Added common management <nav> to core layout
- Replaced goto() with standard links across all list and detail pages
- Fixed 500 error caused by broken import path and strict layout data
- Created Comp_person_search component for dashboard integration
- Improved reactivity in person results table using derived liveQuery
- Implemented User Account Linking UI in Person detail page
- Added logic to filter and link unlinked users to person records
- Modernized Core Dashboard with person search integration
- Completed V3 migration for Event Badge CRUD operations
- Implemented User module V3 logic and editable fields
- Created management routes for Accounts, Sites, Users, and Lookups
- Updated Site Domain logic to use 'fqdn' and show 'access_key'
- Modernized Core Dashboard with navigation cards
- Restored Dexie User table definition
- Created V3 logic files: ae_core__account.ts, ae_core__site.ts, ae_core__person.ts
- Standardized on API -> Processor -> DB Save pattern
- Added editable_fields definitions for Account, Site, Person, and Activity Log
- Updated Activity Log module to use V3 wrappers
- Updated Dexie schema with account, site, and site_domain tables
- Added V3 tests to testing page
- Added 'Test Journal Module Soft Deletes' to the testing page.
- Verified 'disable' and 'hide' methods for journal entries.
- Ensured all Journals CRUD operations utilize the standardized V3 wrappers.
- Documented successful end-to-end testing of the new API patterns.
- Added update_ae_obj_v3, update_nested_obj_v3, delete_ae_obj_v3, and delete_nested_ae_obj_v3.
- Refactored Journals and Journal Entries modules to utilize the new V3 API wrappers.
- Standardized data processing and IDB caching for all CRUD operations in Journals.
- Updated testing page with comprehensive V3 CUD test buttons.
Refactor how is ensured in badge objects when fetched from the API and cached locally.
Previously, directly injected into results, creating an inconsistency with other data fetching functions. This change centralizes the injection logic within .
- Updated to accept an optional parameter.
- Added logic within the processor to set and on badge objects if they are missing, using the provided context.
- Modified call sites (, , ) to pass the to the function.
This ensures consistent and robust handling of the across badge-related data operations, aligning with the project's data processing patterns.
- Centralize project-wide documentation into a new /documentation directory.
- Remove old, deprecated README guideline files.
- Create a comprehensive AETHER_API_OBJECTS.md file detailing all API data models.
- Added new README.md for the v3 Event Leads module at `src/routes/events/[event_id]/(leads)/README.md`, adapting content from its legacy counterpart.
- Created placeholder README.md files for the main v3 Events module (`src/routes/events/[event_id]/README.md`), the v3 Launcher module (`src/routes/events/[event_id]/(launcher)/README.md`), and the v3 Badges module (`src/routes/events/[event_id]/(badges)/README.md`).
- Moved legacy event modules (`events_badges`, `events_leads`) to `src/routes/legacy/` and renamed them to `events_badges_v2` and `events_leads_v2` respectively.
- Created new directory structures for Svelte 5 and SvelteKit-based event modules under `src/routes/events/[event_id]/`:
- `(launcher)`
- `(badges)`
- `(leads)`
- Updated `src/routes/events_leads/README.md` (now `src/routes/legacy/events_leads_v2/README.md`) with comprehensive documentation for the legacy lead retrieval module, reflecting its features, data model, routing, components, and technical implementation details, incorporating project conventions.
- Updates `GEMINI.md` with a detailed summary of the days debugging session for the v3 Badges page.
- Updates `TODO.md` to mark recent bug fixes as complete and adds new tasks for outstanding issues (e.g., `order_by_li` fix).
- Updates `ARCHITECTURE.md` to reflect the current state of the tech stack (Tailwind v4, component library status).
- Creates `README.md` files for the new v3 Badges module and the legacy Lead Retrieval module to clarify their purpose and status.
This commit addresses several critical issues preventing the new v3 badge search page from functioning correctly.
- **Fixes API Fetch Error:** Resolves a `TypeError: NetworkError` that occurred during badge searches. The error was caused by an incorrect `order_by_li` parameter being sent to the API. The parameter is now temporarily commented out in `ae_events__event_badge.ts` to allow searches to succeed.
- **Fixes Svelte 5 Binding Error:** Implements a defensive initialization for badge search filter properties directly in the `(badges)/badges/+page.svelte` script. This prevents a `props_invalid_value` runtime error by ensuring that bound store values are not `undefined` when the child search component is rendered.
- **Fixes Invalid Attribute Name Error:** Corrects the `onsubmit|preventDefault` syntax in the new badge creation and upload forms. The `preventDefault` logic is now handled inside the respective submit handler functions.
- **Restores Site-wide Styles:** Re-adds the global Skeleton UI CSS imports to `app.css` to fix numerous "unknown utility class" errors (e.g., `preset-tonal-*`) and restores button styles in the legacy leads list.
- **Refactors Badge Search Component:** Updates the `ae_comp__badge_search.svelte` component to use its own reactive props for state management instead of relying on global stores, and removes obsolete/conflicting logic.
This commit addresses several critical issues that were preventing the application from building and rendering correctly.
- **Restores Skeleton UI CSS:** Re-adds the global Skeleton UI CSS imports to `app.css`. This fixes numerous "unknown utility class" errors (e.g., `preset-tonal-secondary`) across the site, allowing components to render with their intended styles again.
- **Fixes Invalid Attribute Name Error:** Corrects the `onsubmit|preventDefault` syntax in the new badge creation and upload forms (`ae_comp__badge_create_form.svelte`, `ae_comp__badge_upload_form.svelte`). The `preventDefault` logic is now handled inside the respective submit handler functions, resolving the Svelte v5 parsing error.
- **Fixes Svelte 5 Binding Error:** Implements a defensive initialization for badge search filter properties directly in the `(badges)/badges/+page.svelte` script. This prevents a `props_invalid_value` runtime error by ensuring that bound store values are not `undefined` when the child search component is rendered.
- **Fixes Invalid CSS Classes:** Replaces incorrect `preset-tonal-*` classes in the legacy `leads_list.svelte` component with standard Tailwind CSS utility classes to prevent further styling-related errors.
- Initialize ds_loaded properties in ae_stores.ts to fix svelte binding error.
- Make db_save_ae_obj_li__ae_obj in core__idb_dexie.ts schema-aware to fix Dexie DataError when saving to tables with non-'id' primary keys.
- Implemented a whitelist for editable fields for the 'event' object type to prevent sending read-only fields in POST/PATCH requests.
- Created a new file to define the editable fields.
- Modified and to use this whitelist.
- Removed the temporary cleaning logic from the event settings page.
- Corrected Svelte 5 to in event settings components.
- Updated Dexie interfaces for badge, badge_template, and device to use string IDs.
Added a summary of the new event settings page to GEMINI.md, documenting the form-based UI, collapsible sections, view toggles, and Svelte 5 reactivity improvements.
This commit introduces form-based UI components for 'mod_badges_json' and 'mod_abstracts_json', and expands the form for 'mod_pres_mgmt_json'. It also adds view toggles to switch between form and raw JSON editing for each of these sections, providing greater flexibility for the user.
This commit introduces a new component for editing basic event fields and adds collapsible sections to the event settings page for better organization. The new fields include code, conference, name, summary, description, timezone, start/end datetimes, and notes.
Added a new 'Development Guidelines' section to GEMINI.md to provide clear instructions on Svelte v5 conventions and the project's 'id' vs 'id_random' usage. This will serve as a reminder to avoid common pitfalls and ensure code consistency.
Corrected event settings components to use Svelte 5 bindable props for two-way data binding. This ensures that changes in child form components (ae_comp__event_settings_form.svelte, ae_comp__event_settings_pres_mgmt_form.svelte) are reactively reflected in the parent page (settings/+page.svelte) and properly handled during save operations.
This commit introduces form-based UI components for editing the 'cfg_json' and 'mod_pres_mgmt_json' fields on the event settings page. This provides a more user-friendly experience than editing the raw JSON directly. It also fixes an issue where the JSON objects were being displayed as '[object Object]'.
This commit introduces a new page at /events/[event_id]/settings that allows users to view and edit the JSON configuration fields for an event. The page provides textareas for each config field and a 'Save' button to persist the changes.
Updated .prettierrc to set 'useTabs' to false and 'tabWidth' to 4, ensuring that Prettier formats code with 4-space indentation instead of tabs, as per user preference.
This commit fixes the event list on the /events page by correcting the data fetching logic. It also adds links to the 'Pres Mgmt', 'Badges', 'Leads', and 'Launcher' modules for each event, and updates the main event link to use the random string ID.
This commit refactors the IndexedDB schema to use the string-based random IDs as primary keys instead of the numeric auto-incrementing IDs. This change affects the 'badge' table and all related components that interact with it. The badge detail page and list component have been updated to use the new primary key for more efficient and consistent data retrieval.
This commit updates the badge list to use the string-based random IDs for badge links. It also updates the badge detail page to correctly fetch the badge data using the random ID from the URL.
This commit fixes several issues with the new badge search functionality. It resolves a 500 Internal Error by moving the Dexie liveQuery into an onMount block to prevent server-side execution. It also fixes a Svelte 5 reactivity issue by using a more explicit subscription pattern for the liveQuery and by correctly accessing state variables in the template. The badge search results are now correctly displayed.
Renamed Tiptap_editor to CodeMirror_wrapper and updated all usages. Renamed the wrapper file to element_codemirror_editor_wrapper.svelte. Fixed a TypeError in the CodeMirror editor buttons by using EditorSelection.range() to correctly create selection ranges.
This commit addresses several issues related to the migration from TipTap to CodeMirror:
- **CodeMirror Initialization Fixes:**
- Resolved 'Unrecognized extension value' errors by refactoring to explicitly import individual CodeMirror extensions instead of relying on . This ensures proper singleton usage and prevents module duplication issues.
- Updated and to utilize these individual extensions.
- **Text Wrapping Enabled:**
- Added to the extensions in and to enable text wrapping in the CodeMirror editors.
- **Content Saving Fixes:**
- Corrected content binding for CodeMirror editor instances in various IDAA components:
- (description, location_text, attend_text)
- (content, notes)
- (content)
- (description, notes)
- (description, notes)
- Ensured that the prop of is correctly bound to the respective state variables in the parent components, and these state variables are initialized with existing content.
- **Save Button Enablement:**
- Fixed an issue in where the Save button was not enabling on content changes. The logic now directly compares the and with the original object's content, ensuring reactivity.
Resolved a critical parsing error in leads_view_lead.svelte due to incorrect Svelte class directive syntax.
Addressed multiple svelte/no-at-html-tags linting errors across the following files to mitigate potential XSS vulnerabilities and improve code safety:
- src/routes/events_leads/exhibit/[slug]/leads_add_scan.svelte
- src/routes/events_leads/exhibit/[slug]/leads_manage.svelte
- src/routes/events_leads/exhibit/[slug]/leads_view_lead.svelte
Replaced {@html} blocks with safer Svelte conditional rendering ({#if}) and direct interpolation ({value}) for static and dynamic content where appropriate. Removed commented-out {@html} tags that were still triggering linting errors.
Migrated the ESLint configuration to the new flat config format ()
and addressed several initial linting errors.
Key changes include:
- Updated ESLint configuration to treat as warnings instead of errors.
- Fixed errors in by declaring and .
- Corrected error in by using instead of an out-of-scope .
- Resolved error in by replacing the undefined directive with the component.
- Addressed errors in by replacing with and with .
- Fixed errors in by importing necessary modules (, , ) and adding missing props (, , , , ).
This commit fixes a bug where presentations, presenters, and files related to a session were not being displayed. The issues were caused by incorrect property names ( suffix) and improper use of Svelte 5 features after a recent refactoring.
This commit introduces a major refactoring of the data processing logic across multiple modules (events, archives, posts, sponsorships) to use a standardized pattern with . This improves consistency and maintainability.
Key changes:
- Replaced module-specific data processing with a generic helper.
- Removed deprecated functions.
- Updated Svelte components to leverage the new Svelte 5 runes, simplifying state management.
- Fixed linting errors and updated test configurations.
- Added .
This commit fixes a bug where the session view was not displaying correctly due to incorrect usage of reactive props.
- In , the prefix was added to the props passed to the component to make them reactive.
- In , the prefix was removed from the variables in the template, as they are already reactive when using Svelte 5 runes.
- The block for was corrected to use the resolved value.
This commit fixes a bug where presentations were not being displayed correctly within the event session view. The issue was caused by an incorrect query that was using the standard session ID to filter presentations, instead of the random session ID.
The liveQuery in has been updated to use the from the derived variable. This ensures that the query correctly fetches all presentations associated with the session.
Reverted changes to legacy launcher links in various components. These links are intentionally structured to work with nginx location aliases and should not be modified to conform to SvelteKit routing.
The session content was not loading in the new Events Launcher because the loaded session data was not being correctly assigned to the events store.
This commit fixes the issue by correctly assigning the loaded session object and its nested properties to the events store.
This commit introduces the initial setup for the `ae_app_3x_llm` branch.
The purpose of this branch is to explore and implement more direct
collaboration with AI Large Language Models (LLMs) for complex code
changes, including the creation of new function libraries and the
modification of existing code.
This initial commit includes:
- GEMINI.md: A file to provide context to the AI about the project.
- TODO.md: A to-do list for upcoming development tasks, created in
collaboration with the AI.
- **Icons:** Standardizing on **Lucide-Svelte**. Avoid legacy Font-Awesome where possible.
### Reactivity Patterns (The "Aether Way")
- **Svelte 5 Runes:** Use `$state`, `$derived`, and `$effect`.
- **Navigation Shield:** Use SvelteKit's `page.url` searchParams as the single source of truth for navigation-driven selection. Sync to global stores only via `untrack()` within effects.
- **Side-Effect Purge:** Keep `liveQuery` observables pure. Do NOT update global stores ($events_slct) inside derived observables to prevent infinite reactivity loops.
- **Dexie LiveQuery:** Use the `$` prefix (e.g., `$lq__obj`) consistently. Results from `liveQuery` are observables.
- **Initialization:** Always initialize reactive state (`$state`) outside of `$props()` destructuring.
### Performance & Data
- **SWR (Stale-While-Revalidate):** Never block SvelteKit `load` functions with API calls for cached data. Let the UI render instantly from IndexedDB and update reactively.
- **Triple-ID Pattern:** Map `id`, `[obj_type]_id`, and `[obj_type]_id_random` consistently.
- **ID Vision:** The backend uses String IDs. Primary keys should use `_id` or `id` (String) mapping to backend `id_random`.
## 🧠 Recent Strategy & Patterns
- **Safe Handover (Native):** Rename `.tmp` to `.file` ONLY after SHA-256 verification in Electron.
- **Envelopes:** API helpers automatically handle the `{data: ...}` envelope returned by the backend.
- **Bootstrap Paradox:** Use unauthenticated bypass (`x-no-account-id: "Nothing to See Here"`) for initial site/domain lookups.
- **Sev-1 Incident Recovery (2026-02-13):** Purged redundant/misplaced headers (`x-aether-api-token`, `Access-Control-Allow-Origin`). Unified all CRUD helpers to standard `/v3/crud/...` paths.
- **Account ID Scavenging:** Core fetch helpers now proactively read `account_id` from `localStorage` (`ae_loc`) if missing from config. This is the mandatory fix for Svelte 5 hydration race conditions where `onMount` triggers API calls before global stores are synced.
- **V3 Event File Mapping (2026-02-19):** Mapped prefixed backend fields (`hosted_file_hash_sha256`, `hosted_file_size`) to flat properties (`hash_sha256`, `file_size`) in the data layer. Required `inc_hosted_file=true` query param for full metadata retrieval.
- **Launcher Location Discovery:** Resolved missing locations in room select by ensuring `load_ae_obj_li__event_location` requests `hidden: 'all'` during background sync and initial load.
## 🤝 Coordination & Continuity
- **Handshake:** Use the `message` tool to notify the Backend Agent of UI/Data requirements.
- **Active Tasks:** Track your progress in `documentation/AGENT_TODO.md`.
- **Reference:** See `README.md` for build/deploy steps and `TODO.md` for project milestones.
The deployment is fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built inside a clean Docker container using `vite build --mode <env>`, which reads the corresponding `.env.<env>` file for `PUBLIC_` variables.
## Environments
| Environment | Env file | Vite mode | API server |
# Build Docker image and restart container locally
npm run build:docker:dev
npm run build:docker:test
npm run build:docker:prod
# Deploy to remote server (SSH → linode.oneskyit.com → deploy.sh)
npm run deploy:remote:test
npm run deploy:remote:prod
```
### Technical Details
- **Unified Orchestration**: All services (API, UI, Redis) are managed via `~/OSIT_dev/aether_container_env/docker-compose.yml`.
- **Dockerfile**: Multi-stage build. Stage 1 (builder) runs `vite build --mode $BUILD_MODE` using `.env.$BUILD_MODE`. Stage 2 (runtime) creates the final lightweight Node image.
- **Environment Handling**:
-`PUBLIC_` variables are baked into the image at build time via the `.env.<mode>` file.
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
- **Remote deploy**: `aether_container_env/deploy.sh` handles git pull + Docker build + restart on the server. Triggered via `npm run deploy:remote:*`.
### Client-Side Cache & IDB Version Management
The app uses Dexie (IndexedDB) as a local cache for API data (SWR pattern). To prevent
stale cached records from persisting across deploys, two version-tracking systems exist
in `src/lib/stores/store_versions.ts`:
**localStorage store versions (`AE_LOC_VERSION`, etc.)**
Track the schema of persisted Svelte stores (`ae_loc`, `ae_events_loc`, etc.).
Bump when a store's shape changes in a breaking way (field type change, required rename).
The check runs synchronously at module import time, before any store hydrates.
**IDB content versions (`IDB_CONTENT_VERSIONS`)**
Track the content shape of Dexie table rows — specifically what `properties_to_save`
writes to each table. Bump when `properties_to_save` in an object file changes in a way
that makes existing cached rows stale (fields added/removed/renamed, computed field behavior
changed). The `check_and_clear_idb_table()` helper reads a localStorage key per table and
clears the Dexie table on mismatch. Call it from the module's layout on mount.
**When to bump `IDB_CONTENT_VERSIONS`:**
If you change `properties_to_save` in `ae_events__event.ts` (or any other object file),
bump the matching entry here. Failure to do so has historically caused silent "no data"
states that are extremely difficult to diagnose — stale rows pass silently, filter to zero,
and the error looks identical to a genuinely empty result.
Currently wired: `events.event` (via `src/routes/idaa/(idaa)/+layout.svelte`).
All other tables are defined but not yet wired — see the comment block in `store_versions.ts`.
---
# svelte app
## Developing (Local HMR)
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
For the best developer experience with Hot Module Replacement (HMR), start a local development server on your host machine:
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
The local dev server will communicate with the **FastAPI backend running in Docker** (typically via `dev-api.oneskyit.com`). This gives you the speed of local Svelte development with the power of the full Aether stack.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
# Rebuild the node_modules directory and manually install extra Svelte packages
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
## Building and running in production mode
To create an optimised version of the app:
Run the npm update to fix the node_modules directory and package.json
```bash
npm run build
npm list
npm outdated
npm update
npm outdated
npm list
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start":"sirv public --single"
```
## Using TypeScript
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
Other installs?:
Are both still needed? I know at least one of these is. 2024-07-23
- Best Rich Text Editor, made for Svelte Developers with Tiptap
- ShadEditor is "evolving" to be Edra.
- ShadCN is still stuck on Tailwind 3. Waiting to upgrade to Tailwind 4.x. Tailwind 4.x was released in late January 2025. ShadCN is still being worked on as of late March 2025.
The application uses standard SvelteKit `.env` files for build-time configuration (specifically for `PUBLIC_` prefixed variables).
- **`.env.dev`**: Used by `npm run build:docker:dev` and `npm run build:dev`.
- **`.env.test`**: Used by `npm run build:docker:test` and `npm run build:test`.
- **`.env.prod`**: Used by `npm run build:docker:prod` and `npm run build:prod`.
- **`.env.local`**: Used during local development (`npm run dev`).
**Note:** Runtime variables (like private API keys or DB credentials) are managed in the deployment directory's `.env` file and passed to the containers via Docker Compose.
---
## Developing
Start a local development server:
```bash
npm run build
surge public my-project.surge.sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Deployment
```bash
# Build Docker image locally and restart container
npm run build:docker:dev
npm run build:docker:prod
# Deploy to remote server (linode.oneskyit.com)
npm run deploy:remote:test
npm run deploy:remote:prod
```
These commands use the multi-stage **Dockerfile** to build the app in a clean environment and automatically restart the corresponding Docker containers.
IDAA Jitsi meetings are embedded in an iframe on the `idaa.org` website. To allow members to "break out" of the iframe (for a better experience on mobile or to use full-tab features), the app provides an "Open Meeting Externally" link.
Currently, this link is generated from `$page.url.href`, which often lacks the `key` (site access key) and `uuid` (Novi identity token) required for Aether's authentication gate, especially if the user has navigated internally within SvelteKit.
## 1. Objective
Ensure all "Breakout" and "Copy Link" actions on the IDAA Video Conferences page include the necessary `key` and `uuid` parameters.
- Provide a straightforward policy to keep build caches useful but bounded.
Recommendations
- Primary CI cache: **registry-based buildx cache** (preferred). Use a single cache ref (e.g. `ghcr.io/ORG/REPO:cache`) reused by CI builds.
- Local dev cache: use `--cache-to type=local` for fast iteration but prune periodically.
- Retention: keep registry cache for 30 days by default. Implement registry GC or lifecycle rule to delete older cache blobs.
Rotation strategy
- Option A (simple): CI always writes to the same cache ref `:cache`. Periodically (monthly) run a job to `docker pull` and `docker image rm` older tags if you use date-based tagging.
- Option B (date-tag): CI writes cache to `cache-YYYYMMDD` and a small scheduled job deletes tags older than 30 days.
Pruning commands (developer)
- Remove local build cache older than 72 hours:
```bash
docker builder prune --filter "until=72h" --force
```
- Remove all builder cache (aggressive):
```bash
docker builder prune --all --force
```
CI runner requirements
- `docker` and `docker buildx` available in runner environment.
- Registry credentials provided via CI secrets with permission to push/pull images.
Security & Secrets
- Do not store registry credentials in repo. Use CI secret storage.
This document outlines the overall architecture and key technologies used in the Aether SvelteKit frontend project.
## 1. Project Overview
The Aether project is a Svelte and SvelteKit based application, utilizing Tailwind CSS and Skeleton for styling and UI elements. It serves as the frontend UI/UX for the Aether system, which interacts with a Python FastAPI backend.
## 2. Core Technologies
- **Frontend Framework:** Svelte 5 and SvelteKit v2
- **Routing:** SvelteKit's file-system based routing.
- **Styling:** Tailwind CSS v4
- **UI Component Libraries:**
- Skeleton (Design System, Tailwind Components, Functional Components - being phased out due to conflicts with Tailwind CSS v4)
- Flowbite (Tailwind Components)
- Custom Components (a growing library of `ae_comp__*` and `element_*` components)
- **State Management:** Svelte stores, potentially with `liveQuery` from Dexie for reactive IndexedDB interactions.
### 2.1. Journals as the Canonical Frontend Pattern
The Journals module is the current frontend reference for configuration modal structure and journal-entry field semantics. When other docs disagree, Journals should be treated as the implementation target until proven otherwise.
- Entry Config modal sections now follow `Metadata`, `Status & Security`, `Privacy Flags`, `Alerts & Messaging`, and `Admin`.
-`summary` is a first-class journal-entry field and belongs with metadata.
-`alert` and `alert_msg` are separate fields: the flag and its text payload.
-`priority` is a boolean flag in the object model, while `sort` remains the numeric ordering field.
## 3. Module Structure
The Aether project is organized into several modules, categorized as Core, Extended, and Custom.
### 3.1. Official Modules
#### Core Modules
These are foundational modules essential for the application's basic functionality.
- **Accounts:** Minimal implementation.
- **Files:** Manages hosted files.
- **People:** Minimal implementation for person records.
- **Sites:** Minimal implementation for site configurations.
- **Users:** Minimal implementation for user management.
#### Extended Modules
These modules provide additional features and functionalities.
- **Archives:** Minimal implementation.
- **Events:** Includes features for Badges and Presentation Management.
- **Posts:** Minimal implementation.
- **Journals:** Manages journal entries.
#### Custom Modules
These modules are tailored for specific client needs.
- **IDAA:** Includes Archives, Bulletin Board (BB), and Recovery Meetings functionalities.
## 4. Data Storage Mechanisms
### 4.1. Local Storage
Used for client-side persistence of various application states and configurations.
-`api`: API-related settings.
-`app`: Global application settings.
-`core`: Settings and data specific to core modules.
-`<module>`: Settings and data specific to extended modules.
-`<custom>`: Settings and data specific to custom modules.
### 4.2. IndexedDB (Dexie.js)
Used for more structured client-side data storage, often for caching and offline capabilities.
-`ae_core_db`: Core database instance.
-`<module>`: Module-specific database instances.
-`<custom>`: Custom module-specific database instances (none currently defined).
## 5. Data Sorting
Standardized sorting orders are applied across various data lists.
The Aether SvelteKit frontend runs in a standard web browser in almost all cases. The Electron native app is a **very specialized exception** with a narrow, specific purpose.
### 7.1. Standard Browser (Default)
All Aether modules run in a regular browser (Chrome, Chromium, Firefox, Safari). This includes:
- Badge printing — `window.print()` works well across Chrome, Chromium, and Firefox. Chrome is recommended for onsite badge printing stations.
- All CRUD operations, event management, journals, IDAA, reports, etc.
The Electron app (`aether_app_native_electron/`) exists **solely** to support the **Events Presentation Management Launcher**. It provides OS-level capabilities that a browser sandbox cannot:
- Control of third-party presentation software (PowerPoint, Keynote, LibreOffice Impress)
- Local filesystem access for slide file management
- Hardware telemetry for connected devices
**What Electron is NOT used for:**
- Badge printing (browser works well)
- Any other Aether module
- Any general-purpose Aether functionality
The bridge is exposed as `window.aetherNative` (set by Electron's preload script). All code that calls `window.aetherNative` should degrade gracefully when it is `undefined` (i.e., in a normal browser). See: `src/lib/electron/electron_relay.ts`.
**When to assume Electron is available:** Only inside the `/events/[event_id]/(launcher)/` route group, and only when the page was loaded from the native app.
## 8. IndexedDB LiveQuery Usage
-`lq__xyz_obj`: Used for general read-only access to liveQuery results.
-`lqw__xyz_obj`: Used for forms and binding values, representing a writable snapshot of liveQuery results.
- **Note:** Care must be taken when binding to `lqw__xyz_obj` to manage updates and potential conflicts with the underlying liveQuery.
This document outlines the key data structures and their properties used within the Aether SvelteKit frontend project. It covers object properties, field definitions, and how data is managed.
## 1. Object Properties and Fields
### 1.1. Core Standard Fields
These fields are expected to be present in most Aether objects, providing a consistent base structure.
-`id`: Primary key for an object (internal use, often *returned* by the API as a randomized string value in place of the actual DB autonum).
-`id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
-`<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`).
-`code`: Short, unique identifier.
-`name`: Display name.
-`enable`: Boolean for active/inactive status.
-`hide`: Boolean for visibility.
-`priority`: Boolean/tinyint(1) ordering flag used by the object model.
-`sort`: Numeric value for ordering within a priority group.
-`group`: Categorization string.
-`notes`: General notes/comments.
-`created_on`: Timestamp of creation.
-`updated_on`: Timestamp of last update.
### 1.2. Journal Entry Fields
Journal entries use the shared object fields plus a few content-specific fields that matter in the UI and config modal.
-`summary`: Short entry summary shown in metadata and list contexts.
-`content`: Main body text for the entry.
-`alert`: Boolean flag used to highlight an entry as an alert.
-`alert_msg`: Supporting alert text shown when the alert flag is enabled.
-`private` / `public` / `personal` / `professional`: Visibility and audience flags used by the Entry Config modal.
### 1.3. Special Use Fields
Fields with specific purposes or conditional usage across different object types.
-`for_type`: Indicates the type of object this object is linked to (e.g., 'account', 'event').
-`for_id`: The ID of the object this object is linked to.
-`archive_on`: Timestamp indicating when an object was archived.
-`passcode`: A password or access code associated with an object.
-`external_id`: An identifier from an external system.
### 1.4. Configuration and JSON Fields
Fields designed to store structured data in JSON format.
-`cfg_json`: Configuration data for an object, stored as a JSON string.
-`data_json`: General purpose data for an object, stored as a JSON string.
-`linked_li_json`: A list of linked items, stored as a JSON string.
### 1.5. Special Generated Fields (Client-side)
These fields are generated on the client-side, primarily for facilitating UI logic, such as sorting. They are not typically stored in the backend database.
-`tmp_sort_1`: Temporary sort field 1.
-`tmp_sort_2`: Temporary sort field 2.
-`tmp_sort_3`: Temporary sort field 3.
### 1.6. Future Standard Fields
A list of potential future standard fields, often prefixed with `obj_`. These are conceptual and represent planned expansions to the data model.
- **Specific Sorting (e.g., for time-based events):** `type > start_date/time > code or name`
## 3. Data Storage Mechanisms
### 3.1. Local Storage
Used for client-side persistence of various application states and configurations.
-`api`: Stores API-related settings and tokens.
-`app`: Stores global application settings and preferences.
-`core`: Stores settings and data specific to core modules.
-`<module>`: Stores settings and data specific to extended modules (e.g., `journals`, `events`).
-`<custom>`: Stores settings and data specific to custom modules (e.g., `idaa`).
### 3.2. IndexedDB (Dexie.js)
Used for more structured client-side data storage, often for caching, offline capabilities, and larger datasets.
-`ae_core_db`: The primary Dexie database instance for core application data.
-`<module>`: Module-specific database instances (e.g., `db_journals` for journal data).
-`<custom>`: Custom module-specific database instances (none currently defined, but reserved for future use).
### 3.3. IndexedDB LiveQuery Usage
Dexie's `liveQuery` is used to provide reactive data streams from IndexedDB.
-`lq__xyz_obj`: Represents a read-only liveQuery result for a single object.
-`lqw__xyz_obj`: Represents a writable liveQuery result, typically used for forms and data binding.
- **Note:** When using `lqw__xyz_obj`, developers must carefully manage updates to avoid conflicts with the underlying liveQuery and ensure data integrity.
- **Style:** Strictly `snake_case` for all function and variable names.
- **Deprecated:** `camelCase` should be refactored to `snake_case`.
- **Prefixes:**
-`load_ae_obj_id__<object_type>`: For loading a single Aether object by ID.
-`load_ae_obj_li__<object_type>`: For loading a list of Aether objects.
-`create_ae_obj__<object_type>`: For creating an Aether object.
-`update_ae_obj__<object_type>`: For updating an Aether object.
-`delete_ae_obj_id__<object_type>`: For deleting an Aether object by ID.
-`db_save_ae_obj_li__<object_type>`: For saving a list of Aether objects to IndexedDB.
-`db_update_ae_obj_id__<object_type>`: For updating an Aether object in IndexedDB.
-`process_ae_obj__<object_type>_props`: For module-specific data transformation functions.
- **Deprecated:** Ambiguous `handle_` prefixes should be replaced with more descriptive `snake_case` names (e.g., `handle_submit_form` -> `submit_form`).
## 4. Object and Property Naming
- **Singularity:** Use singular nouns for objects and properties (e.g., `example.id`, not `examples.id`).
- **IDs:**
-`id`: Primary key for an object (internal use, often a UUID).
-`<object_type>_id`: Specific ID for an object (e.g., `person_id`).
-`<object_type>_id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters).
To ensure instant page transitions and a high-performance feel, the Aether platform utilizes a **Non-Blocking Load Pattern** (also known as Stale-While-Revalidate or SWR). This pattern leverages Dexie's `liveQuery` for reactive UI and SvelteKit's `load` functions for background data synchronization.
## 🚀 The Core Principle
**Never block the `load` function with API calls if the data is already being observed by a `liveQuery`.**
The page should render *instantly* using cached data from IndexedDB. Fresh data from the API should settle in the background and update the UI automatically via reactivity.
---
## ❌ Anti-Pattern (Blocking)
This pattern causes a "white screen" or "frozen UI" while the browser waits for the API response.
```typescript
// +page.ts
exportasyncfunctionload({params,parent}){
constdata=awaitparent();
constevent_id=params.event_id;
// BAD: This blocks the navigation until the API responds.
This pattern completes the navigation immediately.
```typescript
// +page.ts
exportasyncfunctionload({params,parent}){
constdata=awaitparent();
constevent_id=params.event_id;
if(browser){
// GOOD: Fire and forget.
// This function updates IndexedDB in the background.
events_func.load_ae_obj_id__event({
event_id: event_id,
try_cache: true
});
}
returndata;// Navigation completes instantly
}
```
```svelte
<!-- +page.svelte -->
<scriptlang="ts">
import{liveQuery}from'dexie';
import{db_events}from'$lib/ae_events/db_events';
// UI reacts automatically when the background task finishes.
letlq__event_obj=$derived(
liveQuery(()=>db_events.event.get(event_id))
);
</script>
{#if$lq__event_obj}
<h1>{$lq__event_obj.name}</h1>
{:else}
<p>Loading...</p>
{/if}
```
---
## 🛠️ When to use Await
Use `await` in `load` functions ONLY for:
1.**Critical Auth Checks:** If you must verify a session before even showing a layout.
2.**Parent Data:**`const data = await parent();` is necessary to build the context.
3.**Server-Side Rendering (SSR):** If the data *must* be present in the initial HTML for SEO (rare for Aether feature modules).
## 📈 Performance Gains
By adopting this pattern across the Events module, we achieved:
-**~200-500ms reduction** in perceived page load time.
-**Elimination of waterfalls** (sequential API calls).
-**Better offline support**, as the UI is always ready to show what's in the local cache.
---
## Svelte 5 Runes + liveQuery: Critical Patterns
These rules apply to all Svelte 5 runes-mode components (the entire Aether frontend). Violations here are a common source of subtle reactivity bugs and unnecessary re-renders.
### Rule 1: Use `$derived.by()` when liveQuery depends on a reactive value
**The problem:**`$derived(liveQuery(callback))` looks like it should re-run when a store value inside `callback` changes. It does NOT. Svelte tracks reactive dependencies synchronously during the expression evaluation. The `liveQuery` callback is called later inside Dexie's async context — Svelte's tracking is already finished. The dependency is never registered.
```svelte
<!-- ❌ WRONG: $events_slct.event_session_id is read inside the async callback.
Svelte never tracks it. The liveQuery is created once and never recreates
**Rule of thumb:** If the liveQuery result changes based on a reactive value (store property, `$state`, `$props`), always use `$derived.by()`. Reserve `$derived(liveQuery(...))` only for liveQueries that watch a table broadly and don't filter by a reactive value.
---
### Rule 2: Keep liveQuery closures pure (data-only)
**The problem:** Writing to a Svelte store inside a liveQuery callback runs inside Dexie's async transaction context. Svelte's reactive tracking is undefined there. The write may fire at unpredictable times and create hard-to-debug reactivity loops.
```svelte
<!-- ❌ WRONG: Store side-effect inside liveQuery async callback. -->
### Rule 3: Use cheap equality guards in `$effect` before writing to stores
Every store write in a `$effect` triggers downstream reactivity. Always guard with a comparison before writing. The cost of the comparison is always less than the cost of spurious re-renders.
**For single objects** — compare `id` + `updated_on` (O(1)):
**Never use `JSON.stringify` for equality.** It serializes the full object tree on every reactive cycle and is O(total serialized bytes).
---
### Rule 4: Always use `untrack()` when writing to stores inside `$effect`
Without `untrack()`, reading a store to check its current value inside `$effect` registers it as a dependency — the effect re-runs whenever it writes, creating an infinite loop.
**Never hardcode `log_lvl: 2` in a call-site or override `log_lvl` inside a function body.** The parameter default exists so callers can control verbosity. Overriding it forces debug logging regardless of what the caller passed.
| Anonymous | `anonymous` | Default — not signed in |
> **Note on Public vs Authenticated:** `public` is a *site-wide* unlock (anyone with the passcode). `authenticated` verifies a *specific identity*. In the hierarchy, public outranks authenticated because it implies broader site access.
---
## `$ae_loc` Store — Permission Flags
`$ae_loc` is a `persisted()` store (backed by localStorage). Key fields:
```typescript
$ae_loc.access_type// string: current access type ('anonymous', 'trusted', etc.)
// Cumulative boolean flags (true = "you have AT LEAST this level")
$ae_loc.anonymous_access// always true
$ae_loc.authenticated_access// true from authenticated and above
$ae_loc.public_access// true from public and above
$ae_loc.trusted_access// true from trusted and above ← most-used gate
$ae_loc.administrator_access// true from administrator and above
$ae_loc.manager_access// true from manager and above
$ae_loc.super_access// true only at super
// Exclusive check flags (true = "you are EXACTLY this level")
$ae_loc.trusted_check// true only if access_type === 'trusted'
$ae_loc.administrator_check// etc.
// (rarely needed — prefer the _access flags)
// Behavior flags
$ae_loc.edit_mode// boolean — user preference, see below
$ae_loc.adv_mode// boolean — advanced mode toggle
```
### Additional intermediate levels (in permission checks, not in hierarchy order)
`support`, `assistant`, `verified`, `provisional` — appear in `_access` flags but are not part of the canonical `access_level_order`. Treat as internal/intermediate.
---
## Edit Mode — Critical Rules
`$ae_loc.edit_mode` is a **user preference**, not a permission level.
**Rules that must never be broken:**
1.**Components must never write to `$ae_loc.edit_mode`** — only the system menu toggle and sign-out/permission-drop handlers may change it.
2. Edit mode is only available to `trusted` and above in 95% of modules (the toggle is hidden from lower-access users).
3. Edit mode persists across navigation — it is NOT reset by page loads or component mounts.
4. Sign-out and permission drops to below `authenticated` should reset `edit_mode` to `false`.
> **Background:** A bug was fixed (2026-02-27) where `ae_comp__badge_obj_view.svelte` was writing `$ae_loc.edit_mode = false` in a data-loading `$effect`, silently overriding the user's preference on every navigation to the badge print page.
---
## Authentication Methods
| Method | Grants | Used For |
| --- | --- | --- |
| Site passcode (`site_access_code_kv`) | `trusted`, `public`, or `authenticated` | Onsite staff and event attendees |
| AE Username + Password | `trusted` and above | Staff with AE accounts |
| Novi UUID | `authenticated` | IDAA members (Novi membership system) |
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`:
```typescript
site_access_code_kv:{
administrator: null,// highest passcode tier
trusted: null,// onsite staff passcode
public:'public1980',// example
authenticated:'auth1980'
}
```
### `x-no-account-id` — Narrow Transport Exception
`x-no-account-id` is a transport-level escape hatch that strips account context before the request leaves the frontend. It is not a permission grant and it is not a replacement for JWT or `x-account-id`.
Use it only when the request truly cannot be made account-scoped. Current legitimate cases should stay narrow:
1. Bootstrap / site-domain discovery before the account is known.
2. Explicit public or guest endpoints that do not have an account context.
3. Helper paths that intentionally need a global-default fallback.
If a request already has a valid account context, prefer `x-account-id` and let the JWT carry session identity. Treat any new `x-no-account-id` use as temporary until it is reviewed and either replaced or justified.
**Root cause discovered 2026-04:** SvelteKit `+page.ts`/`+layout.ts` load functions run *before* layout `$effect` hooks and fire during link prefetch (hover). `if (browser)` guards do NOT prevent this — they only prevent SSR. This means API calls inside these files execute before Novi auth completes, writing private IDAA data to the user's IndexedDB even for unauthenticated sessions.
**The fix — established pattern for all IDAA routes:**
1.**Load/layout `.ts` files = thin shells.** Pass URL params only. No API calls. No `if (browser)` data fetching.
2.**Data loading = `$effect` in `.svelte` files**, gated on:
```svelte
if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;
This pattern lives in `ae_comp__badge_obj_li.svelte` — move to `ae_utils` if needed elsewhere.
---
## Module-Specific Permission Patterns
### Journals — Entry Config Admin Actions
- Entry configuration admin controls are gated to `trusted_access` and above.
- `manager_access` and `administrator_access` see the Delete action, which performs a hard delete.
- `trusted_access` users see Remove instead, which follows disable semantics rather than a hard delete.
- The Admin section is the place for staff notes, enabled/default access state, and destructive entry actions; the template toggle belongs in Metadata, while visibility/audience flags remain separate.
This document is a recipe book. Copy these patterns directly. Deviate only when a component's specific purpose genuinely requires it — and document why in a comment.
---
## 1. Hero Card
*Used for: session identity, presenter identity, location identity — the top-of-page "Is this the right one?" card.*
**Rule:** Never set `bg-*` or `text-*` color classes on `<Modal>`. Let the Flowbite component + active theme handle it. Only structural layout classes (`shadow-md`, `relative`, `flex`, etc.) belong on the `class` prop if needed.
---
## 12. Muted / Secondary Text
Replace all `text-gray-*` patterns with opacity wrappers on inherited text color:
| Icon with visible adjacent text | `aria-hidden="true"` on icon, text provides meaning |
| Icon-only button (no visible text) | `aria-label="Description"` on the `<button>` |
| Icon used as bullet point | `aria-hidden="true"` on icon |
**Never use `<i>` tags.** Always `<span class="fas ...">`.
---
## 16. Native `<select>` Dark Mode
Browser-native `<select>` and `<option>` elements **cannot be reliably styled** with Tailwind `dark:` utilities — the browser controls `<option>` rendering and ignores most CSS overrides. This causes the "light on light hover" bug in dark mode.
**Fix — add `color-scheme` directive to force OS-level dark styling:**
```svelte
<script>
import { ae_loc } from '$lib/ae_core/ae_stores';
</script>
<!-- Forces browser to render the select widget in dark/light OS mode matching your theme -->
**Why this works:** `color-scheme: dark` instructs the browser to use its native dark-mode widget rendering (dark `<select>`, dark `<option>` backgrounds). It's the only cross-browser mechanism that affects `<option>` hover colors.
**Alternative — replace with custom Skeleton/Flowbite component** if you need full styling control (e.g., color-coded options, icons). Native `<select>` is acceptable for simple purpose dropdowns with the `color-scheme` fix above.
**Store reference:** `$ae_loc.dark_mode` — boolean, set by the theme engine in `ae_stores.ts`.
This codebase is **fully Svelte 5 runes mode**. No Svelte 4 syntax.
### The basics
```svelte
<scriptlang="ts">
// Props — with optional two-way binding
interfaceProps{count?: number;label: string;}
let{count=$bindable(0),label}:Props=$props();
// Reactive state
letvalue=$state('');
letupper=$derived(value.toUpperCase());
// Side effects (replaces onMount + $: reactive)
$effect(()=>{
console.log('value changed:',value);
return()=>{/* cleanup */};
});
</script>
```
### What NOT to use (Svelte 4 patterns — do not introduce)
```ts
// ❌ No writable() stores for component state
import{writable}from'svelte/store';
// ❌ No reactive declarations
$: doubled=count*2;
// ❌ No onDestroy for cleanup — use $effect return instead
onDestroy(()=>cleanup());
```
### `$bindable()` vs `$state()`
- Use `$bindable()` when the parent needs two-way binding on a prop.
- Use `$state()` for local component state with no external binding.
### Store reactivity trap (important for `$effect`)
The app uses `svelte-persisted-store` (Svelte 4 contract) for `$ae_loc`, `$ae_api`,
`$ae_sess`, etc. In Svelte 5 `$effect`, reading **any field** of a Svelte 4 store
subscribes to the **entire store**. This means unrelated writes to `$ae_loc`
(e.g. iframe height, SWR reload) will re-trigger your effect. Be conservative about
what you read from these stores inside `$effect` blocks. See `PROJECT__Stores_Svelte5_Migration.md`
for the long-term fix plan.
For search pages specifically, this usually means:
- keep true user preferences in persisted local state
- keep transient triggers, loading flags, and last-executed search keys in session state when possible
- let the page effect schedule the search, but put the duplicate-execution guard inside the search executor so page-load auto-search still runs after hydration
- if the search text or filters are mirrored from localStorage on mount, expect that mount-time writes can re-trigger the effect unless the executor has its own guard
### `{#await}` blocks
```svelte
{#awaitsomePromise}
<LoadingSpinner/>
{:thenresult}
<div>{result}</div>
{:catcherror}
<ErrorMessage{error}/>
{/await}
```
---
## 5. V3 API Patterns
### SWR (Stale-While-Revalidate) — the standard load pattern
Return cached Dexie data immediately, refresh from API in background.
**How to verify after any change to the error block:** confirm that a `TypeError` still
produces up to 5 retry attempts with 2s→4s→6s→8s delays before returning false. A single
`return false` after the first network failure means the retry loop is broken.
**Also:** when reviewing these files, check that all four have:
-`ae_auth_error.set()` triggered on 401/403 (shows session-expired banner to the user)
-`timeout = 20000` default (was 60s in PATCH/DELETE until 2026-05-21 — 5-min worst case)
-`did_timeout_abort` flag per attempt (separates helper timeouts from caller aborts)
14.**Account-scoped `liveQuery` trigger firing before bootstrap completes** — components
that load account-specific data via `liveQuery` must not trigger the API fetch until the
bootstrap Sync Effect in `+layout.svelte` has set the real `account_id`.
**What happened:**`element_data_store.svelte` triggered its load when `entry` was falsy.
On a fresh load with no IDB cache, `$ae_api.account_id` was still `null` (bootstrap hadn't
run yet). The `localStorage` scavenge in `api_get_object.ts` then read the stale
`account_id = 1` from a previous dev/demo session and made the API call with the wrong
account. The response was cached in IDB, and the next page load showed the wrong account's
record.
A second failure mode: if IDB _did_ have a cached record from a previous session with a
different account, `liveQuery` returned it as a valid hit (`entry` truthy), so the trigger
never fired to fetch the correct record.
**The fix pattern** for any trigger `$effect` that depends on bootstrapped account context:
```typescript
$effect(() => {
// Use $slct.account_id (non-persisted), NOT $ae_loc.account_id (persisted, stale).
// $slct is initialized to null and set only by the bootstrap Sync Effect, so it
// reliably gates the fetch until bootstrap has completed.
const account_id = $slct.account_id;
const api_ready = !!$ae_api?.base_url;
const entry = $lq__ds_obj as SomeType | null | undefined;
if (!browser || !account_id || !api_ready) return;
// Also re-fetch when IDB holds a record from a different (non-null) account.
// null account_id = global/shared fallback — that is still a valid cache hit.
const entry_is_stale_account =
entry !== undefined &&
entry !== null &&
entry.account_id !== null &&
entry.account_id !== account_id;
if (!entry || entry_is_stale_account) {
trigger = 'load...';
}
});
```
**Why `$slct` not `$ae_loc`:**
`$ae_loc` is a `svelte-persisted-store` — it hydrates from `localStorage` before any
effects run, so its `account_id` may be a stale value from a previous session. `$slct`
is a plain writable store initialized to `null`; the bootstrap Sync Effect is the only
thing that sets it. Until that runs, `$slct.account_id` is `null`, providing a reliable
gate. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → "Bootstrap Race" for the Dexie-side
context.
15. **`tmp_sort_*` comparators written descending instead of ascending** — `build_tmp_sort()` encodes `priority=true` as `'0'` and `priority=false` as `'1'`, designed for **ascending** sort so priority items appear first. Writing a JS `.sort()` comparator as `b.localeCompare(a)` (descending) inverts the encoding and sends priority items to the bottom.
Found in journals (2026-06), IDAA recovery meetings fast-path and API re-sort (2026-06), and as a Dexie anti-pattern in BB post comments.
list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
```
**Companion Dexie trap:** `collection.reverse().sortBy('tmp_sort_*')` — Dexie ignores a collection-level `.reverse()` when `.sortBy()` is called. The sort is always ascending. To reverse the result, call `.reverse()` on the returned array after `await`. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md` → `build_tmp_sort` section.
**Exception — legacy `ae_events__event.ts` encoding:** `ae_events__event.ts` (and `ae_events__event_session.ts`) do NOT use `build_tmp_sort`. They use `priority ? 1 : 0` (priority=true→`'1'`), which requires **descending** sort to put priority items first. `ae_events__event_presentation.ts` DOES use `build_tmp_sort` (it overrides the generic encoding in its `specific_processor`). Do not apply the ascending rule to raw event or session sorts until those modules are migrated to `build_tmp_sort`.
16. **Service worker without `skipWaiting()` + `clients.claim()` silently serves stale code to long-lived tabs** — The default SvelteKit service worker template does NOT include these calls. Without them, a new SW installs in the background but waits in a **"waiting"** state until every tab running the old version is closed before it activates. Users who leave a page open all day (especially IDAA members in the Novi iframe on idaa.org) run old buggy JS indefinitely after a fix is deployed.
**Symptom that should trigger this check:** Bug reports from users that developers cannot reproduce. Developers constantly refresh and open/close tabs — the new SW activates immediately for them. End users with persistent tabs never get it.
**The fix** (already applied to `src/service-worker.js` as of 2026-06-03):
```js
self.addEventListener('install', (event) => {
event.waitUntil(addFilesToCache());
self.skipWaiting(); // activate immediately, don't wait for tabs to close
});
self.addEventListener('activate', (event) => {
event.waitUntil(deleteOldCaches());
self.clients.claim(); // take control of all open tabs right away
});
```
**Trade-off:** A tab mid-session gets new JS without a page reload. For a read-heavy app like IDAA (browsing meetings) this is harmless. For a form-heavy app the risk is higher — weigh accordingly.
**Last Updated:** 2026-05-18 (Default limit and stepper update)
---
## ⚠️ CRITICAL PRIVACY REQUIREMENT
**ALL IDAA content is PRIVATE. Authentication is required for ALL modules.**
IDAA serves a sensitive population — physicians in addiction recovery. Content exposure to the public is a **severe security failure** and a violation of member trust.
- A previous AI agent accidentally exposed IDAA Bulletin Board content publicly. This must never happen again.
- Every route, component, and API call in this module must enforce authentication.
- When in doubt: **it's private**.
**Required access level:**`trusted_access` or higher for all IDAA content.
---
## What IDAA Is
IDAA is a private membership organization for physicians in recovery. They use the Aether platform for:
- A private document archive (historical materials, meeting records)
- A members-only bulletin board (community posts and discussion)
- A searchable directory of in-person and virtual recovery meetings
- Video conferencing (Jitsi-based)
IDAA's Aether instance is embedded as an **iframe inside their existing Novi-powered website** (`idaa.org`). Novi is their external Association Management System (AMS) — it handles membership records and authentication. Aether receives the member context via URL parameters on iframe load.
### Breakout Links and Iframe Persistence
Members often need to open Jitsi meetings outside the Novi iframe (e.g., for full-screen features or on mobile). These are referred to as **Breakout Links**.
- **The Problem:** SvelteKit client-side navigation within the iframe often drops "bootstrap" query parameters like `?key=...` (site access key) and `?uuid=...` (Novi identity token).
- **The Requirement:** When a member breaks out of the iframe into a new browser tab, these keys **must** be present in the URL. Without them, the member will hit the site-domain gate or the IDAA auth gate and see "Access Denied."
- **The Solution:** The Video Conferences page uses a derived `breakout_url` that proactively re-injects the missing `key` (from `$ae_loc.allow_access`) and `uuid` (from `$idaa_loc.novi_uuid`) before generating the external link.
> **Note:** Recovery Meetings has **two UI entry points**:
> 1. **Modal pattern** (primary list flow) — list, view, and edit components live at `recovery_meetings/`
> level, toggled via `$idaa_sess.recovery_meetings` session flags (`show__modal_view`, `show__modal_edit`).
> 2. **Direct page** (`[event_id]/+page.svelte`) — navigating to `/idaa/recovery_meetings/<id>` renders
> the same view/edit components gated by `$idaa_sess.recovery_meetings.edit__event_obj`.
>
> Both patterns use `ae_idaa_comp__event_obj_id_edit.svelte`. The edit form clears **both**
> `show__modal_edit` and `edit__event_obj` on save/cancel so it works correctly from either entry point.
---
## Authentication: Novi UUID System
IDAA members do not log in through Aether — they log in through Novi (idaa.org), and Novi passes their identity to the Aether iframe via URL parameters.
### URL Parameters (on iframe load)
```
?uuid=<36-char-uuid>
&iframe=true
&key=<site-access-key>
```
> **Security note (2026-03-09):** The iframe HTML files previously also passed `email` and `full_name`
> via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed.
> The SvelteKit layout now verifies identity via the Aether server-side Novi proxy — the Novi API
> call originates from the server, not the member's browser.
> See "Iframe Integration" → "Novi UUID Verification Flow" below.
### Verification Flow (`(idaa)/+layout.svelte`)
When a `uuid` param is present in the URL, the layout performs an **async call to the Aether server-side endpoint** (`GET /v3/action/idaa/novi_member/{uuid}`), which proxies to Novi server-to-server:
1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
2. Gets verified name and email — these can't be forged via URL
4. Sets `$idaa_loc.novi_verified = true` on success
A `novi_verifying` UI state prevents the "Access Denied" screen from flashing during the API round-trip.
**All or nothing:** If the Novi API key is not configured on the site, or the verification call fails, access is denied. There is no URL-param fallback.
"novi_api_root_url":"https://www.idaa.org/api",// optional, this is the default
"novi_admin_li":["uuid-1","uuid-2"],
"novi_trusted_li":["uuid-3","uuid-4"],
"novi_idaa_group_guid_li":["group-uuid"]// Jitsi moderators only
}
```
## Novi API Integration — How We Use It
This section documents the exact way Aether uses the Novi API for the IDAA integration so future maintainers can recreate the flow.
- **Purpose:** Verify a Novi-provided `uuid` received via iframe URL parameters, obtain a verified name/email from Novi, and upgrade Aether permissions for that session when appropriate.
- **All-or-nothing policy:** If the Novi API key is not configured or the verification call fails, the Novi-based access path is denied. The layout explicitly prevents child routes from rendering while verification is in-flight to avoid flashing "Access Denied".
- **Rate limits (Novi API):** 20 calls/second · 600 calls/minute · 100,000 calls/day. The Aether backend handles 429 responses; the frontend receives a `429` and retries once after 10 seconds. The 12-hour TTL cache on successful verification (Redis server-side + `$idaa_loc` client-side) prevents repeated calls during normal use. A `503` (Novi unreachable) is auto-retried once after 3 seconds before surfacing an error to the user.
### Verification Flow (implementation)
1. The IDAA iframe loads Aether pages with a `?uuid=<uuid>&iframe=true` param.
2. When the `uuid` param is present the IDAA layout calls the Aether server-side proxy:
// Aether calls Novi server-to-server; member's browser IP is never in the Novi call path.
```
3. On success (`200`), the layout reads `data.full_name` and `data.email` from the response and writes them to the IDAA store, marking verification success.
4. The layout then determines a target Novi permission level (`authenticated`, `trusted`, `administrator`) by checking configured UUID lists (`novi_trusted_li`, `novi_admin_li`) and upgrades the Aether session only if the Novi-derived level is higher than the current global level.
5. The layout also resets a few IDAA-specific query defaults (BB filters, etc.) to safe values after verification.
### Key `site_cfg_json` fields and where they are used
- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Used by the Aether **server** to authenticate against Novi — the frontend never touches the key itself. The frontend checks only for its *presence* in `site_cfg_json` as a guard meaning "IDAA is configured for this site". If missing, Novi-based access is denied.
- **`novi_api_root_url`**: Optional Novi API root (defaults to `https://www.idaa.org/api`). Read by the Aether server, not the frontend.
- **`novi_admin_li`**: Array of UUIDs treated as administrators for IDAA. Merged into `$idaa_loc.novi_admin_li` during layout initialization and used to set `administrator` level.
- **`novi_trusted_li`**: Array of UUIDs treated as trusted members. Merged into `$idaa_loc.novi_trusted_li` and used to set `trusted` level.
- **`novi_jitsi_mod_li` / `novi_idaa_group_guid_li`**: Lists used to map Jitsi moderator privileges and group GUIDs (where applicable).
- **`novi_bb_base_url`**: (optional) Base URL used to build links for Bulletin Board notification emails.
- **`jitsi_exclude_uuids`**: (optional) Array of Novi UUIDs to exclude from Jitsi Reports.
This is the canonical staff/test filter. UUIDs are matched case-insensitively against
`final_participants[].novi_uuid` when present. Example: `["uuid-1", "uuid-2"]`.
- **`jitsi_known_meetings`**: (optional) Array of meeting names / room names to keep in the report.
When this list is non-empty, only matching `room_name` values are shown. Matching is
case-insensitive.
- **Legacy fallback:** `jitsi_exclude_names` is still honored for older configs, but it should be
migrated to UUIDs.
- **Email config values** (`noreply_email`, `noreply_name`, `admin_email`, `admin_name`): used by functions that send notification emails (BB posts, comments, recovery meetings).
-`$idaa_loc.novi_full_name` — display name built from Novi fields
-`$idaa_loc.novi_verified` — boolean flag indicating successful verification
-`$idaa_loc.novi_admin_li`, `$idaa_loc.novi_trusted_li` — merged lists from site config
These fields are read elsewhere in the IDAA UI to enable flows for verified users (for example: creating meetings, posting comments, or auto-populating contact info in notifications).
### Where in the codebase this runs (examples)
- The Novak UUID verification and permission-upgrade logic is implemented in the IDAA layout: [src/routes/idaa/(idaa)/+layout.svelte](src/routes/idaa/(idaa)/+layout.svelte).
- UI elements that permit actions for verified Novi users or trusted members check these values. Example: the "Create New Meeting" button allows creation when either the session has `trusted_access` or a `novi_uuid` is present — see [src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte](src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte).
### Security notes and operational guidance
- The previous implementation leaked `email` and `full_name` via URL params — this was removed because those values are unauthenticated and can be spoofed.
- The API key is sensitive — keep it only in site `cfg_json` and do not expose it in client-side code or public repositories. The key is read and used exclusively by the Aether backend; it is never sent to the browser.
- If Novi changes their customer API shape, update `app/methods/idaa_novi_verify_methods.py` in the backend (display name/email normalization) and this documentation.
If you need a compact checklist for re-creating this flow in another integration, ask and I will add a small runbook with exact request/response field mappings.
### ~~Planned: Server-Side Novi Verification~~ ✅ Implemented (2026-05-19)
**Problem solved:** The previous client-side Novi API call originated from the member's browser.
Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering
could block these calls and produce false "Access Denied" for legitimate members.
**Solution implemented:** A FastAPI endpoint proxies the Novi call server-to-server
(Aether → Novi), with Redis caching. Members' browser IPs are no longer in the call path.
- Standard Aether auth headers (`x-aether-api-key`, `x-account-id`)
- Server reads `novi_idaa_api_key` / `novi_api_root_url` from site `cfg_json`
- Redis cache: `idaa:novi_member:{uuid}` — 4-hour TTL, only 200s cached
-`404` results never cached (recently-joined members not incorrectly denied)
**Frontend:**`verify_novi_uuid()` in `(idaa)/+layout.svelte` now calls this endpoint with
standard Aether headers. The `novi_idaa_api_key` is still checked for presence in
`site_cfg_json` as a proxy for "is IDAA configured for this site" (server holds the key itself).
**Full API spec:**`GUIDE__AE_API_V3_for_Frontend.md` §12.
### Permission Levels (Ascending)
| Level | Condition | Access |
|---|---|---|
| Anonymous | No UUID, unrecognized UUID, or verification failure | No access |
| Authenticated | UUID verified against Novi API | View own content, limited actions |
| Trusted | Verified UUID in `novi_trusted_li` | Full member access to all IDAA content |
| Administrator | Verified UUID in `novi_admin_li` | Full access + edit/manage |
`novi_trusted_li` and `novi_admin_li` are managed in Aether site config (not in Novi directly).
## Identity Linkage: The Novi UUID Rule (Triple Linkage)
**CRITICAL ARCHITECTURAL STANDARD:**
All member-generated content in the IDAA module MUST be explicitly linked to the member's Novi UUID via the `external_person_id` field. This linkage is the primary mechanism for ownership, edit permissions, and auditing.
### 1. Mandatory at Creation
Linkage MUST happen at the moment of initial object creation (POST). Shell records created without an `external_person_id` are considered orphaned and may be inaccessible to the creator.
### 2. Triple Linkage Scope
The following objects require mandatory `external_person_id` linkage:
- **Recovery Meetings** (`ae_Event`)
- **Bulletin Board Posts** (`ae_Post`)
- **Post Comments** (`ae_PostComment`)
### 3. Implementation Patterns
- **Buttons:** Creation buttons (e.g., "Create New Meeting") must include `external_person_id: $idaa_loc.novi_uuid` in their initial `create_ae_obj` payload.
- **Edit Forms:** Edit components must provide robust fallbacks to `$idaa_loc.novi_uuid` for new or incomplete records, ensuring identity is captured even if the initial creation call was narrow.
- **Identity Sync:** Along with the UUID, `full_name` and `email` should also be synced from `$idaa_loc` to provide human-readable context in notifications and admin views.
- **Race Condition Defense:** `$idaa_loc` may be briefly null on mount before the store hydrates from localStorage. Creation buttons and edit submit handlers must scavenge identity directly from `localStorage.getItem('ae_idaa_loc')` as a fallback when the store value is missing.
IDAA staff have their own Novi UUID. When they edit member content, their identity must **not** overwrite the member's `external_person_id`, `full_name`, or `email`.
| Content Type | `external_person_id` for staff | `full_name` / `email` for staff |
|---|---|---|
| BB Post | **Readonly** (unless `administrator_access`) — member's UUID preserved | Same — rendered from existing record, not staff identity |
| Post Comment | **Preserved** — form state initializes from existing record first | Same |
| Recovery Meeting | **Intentionally editable** for trusted staff — staff can reassign meeting ownership | Contact 1 renders from existing `contact_li_json[0]` first; staff identity only fills if blank |
The fallback to `$idaa_loc.novi_uuid` (the current user's UUID) only fires when the record has **no** existing `external_person_id`. For any record properly created after the 2026-04-07 triple-linkage enforcement, this fallback should never be reached.
### 5. Recovery Meetings — Contact 1 Convention
In 99% of cases, **Contact 1 should be the same person linked via `external_person_id`** — the IDAA member who owns and runs the meeting. These are two separate fields:
-`external_person_id` — the ownership/identity link (Novi UUID). Determines who may edit the meeting.
-`contact_li_json[0]` — the displayed contact info (name, email, phone). Shown to members searching for meetings.
They are expected to match but are set independently. Members unlock Contact 1 via confirm dialog if they need to list a different contact. Staff can edit both fields directly.
### Permission Upgrade Rule
```
// RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade.
// If a user has a higher global Aether role (site manager, super),
// their global role is preserved and not overwritten by Novi auth.
```
This ensures that OSIT staff with `super` or `manager` roles retain full access regardless of Novi UUID status.
### Non-Novi Sign-in Paths (unaffected)
- **User/Pass or Auth Link:** No `uuid` in URL → layout Novi block does not run
- **Shared Passcode:** No `uuid` in URL → layout Novi block does not run
### Access Gate (`(idaa)/+layout.svelte`)
The inner layout blocks ALL rendering if the user is not authorized:
- Replies inherit the parent post's visibility rules
### Post Visibility / Archival Filter
Posts with `archive_on` set to a past date are **automatically hidden** from all queries. This is enforced at the component level via a LiveQuery filter:
```typescript
// This filter is REQUIRED — do not remove it
filter((x)=>!x.archive_on||archiveDate>now)
```
Archived posts are soft-deleted — they remain in the database for audit purposes but are not shown to members.
Most recent first (sorted `updated_on DESC`).
### Database (Dexie)
```
db_posts.post — Posts (threads)
db_posts.comment — Post comments (linked by post_id)
**Types:**`ae_Event` (standard event type, filtered for meeting context)
Recovery Meetings reuses the Aether Events object to represent AA recovery meetings. These are NOT conferences — they are regular ongoing meetings (weekly, monthly, etc.) available to IDAA members.
### Search Filters
Members can filter meetings by:
- **Fulltext search** — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
- **Meeting type** — IDAA / Caduceus / Family Recovery
- **My Meetings** — star toggle; shows only meetings the member has starred (favorites)
**Sort options:** Last Updated (default), Meeting Name A–Z, Meeting Name Z–A.
**Empty state behavior:**
- Zero results with active filters → "No meetings found for these filters" + "Clear all filters" button
- Zero results with no filters → bare message shown, then after 8s a "Refresh Meeting Cache" escape hatch appears (clears IDB and re-fetches from API — indicates a stale-cache problem, not a real empty set)
Search uses the standard Aether SWR pattern (IDB cache returned immediately, then API refreshes in background).
### Search Architecture — What Is and Isn't Searched
The fulltext search runs against the `default_qry_str` field (backend-computed STORED GENERATED
**Rich text fields** all use `AE_Comp_Editor_TipTap` with separate `*_new_html` state variables
(not bound to `$idaa_slct.event_obj` directly) to track change state for the save-button logic.
**Zoom URL auto-generation:** Triggered by `$idaa_trig = 'update_zoom_full_url'`. An `$effect`
reconstructs `attend_json.zoom.full_url` from domain + meeting_id + passcode_enc whenever
the Meeting ID, Passcode, Encrypted Passcode, or Domain fields change.
**Recurring text auto-generation:** If `recurring_text` is blank or contains the `*gen*` prefix,
the submit handler generates a human-readable string (e.g., `*gen* weekly: Monday, Wednesday at 7:00 PM America/Chicago`).
Members can opt into a custom text via "Add More Details?" (admin/trusted only).
**Contact 1 lock:** Contact 1 name and email default to the logged-in Novi member's identity
(`$idaa_loc.novi_full_name`, `$idaa_loc.novi_email`). They are `readonly` unless the user
explicitly unlocks them via confirm dialog (or has administrator access).
### Jitsi Integration
Some virtual meetings are hosted via Jitsi. Members with a Jitsi moderator UUID (`novi_jitsi_mod_li`) have elevated permissions in video sessions.
### Edit Form — Implementation Notes (v2)
- The v2 edit form uses a `<style>` block with `@apply`. Tailwind v4 requires
`@reference "../../../../app.css";` at the top of any component `<style>` block that uses `@apply`.
- The country subdivision lookup list (`lu_country_subdivision_list`) contains duplicate entries —
specifically Puerto Rico (`PR`) has two rows with `code = '-'`. The `{#each}` key must use
the array index (`i`) rather than `sub.code` to avoid a Svelte `each_key_duplicate` error.
The duplicate entries are a **backend data quality issue** that should be cleaned up in the DB.
### Demo / Test IDs
No dedicated IDAA recovery meeting demo records — uses the standard Event demo record for dev:
- Event: `pjrcghqwert` (id: 1) "Demo One Sky IT Conference"
---
## Module 4: Video Conferences (Jitsi)
**Route:**`/idaa/video_conferences/`
Embeds Jitsi video conferences directly in the IDAA module. Separate from Recovery Meetings — this is for IDAA board meetings or special sessions, not regular AA meetings.
Moderation permissions are controlled by `novi_jitsi_mod_li` in the IDAA store.
---
## Module 5: Jitsi Reports
**Route:**`/idaa/jitsi_reports/`
**Access:**`trusted_access` or `novi_verified` — same gate as the rest of `(idaa)/`
**Data source:**`activity_log` table — `jitsi_meeting_event` and `jitsi_meeting_stats` log types
**Library function:**`qry__jitsi_report()` in `src/lib/ae_reports/reports_functions.ts`
An admin/staff reporting tool that aggregates raw Jitsi activity logs into human-readable meeting sessions. It is **not** a member-facing page — IDAA members do not see it.
**Reminder:** this page now filters staff by Novi UUID and can whitelist known meeting names from site config.
### View Modes
Two display modes, toggled via a button in the page header:
| Mode | Description |
| --- | --- |
| **Grouped by Room** (default) | One collapsible section per `room_name`. Each section contains a compact table: Date / Time / Duration / Attendees / Participant List. Mirrors the output of the offline Python script (`create_jitsi_report.py`). |
| **Flat List** | Original card-per-session accordion layout. Better for drilling into event timelines and raw participant lists. |
Both modes use the same filtered data set — switching views does not reset filters.
### Dark Mode / Surface Safety
The page now uses explicit page and row surfaces so dark mode does not collapse into white-on-white
text in either the regular app or the Novi iframe.
### Filters
| Filter | Default | Logic |
| --- | --- | --- |
| **Min. Participants** | 2 | Minimum `real_participant_count` to display a session. Used as the only size filter. |
| **Room Name** | edit mode only | Case-insensitive substring match against `room_name`. Hidden unless AE global edit mode is on. |
| **From / To** | last 60 days / today | Date range applied to `start_time`. "To" date includes the full end of day. |
A "Reset Filters" button appears whenever any filter is non-default.
In edit mode, two extra toggles appear:
- **Show excluded IDs** — temporarily include the UUIDs listed in `jitsi_exclude_uuids`
- **Show all meetings** — temporarily ignore `jitsi_known_meetings`
An "Active Exclusions" panel below the filter bar shows the currently applied Novi UUID exclusions
and known meeting-name whitelist values. Each list is collapsible so the page stays compact.
### Staff / Meeting Filtering
**Problem:** Staff/test accounts and one-off test rooms distort the reports.
1. The page reads `$ae_loc.site_cfg_json?.jitsi_exclude_uuids` and excludes matching participants by Novi UUID.
The UUID comes from the Jitsi log `url_params.uuid` field. `g_uuid` is the meeting/group UUID and is not used here.
2. If a participant record does not include a UUID in the activity log, it is left visible; UUIDs are used whenever available.
3.`real_participant_count = real_participants.length` drives filters, exports, and the per-meeting attendee count.
4. Room-level unique participant counts are computed from Novi UUIDs when present, with display-name fallback only for UUID-less records.
5. If `$ae_loc.site_cfg_json?.jitsi_known_meetings` is non-empty, only meetings whose `room_name` matches one of the listed names are shown.
6. The Room Name filter is only shown when global edit mode is enabled.
**Temporary stopgap:** the report also hides these staff display names through the same UUID-exclusion toggle until the long-term logging fix lands:
`Scott I.`, `Brie P.`, `Michelle V.`
**Note:** matching is case-insensitive on the stored `room_name` / meeting name.
### Summary Stats
Shown above the meeting list when data is loaded. Stats reflect the **filtered + exclusion-applied** view:
- **Meetings Shown** — count of sessions passing all filters
- **Total Participants** — sum of `real_participant_count` across all shown sessions
- **Avg Duration** — mean session duration (HH:MM:SS)
- **Total Duration** — sum of all session durations (HH:MM:SS)
In grouped view, each room header also shows its own subtotals (meeting count, unique participants by Novi UUID when available).
Each meeting instance keeps the full participant list visible; the **Copy names** button is edit-mode only so staff can grab the list for follow-up reports without exposing extra controls to normal viewers.
### Caching / Load Behavior
The page now reads cached `activity_log` rows from IndexedDB first, renders that result immediately,
then refreshes from the API in the background. That keeps the report usable even when the network
round-trip is slow.
Both the cache path and the API refresh now page through the matching activity-log set in
`created_on DESC` order with a 1000-row page size before building the report. That avoids the old
"first 500 rows" behavior that could hide newer sessions if the log table grew large.
The report page keeps the newest session first in both the flat list and the grouped-by-room view;
grouped room rows are also sorted newest session first within each room.
### Jitsi URL Builder
Collapsible panel, visible to `trusted_access` users only. Generates properly-formatted Jitsi meeting URLs for IDAA rooms. Component: `ae_idaa_comp__jitsi_url_builder.svelte`.
### Video Conferences → Reports Link
Trusted Access users now get a footer link on the Video Conferences page that jumps back to the Jitsi Reports page. It preserves the current iframe context so the staff workflow stays inside the Novi embed.
**Future idea:** make that link include a `room=` query param for the current meeting so Jitsi Reports can auto-filter to that meeting instance, and have Reset clear that param again.
### Export
CSV and JSON export buttons in the page header export the **currently filtered + exclusion-applied** data set.
### Room Name Fragmentation
The same logical meeting can appear as multiple rooms (e.g. `IDAA-BIPOC-Meeting`, `IDAA-BIPOC-Meeting-2026`, `IDAA-BIPOC-Meeting-March-31`) because the Jitsi URL builder appends a date suffix to generate unique per-session room names. In grouped view, these appear as separate groups. A future normalization pass (strip trailing date suffixes) could optionally merge them — not implemented yet.
### Data Flow
```text
activity_log table
└── qry__jitsi_report() # reports_functions.ts — fetches + aggregates by meeting_id
**SvelteKit layout** (`(idaa)/+layout.svelte`): Calls `GET /customers/{uuid}` on the Novi API using the `novi_idaa_api_key` from `site_cfg_json`. Sets verified name/email in `$idaa_loc` and grants permissions. Shows a "Verifying identity..." spinner during the async call.
**Jitsi page** (`video_conferences/+page.svelte`): Checks `$idaa_loc.novi_verified` in `fetch_novi_data()`. If the layout already verified the user, it reuses `$idaa_loc.novi_email` / `$idaa_loc.novi_full_name` and skips the duplicate member details API call. The group moderator check (`get_novi_group_moderators`) always runs — it is Jitsi-specific.
### ⚠️ Iframe CSS Conflicts (Bootstrap v3)
When `$ae_loc.iframe = true`, the root layout (`+layout.svelte`) injects two external stylesheets from Novi's CDN:
**Last Verified:** 2026-06-03 — Recovery Meetings: documented legacy `tmp_sort_1` encoding for events (requires descending sort, not ascending); documented `$slct.account_id` gate pattern for search trigger; noted service worker `skipWaiting`/`clients.claim` requirement for long-lived IDAA iframe sessions (root cause of user-reported loading failures that could not be reproduced in dev)
This document outlines key insights and strategies developed during Svelte 5 (with runes mode) refactoring and bug-fixing tasks. It specifically addresses common pitfalls and effective patterns for an AI agent working with Svelte.
## 1. Async/Await in Svelte (Runes Mode)
The most frequent source of errors has been related to asynchronous operations and the `await` keyword.
- **Rule:** Any function or block containing `await`_must_ be explicitly marked `async`.
- This applies to callbacks within promise chains (`.then()`, `.catch()`, `.finally()`), DOM event handlers (`onclick`, `onchange`), and Svelte lifecycle functions (`onMount`).
- **Common Error:** `Cannot use keyword 'await' outside an async function`. This happens when `await` is used in a function/block that is not `async`.
- **Solution:** Ensure the surrounding function or the callback itself is declared `async`.
- The correct pattern for asynchronous navigation using `goto` in Svelte 5 is `await goto(await resolve(path), options);`.
-`import { resolve } from '$app/paths';` is crucial when using `resolve()`. A missing import will lead to `no-undef` errors for `resolve`.
-`resolve(path)` is crucial to ensure the path is correctly resolved before navigation, especially in universal applications.
- The `await` before `goto` is necessary if subsequent code depends on the navigation completing or if `invalidateAll` is used.
- **Clearing Stale Caches:** When encountering confusing linting or build errors that don't seem to match the current code, especially after significant refactorings or dependency changes, always try running `npm run clean` to clear stale build artifacts and then re-lint/re-build.
- **Refactoring Promise Chains:**
- When converting `.then().catch().finally()` chains to `async/await` structure, encapsulate the asynchronous operation within a `try...catch...finally` block.
- **Incorrect:**
```javascript
someAsyncFunc()
.then(() => { await anotherAsyncFunc(); }) // ERROR: .then() callback is not async
.catch(() => { /* ... */ })
.finally(() => { await lastAsyncFunc(); }); // ERROR: .finally() callback is not async
```
- **Correct:**
```javascript
async () => {
try {
const result = await someAsyncFunc();
await anotherAsyncFunc(result);
} catch (error) {
console.error(error);
} finally {
await lastAsyncFunc();
}
};
```
- **Note:** If the entire handler (e.g., `onclick`) is already `async`, you can `await` the promise directly within it.
## 2. Reactive Declarations & Scoping
Svelte 5's runes mode introduces new ways of managing reactivity, and understanding variable lifecycles is key.
- **`$state` for Reactive Variables:**
- Variables that are expected to trigger re-renders when their values change, or whose changes need to be observed, should be declared with `$state`.
- **Common Error:** Warnings like "This reference only captures the initial value of `data`. Did you mean to reference it inside a closure instead?" or "Variable `x` is updated, but is not declared with `$state(...)`."
- **Solution:** Declare the variable using `$state(initialValue)`.
- **Context for `data` prop:** When a `data` prop (from a SvelteKit `load` function) is accessed outside of reactive declarations (like `$effect` or event handlers), Svelte might warn that it only captures its initial value. To ensure reactivity or to correctly process it, use it within `$effect` or derive a `$state` variable from it.
- **Function Scoping and Redeclaration:**
- **Common Error:** `Identifier 'function_name' has already been declared`. This occurs when functions with the same name are defined in overlapping scopes.
- **Solution:**
- If a function is only needed within a specific block (e.g., an `onMount` callback), define it _inside_ that block to limit its scope.
- If a function is truly global and needs to be accessible from templates and multiple `onMount` blocks, define it once outside of any `onMount` and ensure it doesn't conflict with other definitions.
- Be mindful of helper functions that might be implicitly pulled into global scope by the Svelte compiler if not correctly encapsulated.
## 3. `replace` Tool Usage Strategy (Critical for AI)
My efficiency heavily relies on the `replace` tool, and precision is paramount.
- **Exact `old_string` Matching:**
- The `old_string` parameter _must_ precisely match the target text in the file, including all whitespace, indentation, newlines, and comments. Even a single character difference will cause the tool to fail with "0 occurrences found".
- For multi-line replacements, always copy the exact block from the `read_file` output.
- **Contextual Specificity:**
- Avoid generic `old_string` patterns (e.g., `onclick={() => {`) if there are many such occurrences in a file. Instead, expand the `old_string` to include enough surrounding unique context (e.g., the entire button element or parent `div`) to ensure it matches only one instance (`expected_replacements: 1`).
- **Iterative Refinement:**
- For complex refactorings involving multiple changes in a file, perform changes in small, atomic steps.
- **Always `read_file` before each `replace` operation.** This ensures the `old_string` is based on the absolute latest content of the file, preventing mismatches due to previous modifications or unexpected formatting.
- After each `replace` operation, immediately run `npm run build` (or `npm run lint`) to validate the change and catch new errors early. This is crucial for catching cascading issues introduced by partial refactorings.
## 4. Error Debugging Workflow
- **Prioritize Compiler/Build Errors:** These are blocking issues that prevent the application from running. Address them first.
- **Analyze Error Messages:** Read the full error message carefully, including line numbers, and look for keywords (e.g., `await`, `async`, `declared`, `undefined`).
- **Consult Svelte Documentation:** The Svelte compiler often provides helpful links (`https://svelte.dev/e/js_parse_error`) which should be a first point of reference if the error is unfamiliar.
- **Re-read File Content:** If a `replace` operation fails or produces unexpected results, immediately use `read_file` to verify the exact state of the file before attempting another change.
Svelte 5 enforces strict contracts for bound properties (`bind:prop`). If a component expects a property to have a fallback/default value, you cannot bind `undefined` to it.
- **The Error:** `Uncaught Svelte error: props_invalid_value. Cannot do bind:prop={undefined} when prop has a fallback value`.
- **The Cause:** Attempting to bind a variable that is currently `undefined` to a component prop that has a default value (e.g., `let { prop = false } = $props()`). Svelte cannot determine whether to use the bound `undefined` or the component's internal default, so it throws an error.
- **Data Fetching:** If the data comes from an asynchronous source (API, DB), ensure the object properties have default values *immediately* upon assignment, even before the data is fully populated.
# Aether API V3 Frontend Integration Guide (Svelte/TypeScript)
This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints.
---
## 1. Authentication and Security (Mandatory)
V3 architecture enforces strict **Multi-Tenant Isolation** and **Machine Authorization**. Requests require two levels of validation.
### A. The "Entry Ticket" (API Key)
**Mandatory for all requests.** identifies the application or client.
***Header:** `x-aether-api-key: <your_app_key>`
***Status Code:** `403 Forbidden` if missing or invalid.
### B. The "Visa" (Account Context)
Required for any non-public data (Journals, Badges, Users, etc.).
1.**Standard Access**: Provide the `x-account-id` (the random string ID).
***Header:**`x-account-id: <account_id>`
2.**Administrative Bypass**: For authorized scripts needing global access.
***Header:**`x-no-account-id: bypass`
***Scope:** Narrow escape hatch only. Keep it limited to allowlisted bootstrap/public/global-default paths and prefer `x-account-id` or JWT-backed requests everywhere else.
3.**Token Access**: Provide a **JWT** in the query string.
***Query Param:**`?jwt=<token>`
4.**Important Distinction:** A query parameter named `key` is **not** an account-context bypass signal.
*`key` may be used by specific endpoints/business logic, but it must **not** cause the frontend to remove `x-account-id`.
* Only explicit `x-no-account-id: bypass` should strip account context.
> [!NOTE]
> The `x-no-account-id` path should continue to shrink over time. If you need a new use, document why `x-account-id` or JWT cannot cover it and mark the use as temporary unless it is a hard bootstrap/global-default requirement.
> [!CAUTION]
> **UNSUPPORTED HEADERS:** The header `x-aether-api-token` is **NOT recognized** by the V3 API. If you send it, the backend will treat you as a guest and block access to private data.
---
## 2. Bootstrapping (The FQDN Handshake)
When the frontend first loads and doesn't know the `account_id`, it performs a "handshake" using its domain name.
* Returns 200 + a list containing the `account_id` (random string ID) and `site_id` (random string ID).
*** デザイン Choice:** If the domain is not found, it returns **200 OK with an empty list `[]`**. It is NOT a 404.
> **Access Key Support**
>
> Some client deployments restrict their domain via an access key passed in the browser URL (e.g. `?key=abc123`). The frontend reads this param and forwards it as `access_key` in the POST body.
***Security:** Returns 403 if the record doesn't belong to your `x-account-id`.
### B. POST Search
The primary way to retrieve data.
***Endpoint:** `POST /v3/crud/{obj_type}/search`
***Security:** Automatically filters results to only show records belonging to your `x-account-id`. If no account context is provided, it will return **0 records** for private objects.
### C. POST Create / PATCH Update
Modify data in the system.
***Endpoints:**
*`POST /v3/crud/{obj_type}/`
*`PATCH /v3/crud/{obj_type}/{id}`
***Strict Mode (Default):** The API validates your payload against the Pydantic model. If you send fields that do not exist in the model, the database might return a 400 "Unknown column" error.
***Permissive Mode (Header):** To allow the frontend to send "extra" fields (like local UI state) without causing errors, use the following header:
***Header:**`x-ae-ignore-extra-fields: true`
***Behavior:** When set to `true`, the backend will automatically strip any fields from the payload that are not defined in the object's model before attempting to save to the database.
#### `*_json` field serialization — do NOT pre-stringify in route/component code
The frontend API wrappers (`src/lib/ae_api/api_post__crud_obj.ts` for V3, `src/lib/api/api.ts` for legacy CRUD) automatically serialize any field whose name ends in `_json` (e.g. `cfg_json`, `data_json`) before sending. They pretty-print with 2-space indent via an internal `serialize_json_field_pretty()` helper.
**Pass `*_json` fields as plain JS objects from routes and components.** The serialization layer handles the rest.
```ts
// ✅ Correct — pass as plain object; V3 wrapper serializes it
The V3 wrapper (`api_post__crud_obj.ts`) only serializes when `typeof value === 'object'`, so it will not double-encode a plain string. The legacy wrapper (`api.ts`) stringifies unconditionally, so pre-stringifying there **will** produce double-encoded JSON. In both cases, the right answer is to pass the raw object and let the layer handle it.
### D. ID Fields in Responses (Vision ID Convention)
> [!IMPORTANT]
> **V3 responses always use random string IDs — never database integers.**
All V3 responses — `POST` create, `GET` single, `GET` list, search, and `PATCH` update — contain:
| Field | Type | Use |
| :--- | :--- | :--- |
| `{obj_type}_id` | `string` | **Primary public ID.** Use this for subsequent `PATCH` calls and UI routing. |
| `{obj_type}_id_random` | `string` | Legacy alias. Same value as `{obj_type}_id`. Present for backward compat only. |
> **Note on `_id_random` suffix:** The `{obj_type}_id_random` field is a legacy artifact from the pre-Vision model. Once you confirm `{obj_type}_id` is a random string (length 11–22), you do not need `_id_random` as a fallback. New code should only read `{obj_type}_id`.
---
## 4. V3 Uniform Lookup System
The V3 Lookup system provides a hierarchical, deduplicated interface for standardized reference tables (Countries, Timezones, etc.). It supports global defaults, account-level overrides, and object-level overrides, with optional site-specific whitelisting.
### How the hierarchy works
Each lookup table (`lu_v3_country`, `lu_v3_time_zone`, etc.) can hold multiple rows for the same logical item at different scopes:
| Account override | set | `NULL` / `NULL` | Global default |
| Object override | set | set | Account override + Global default |
The API uses `ROW_NUMBER() PARTITION BY group` to collapse all rows for the same item down to the single highest-priority winner before returning results. **`group` is the identity key** — it is what makes two rows "the same item competing for priority."
> [!IMPORTANT]
> **The `group` field is not a display label.** It is the deduplication key. Each lookup type uses a different natural key for `group`:
> For `time_zone`, `group` and `name` must always be identical — there is no concept of "override all US timezones as a group." Each timezone is its own identity.
### A. List Lookups
Retrieve the deduplicated, ranked list for a lookup type.
*`include_disabled` (Optional): `true` includes shadowed/disabled records (useful for admin views).
**Frontend keying:** Always key Svelte `{#each}` blocks on `group`, not `id` or `name`. `group` is guaranteed unique in the response. Keying on `id` will break if an account override wins (different `id`, same logical item).
***Usage:** Use when you have an external code (e.g., ISO `"US"`) and need the full Aether record. Scans `name`, `group`, and other identity fields.
### C. Site Whitelist Policy
To restrict which lookup items appear for a specific site, add a `lookup_policy` to `site.cfg_json`:
```json
{
"lookup_policy":{
"country":["US","CA","GB"],
"time_zone":["America/New_York","US/Eastern"]
}
}
```
> **Whitelist values must match the `group` field** — i.e., the natural key for that type (ISO code for country, IANA name for time zone). Using a display name will silently return no results for that item.
### D. Adding and managing client overrides
When a client needs a customized label or wants to hide/reorder lookup items, create override records rather than modifying global defaults.
**Rules:**
1.**Never modify global default rows** (`account_id = NULL`). Those are shared across all accounts. Any change there affects every client.
2.**Set `group` to the exact same value as the global default row** for the item you are overriding. If `group` doesn't match, the override creates a new item instead of replacing the existing one.
3.**Set `account_id`** to the client's account ID. Leave `for_type` / `for_id` null unless the override is specific to a single object (e.g., one site).
**Example — rename "US/Eastern" for one account:**
(42,'US/Eastern','Eastern Time (Client Label)','US/Eastern',1,1,50);
```
The `name_override` field is the display label the frontend should prefer when set. `group = 'US/Eastern'` ensures this row competes with — and wins over — the global default in the `PARTITION BY group` deduplication.
**To disable an item for one account** (hide it from their dropdowns):
```sql
INSERTINTOlu_v3_time_zone
(account_id,name,`group`,enable)
VALUES
(42,'US/Samoa','US/Samoa',0);
```
Setting `enable = 0` on an account-scoped row shadows the global default for that account only.
**To remove a client override** (revert to global default):
Simply delete the row where `account_id = <client>` and `group = '<item>'`. The global default row is unaffected and immediately resumes winning.
### E. Adding new global lookup items
When seeding new lookup data (e.g., adding timezones in bulk):
1. Set `group = name` for every row (for `time_zone`). This is a hard invariant — if `group` is set to a regional label like `"United States"` instead of the timezone name, the entire group collapses to a single winner and all but one entry disappear from the API response.
2. Set `account_id = NULL` and `for_type = NULL` / `for_id = NULL` for global defaults.
3. After seeding, verify with:
```sql
-- Should return 0 rows; any result means multiple items will collapse into one
SELECT `group`, COUNT(*) AS cnt
FROM lu_v3_time_zone
WHERE account_id IS NULL
GROUP BY `group`
HAVING cnt > 1;
```
---
## 5. Event File Data Retrieval (Hosted Files)
Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File itself is a metadata record for binary content (files), which is accessed via separate Action endpoints (e.g., `/v3/action/hosted_file/download`). This API endpoint provides metadata about the associated hosted file. To retrieve this additional metadata:
1. **Top-Level Convenience Fields:** The response will include top-level fields for commonly needed hosted file data. These are populated directly from the SQL view via JOINs.
* `hosted_file_hash_sha256` (string)
* `hosted_file_subdirectory_path` (string)
* `hosted_file_content_type` (string)
* `hosted_file_size` (string - in bytes)
2. **Nested Hosted File Object:** A full `hosted_file` object will be nested under the `hosted_file` key. This object (`Hosted_File_Base` model) will contain all its standard fields, including `id` (random string ID), `hash_sha256`, `content_type`, `size`, etc.
These helper endpoints let the frontend request small server-side transformations without uploading new blobs. They return a newly-created `hosted_file` metadata object on success.
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`. Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize. Returns 400 on failure.
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize.
- For longer-running clips you can schedule the job in the background by adding `?background=true`. When scheduled the API returns `202 Accepted` and the clip runs asynchronously on the server; check the returned `hosted_file` record later via the standard V3 `hosted_file` endpoints.
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
Frontend guidance:
- Call these routes with the same `link_to_type` / `link_to_id` you plan to associate the resulting hosted_file with — the server resolves random IDs for you.
- After a successful response, use the V3 `hosted_file` action endpoints (download/delete) to manage or retrieve the new file.
- These endpoints run synchronously and can take time for large inputs; for heavy or batch workloads use a queued job pattern instead.
- These endpoints may take time for large inputs. Prefer using `?background=true` to schedule work and receive a `202 Accepted` response for async processing. For heavy or batch workloads use a queued job pattern instead.
- **Auth:** include `x-aether-api-key` (if required) and account context via `x-account-id: <ACCOUNT_ID>`. Admin bypass (`x-no-account-id: bypass`) or `?jwt=<token>` are accepted per site policy.
- **Request:** `multipart/form-data` with single file field `file` (Zoom CSV). Query params:
- `begin_at` (int, default `0`)
- `end_at` (int, default `20000`)
- `return_detail` (bool, default `false`)
- Delimiter is auto-detected; Zoom CSV layout: row 1 = metadata, row 2 = blank, row 3 = headers (the backend skips the first two rows).
Behavior / notes:
- The handler forces `Registrant email` to be used as the `external_id`. `Unique identifier` is used as `external_registration_id` only when it is meaningful (placeholders like `N/A`, `NA`, `UNKNOWN` are ignored).
- Per-ticket custom fields are parsed (Organization, Job title, Phone, Address lines, City, State/Province, Postal/Zip, Country, etc.).
- Marketing-consent values are mapped to `agree_to_tc` and `allow_tracking`.
- TEMP AXONIUS MAPPING: the import temporarily defaults `event_badge_template_id` to `21` and `event_badge_template_id_random` to `RKYp2HcQm9o`. Ticket-name → `badge_type_code` mapping is applied for some labels (e.g., contains "sponsor" → `sponsor`; contains "attend"/"attendee" → `attendee`). This mapping is temporary (April 2026) — surface this to staff.
- Rows missing `Registrant email` are skipped.
- The server upserts via existing backend methods and creates/updates `event_person`, `event_person_profile`, and `event_badge` records as needed.
Frontend guidance:
- UI must be staff-only and should validate an `event_id` is selected.
- For large files, use `begin_at`/`end_at` to process in chunks.
- Prefer `return_detail=false` for large imports to reduce payload size.
Common errors:
- `403` — missing/invalid account context or API key.
- `404` — event not found.
- `500` — file save or processing error.
Example curl (replace placeholders):
```bash
curl -v -X POST "https://api.example.com/event/<EVENT_ID>/badge/import/zoom_csv?begin_at=0&end_at=20000&return_detail=false" \
Sample success (detailed, `return_detail=true`) — `data` contains full `event_person` objects with nested `event_badge` (may include temporary `event_badge_template_id`: `21` and `event_badge_template_id_random`: `RKYp2HcQm9o`).
Paste this section into the guide as a temporary Axonius-specific note (April 2026). Consider linking staff to a sample Zoom CSV for QA.
---
## 7. User Actions (`/v3/action/user/`)
Stateful user account operations that are not standard CRUD. All require `x-aether-api-key`.
> [!IMPORTANT]
> **Migration from legacy `/user/*` routes:** The table below maps each legacy endpoint to its V3 replacement. Run both in parallel during transition; remove legacy routes once traffic logs confirm they are quiet.
- `valid_email` (optional `bool`): if `true`, marks `email_verified = true` on success.
- `inc_user_role_list` (optional query param, default `false`): include role list in the returned user object.
**Response on success:** Full user object (same shape as `GET /v3/crud/user/{id}`).
**Errors:** `400` missing credentials, `403` wrong password / account disabled / account not yet enabled / account expired, `404` user not found.
> **Auth key flow:** Auth keys are one-time-use — the key is cleared from the DB immediately on successful authentication. Request a new one via `GET /v3/action/user/{id}/new_auth_key`.
---
### B. Verify Password
Check a user's current password without changing it.
The returned key can then be passed to `/authenticate` (as `auth_key`) or embedded in a login URL. The user record must have `allow_auth_key = true` for key-based authentication to work.
---
### E. Email Auth Key URL
Generate a new auth key and email a one-time login link to the user's email address.
| `root_url` | `string` | *(required)* | Base URL the login link is built from. Must be provided — if omitted the link in the email will be malformed (`None?...`). |
| `key_param_name` | `string` | `auth_key` | Query param name used for the auth key in the generated link. |
> [!IMPORTANT]
> `root_url` is **required in practice**. The FastAPI query param accepts `null` but the email builder does not guard against it — omitting it produces a broken link in the email.
**Magic link URL format (default `key_param_name`):**
The frontend at `root_url` should read these query params and call `POST /v3/action/user/authenticate` with `{ "user_id": "...", "auth_key": "..." }`. Note that `valid_email=True` is **always** injected — authenticating via a magic link automatically marks the user's email as verified.
**Response:** `data: true` on success (email sent). `404` if user not found. `500` if delivery failed — common causes: account email not configured, user `enable = false`, or `allow_auth_key = false`.
---
### F. User Lookups via V3 CRUD Search
The three legacy lookup routes (`lookup`, `lookup_email`, `lookup_username`) are replaced by standard V3 CRUD search:
> **Note:** `exhibitor_notes` has HTML tags stripped automatically for clean CSV output.
### Permission Requirement — `leads_api_access`
> [!IMPORTANT]
> This endpoint enforces a **per-exhibit permission flag**. The `event_exhibit` record **must** have `leads_api_access = true` set in the database, OR the caller must have manager-level account access (JWT with `manager: true`).
>
> If `leads_api_access` is `false` or `null` on the exhibit, the API returns:
> ```json
> { "detail": "Access denied: leads API access is not enabled for this exhibit." }
> ```
> **Fix:** Enable the flag on the exhibit record via `PATCH /v3/crud/event_exhibit/{id}` with `{ "leads_api_access": true }`, or set it directly in the database/admin panel.
#### Dual purpose of `leads_api_access`
This flag serves two related but distinct roles:
1. **3rd-party API access (original intent):** Controls whether external systems (exhibitor apps, badge-scanning devices, etc.) are permitted to push or pull lead data for this exhibit via the API.
2. **UI export gate (new):** The frontend should read `leads_api_access` from the exhibit record and use it to show or hide the export/download button. Only render the button when the flag is `true` — this prevents users from triggering a request that will always 403.
The recommended pattern is to fetch the exhibit record first and gate the UI on this field before the user ever sees the export option. The API enforces the same check server-side as a safety net.
// resp is a file blob — use URL.createObjectURL() or trigger a download
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
```
---
## 12. IDAA: Server-Side Novi Member Verification
Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether backend. This eliminates false "Access Denied" failures for members on hotel/conference WiFi, VPNs, and Cloudflare-filtered networks — the Novi call originates from the server's IP, not the member's browser IP.
- **Method:** `GET`
- **Path:** `/v3/action/idaa/novi_member/{uuid}`
- **Auth:** Standard V3 (`x-aether-api-key` + `x-account-id` or `?jwt=`)
### Request
| Parameter | Location | Required | Description |
|---|---|---|---|
| `uuid` | Path | Yes | Novi member UUID (from Novi AMS) |
### Response on success (`200 OK`)
```json
{
"data": {
"verified": true,
"full_name": "Alice S.",
"email": "alice+member@idaa.org"
}
}
```
- `full_name`: `"{FirstName} {LastName[0]}."` format. Falls back to the Novi `Name` field if first/last are absent.
- `email`: Novi `Email` field with space → `+` normalization applied (Novi quirk — `alice member@idaa.org` → `alice+member@idaa.org`).
### Error responses
| Status | Meaning | Frontend action |
|---|---|---|
| `404` | UUID not found in Novi, or Novi returned 200 with no identity data (empty-member anti-pattern — member may have just joined) | Treat as denied / not a member |
| `429` | Novi rate limit hit | Surface as `'rate_limited'`; advise retry |
| `503` | Novi unreachable or Novi 5xx error | Auto-retry once after 3s; if retry also fails, surface as `'api_error'` |
### Migration from direct Novi call — ✅ Complete (2026-05-19)
`+layout.svelte:verify_novi_uuid()` now calls this endpoint instead of Novi directly. Response code mapping (for reference):
| Direct Novi result | This endpoint returns | Frontend behavior |
|---|---|---|
| `200` with identity data | `200` | `verified` |
| `200` with no identity data | `404` | `denied` |
| `404` | `404` | `denied` |
| `429` | `429` | Auto-retry after 10s; `'rate_limited'` if retry fails |
| Network error / Novi 5xx | `503` | Auto-retry after 3s; `'api_error'` if retry fails |
### Caching
Verified results are cached in Redis (`idaa:novi_member:{uuid}`, 4-hour TTL). `404` results are **never** cached so recently-joined members are not incorrectly denied on their next attempt.
---
## 11. Troubleshooting 403 Forbidden
If you receive a 403 on a valid ID:
1. Verify `x-aether-api-key` is correct.
2. Ensure you are sending `x-account-id` and NOT `x-aether-api-token`.
3. Verify the record actually belongs to the account ID you are sending.
4. Check if the object is marked `public_read: True` in the registry. (Posts and Archive Content allow guest access; Journals and Badges do not).
5. Confirm the frontend is not treating `params.key` as an implicit bypass and stripping `x-account-id`.
6. If list/search endpoints work but `GET /v3/crud/{obj_type}/{id}` still returns 403, this is likely endpoint-level policy (e.g., requires stronger auth like JWT) rather than a transport/header bug.
This guide explains how to implement real-time communication using the **Aether API V3 WebSocket** protocol. V3 introduces granular routing, strict message schemas, and improved multi-tenant isolation compared to previous versions.
The V3 WebSocket path requires both a `group_id` and a `client_id`. Both are treated as **opaque unique strings** by the backend — no specific format is enforced.
**`group_id`** — identifies the shared channel (e.g., an event ID, a room name, or a Vision ID random string). All clients using the same `group_id` receive group-targeted messages together.
**`client_id`** — uniquely identifies this specific connection. The backend accepts any unique string (UUID, timestamp, Vision ID — no format validation). The **recommended pattern** is a UUID v4 generated once and persisted in `localStorage` so the same identity is reused across page reloads and sessions on that browser.
> Use `ws://` for local development and `wss://` in production (any HTTPS site). The Nginx config must include the Upgrade block — see Section 6.
### B. Authentication
Browsers **cannot** set custom HTTP headers on WebSocket connections. Pass the API Key and account context as **query parameters** instead:
| `sent_at` | string | Auto | ISO 8601 Timestamp. Set by server. |
> **Frontend tip:** Only send `msg_type`, `target`, and whatever content fields you need (`msg`, `cmd`, `payload`, `to_id`). The server enforces `from_id`, `group_id`, and `sent_at` from the connection context, preventing spoofing.
---
## 4. Message Targeting Logic
V3 uses the `target` field to determine which Redis channel to use, ensuring only the intended recipients receive the data.
### A. Group Broadcast
Sends the message to every client connected to the same `group_id`.
```json
{
"msg_type":"msg",
"target":"group",
"msg":"Hello team!"
}
```
### B. Direct Message (DM)
Sends the message to one specific client ID, regardless of their group.
```json
{
"msg_type":"msg",
"target":"direct",
"to_id":"target_client_random_id",
"msg":"Private message just for you."
}
```
### C. System Broadcast
Sends the message to **every** connected client on the platform (use sparingly).
```json
{
"msg_type":"cmd",
"target":"broadcast",
"cmd":"MAINTENANCE_WARNING"
}
```
### D. Echo
Sends the message back only to the sender (useful for testing round-trip latency).
```json
{
"msg_type":"msg",
"target":"echo",
"msg":"Ping!"
}
```
---
## 5. Specialized Message Types
### Commands (`cmd`)
Used for remote control or orchestration.
```json
{
"msg_type":"cmd",
"target":"group",
"cmd":"RELOAD_UI",
"payload":{"force":true}
}
```
### Heartbeats (`heartbeat`)
Keep the connection alive and **refresh presence** in the backend. Should be sent every 30-60 seconds.
- The server intercepts `heartbeat` messages and refreshes the Redis presence TTL (1 hour window) before echoing back.
- Without periodic heartbeats, a client idle for >1 hour may disappear from the presence set even while still connected.
- Use `target: 'echo'` so the server sends the heartbeat straight back — useful for measuring round-trip latency.
```json
{
"msg_type":"heartbeat",
"target":"echo"
}
```
---
## 6. Infrastructure Requirements (Nginx)
Unlike standard REST endpoints, WebSockets require explicit "Upgrade" handling in the Nginx gateway. If you are deploying to a new server, ensure the following block is present in your Nginx configuration:
```nginx
location/v3/ws{
proxy_passhttp://fastapi_backend;
proxy_http_version1.1;
proxy_set_headerUpgrade$http_upgrade;
proxy_set_headerConnection"upgrade";
proxy_set_headerHost$http_host;
proxy_read_timeout2100s;# Match your app's max heartbeat/session time
}
```
---
## 7. Common Pitfalls & Troubleshooting
- **HTTP 404 Errors**: This almost always means Nginx is missing the `location /v3/ws` block and is trying to serve the request as a static file from the disk.
- **HTTP 400 Errors**: Check your `Host` header. Nginx routes requests based on the `server_name` directive. If you connect to an IP or a non-standard hostname (like `localhost`), ensure it is explicitly listed in your Nginx config.
- **Connection Drops**: If the connection drops exactly after 60 seconds, check your Nginx `proxy_read_timeout`. It should be set high (e.g., `2100s`) to allow for long-lived WebSocket sessions.
---
## 8. Migration Guide (V2 to V3)
If you are upgrading from the legacy V2 WebSocket (`/ws/group/...`):
1.**Change the URL**: Prepend `/v3/` to your WebSocket path.
2.**Wrap your JSON**: In V2, you might have sent `{"msg": "hi"}`. In V3, this must be `{"msg_type": "msg", "target": "group", "msg": "hi"}`.
3.**Use unique string IDs**: Both `group_id` and `client_id` in the path are opaque strings — any unique value works (timestamp, UUID, Vision ID random string). Just don't use raw database integer IDs. For `to_id` in direct messages, use whatever `client_id` that target client registered with.
4.**Listen for `msg_type`**: Update your frontend handlers to switch logic based on the `msg_type` field instead of proprietary keys.
The Aether UI is calm, focused, and softly luminous. It must be immediately readable under conference-room lighting, at a glance, by presenters who are nervous and in a hurry. Staff in the Speaker Ready Room need scan-speed identity confirmation. Remote presenters uploading files from home need a clear, unambiguous interface that doesn't waste their time.
Core principles:
- **Identity first.** The user's first question is always *"Am I in the right place?"* Answer it with the hero card — name, time, room — before anything else is shown.
- **Progressive disclosure.** Admin fields (codes, IDs, passcodes) are hidden unless `edit_mode` is active.
- **Theme-aware always.** Zero hardcoded colors. Every background, border, and text color must respond to light/dark mode and the active theme via CSS variables.
- **Transitions, not pops.** Every interactive state change is smoothed with `transition-colors duration-200`.
- **Section 508 / WCAG 2.1 AA** compliance is non-negotiable. Contrast ratios, focus indicators, ARIA labels, and screen-reader regions are required everywhere.
---
## 2. Technical Stack Mandates (2026 Standard)
To maintain codebase health and performance, all new development must adhere to the following stack:
### 🚀 Svelte 5 Runes
- **Mandatory**: Use `$state`, `$derived`, and `$effect`.
- **Snippet pattern**: Use `{@render snippet()}` for reusable UI blocks within components.
- **Avoid**: Legacy `export let` (use `$props()`), `onMount` for reactive derived state (use `$derived` or `$effect`), and `$$slots` (use Snippets).
### 🎨 Tailwind 4 + Skeleton v4
- **Mandatory**: Use `preset-*` classes for interactive elements (e.g., `preset-tonal-primary`).
- **Customization**: Use Tailwind 4 `@theme` blocks for project-wide overrides.
- **URLs**: Skeleton for Svelte for LLMs docs: https://www.skeleton.dev/llms-svelte.txt
### 🔣 Lucide Icons
- **Mandatory**: Use `@lucide/svelte` components (e.g., `<Calendar size="1em" />`).
- **Migration**: Replaced all FontAwesome `fas fa-*` icons in general modules.
- **🚨 Exception: IDAA Module**: The IDAA module **must** retain FontAwesome and Bootstrap classes. It integrates with Novi CMS which relies on these legacy standards. **Do not migrate IDAA icons.**
---
## 3. The AE_Firefly Theme
**App default since 2026-03-06.** Set in `ae_stores.ts` as `theme_name = 'AE_Firefly'`.
Always wrap in `{#if $lq__obj}{...}{:else}...skeleton...{/if}` — **never** show real content structure before data exists.
---
## 7. Dark Mode Rules
- **Never write `dark:` overrides for background or text colors.** The Firefly theme handles both modes through CSS variables. Writing `dark:bg-gray-800` or `dark:text-gray-400` bypasses the theme and breaks if the user switches themes.
- **Exception allowed:** `dark:text-primary-300` and `dark:text-tertiary-300` in info chips are intentional — they reference theme variables that gracefully degrade.
- **Exception allowed:** `dark:border-surface-700` in fine-grained border work when `border-surface-200-800` isn't strong enough.
---
## 8. Accessibility (Section 508 / WCAG 2.1 AA)
| Requirement | Implementation |
|---|---|
| Decorative icons | `aria-hidden="true"` on all icons |
| Async content regions | `role="status" aria-live="polite"` on loading/empty sections |
| Focus indicators | `focus-visible:ring-2 focus-visible:ring-primary-500` on custom interactive elements |
| Interactive dialogs | `aria-haspopup="dialog"` on trigger buttons |
| Form inputs | Visible `<label>` linked via `for` / `id`, or explicit `aria-label` |
| Color-only information | Always pair color coding with icon or text — never color alone |
| Minimum touch target | 44×44px effective hit area for all tap targets |
| Button label + icon | All buttons should include **both a Lucide icon and text label**. Icon-only is acceptable for space-constrained toolbar/header actions (with `title` attribute); text-only is acceptable when layout is extremely tight. The icon+text combination aids non-English-native users who may not read the label fluently. |
---
## 9. Debug Code — Remove Before Committing
These patterns are breakpoint debuggers added during development. **Never commit them:**
```html
<!-- Breakpoint border debugger — REMOVE before commit -->
- **`opacity-*` for muted text**: Use `opacity-60` (secondary) or `opacity-40` (tertiary/hint) instead of `text-gray-*`. This works in any theme and both modes.
- **`/` opacity modifier**: `bg-primary-500/10` is preferred over separate `opacity-10` — it targets only the background.
- **`text-sm leading-relaxed`**: Standard for body-level descriptive text in cards.
- **`tracking-wide uppercase`**: Use for section label/eyebrow text with `opacity-40`.
- **`whitespace-pre-wrap`**: Required for any `<pre>` or `<p>` displaying user-entered multi-line text (preserves breaks without horizontal overflow).
---
## 12. Known Issues & Workarounds
### `btn` + `preset-filled-*` resolves to transparent inside `card` components
**Symptom:** A button using `btn preset-filled-primary` (or any `preset-filled-*`) inside a `card` div renders with `background-color: transparent`, making it invisible against the card surface.
**Root cause:** The Skeleton v4 `btn` class sets a transparent background via a CSS variable chain. When nested inside a `card` element, the `preset-filled-*` class fails to win the specificity battle and the button appears invisible. This affects both light and dark mode.
**Workaround:** Skip `btn` and `preset-filled-*` entirely for buttons inside `card` elements. Use direct Tailwind token classes instead:
**Scope:**`btn` + `preset-*` classes work correctly on standalone buttons (e.g. page headers, nav bars). The issue is specific to the `card` component context. If we migrate away from Skeleton `card`/`btn`, this issue goes away.
## 6. Inline Field Editing — `element_ae_obj_field_editor`
The standard component for single-field inline editing throughout the platform. Wraps a `PATCH /v3/crud/{obj_type}/{obj_id}` call behind a click-to-edit UI that respects `$ae_loc.edit_mode`.
```svelte
import Element_ae_obj_field_editor from '$lib/elements/element_ae_obj_field_editor.svelte';
```
### Basic usage — text field with custom display
Wrap the display content in the default snippet. The component renders it in view mode and swaps in the input on edit.
| `current_value` | — | Required. Bound with `$bindable` — liveQuery updates flow through automatically |
| `allow_null` | `false` | Shows a "Set Null" button in edit mode |
| `display_block` | `false` | Makes the wrapper `display: block` instead of `inline-block` |
| `on_success` | — | Callback after successful PATCH — use to trigger SWR cache refresh |
| `object_reload` | `true` | Triggers internal SWR reload after patch (in addition to `on_success`) |
### Behavior notes
- The edit trigger button is `visibility: hidden` (not `display: none`) when `$ae_loc.edit_mode` is off — this preserves layout so the page doesn't shift when edit mode toggles.
- Optimistic display: draft value is shown immediately after save; cleared once liveQuery confirms the update came back from the DB.
- `on_success` should always call the relevant `load_ae_obj_id__*` function to keep Dexie in sync.
---
## 7. URL Parameters
URL params consumed by the app. Params are read by layouts and applied on mount.
### Global (active on all routes — read by `src/routes/+layout.svelte`)
| Param | Values | Effect |
| --- | --- | --- |
| `iframe` | `true` / `false` | Enables iframe mode — hides the AE system bar for all users by default, suppresses sign-in/passcode UI |
| `show_menu` | `true` | Override: show the AE system bar inside an iframe. Intended for admins/trusted users who need menu access while testing an embed. |
| `hide_menu` | `true` | Explicitly hide the AE system bar outside of iframe mode (e.g. fullscreen kiosk pages). |
| `theme` | theme name | Applies a theme on load, then removes param from URL (no history entry) |
| `theme_mode` | `light` / `dark` | Applies theme mode on load, then removes param from URL |
Dexie's `liveQuery` works well with Svelte 5 runes, but the combination requires a few stable patterns so queries don't get recreated unintentionally and components render correctly on a "cold start" (empty IndexedDB).
- Keep the observable instance stable: wrap `liveQuery` in a stable `$derived` so the observable isn't recreated on every render. Recreate the `liveQuery` only when explicit dependencies change (IDs, filters, or search keys).
```typescript
// stable derived wrapper — only recreated when `id` changes
letlq__obj=$derived(
(()=>{
// capture the dependency(s) in a single stable closure
constid=url_id;
returnliveQuery(async()=>{
if(!id)returnnull;
console.log('[LQ] running for id=',id);
returnawaitdb.table.get(id);
});
})()
);
```
- Use `$derived.by(() => ...)` where available in your runes shim to build queries from a computed set of inputs (IDs list, search params). This preserves a stable observable instance while still reacting to explicit dependency changes.
- Avoid capturing mutable objects or inline expressions in the `liveQuery` closure. If the closure captures a changing reference, the query may be recreated unexpectedly or miss the first write.
## Common Gotchas and Fixes (Why things sometimes need multiple refreshes)
- Cold start (IDB empty) + non-blocking API writes: If you mount a component before data is written to IDB, `liveQuery` may run against an empty DB. The API write will populate IDB later, but sometimes a chain of dependent queries (e.g., presentations -> presenters) won't all rerun in the order you expect. The symptoms you described — session shows after one refresh, presenters only after a second — are consistent with either (a) queries recreated in the wrong order or (b) dependent store values being set only after some subscriptions are already created.
### Bootstrap Race: Account-scoped Loads Before `account_id` Is Set (2026-06)
Account-scoped `liveQuery` triggers can fire before `+layout.svelte`'s bootstrap Sync Effect
has propagated the real `account_id`. Two failure modes:
1.**IDB empty:** fetch runs with `account_id = null`. The `localStorage` scavenge in
`api_get_object.ts` reads the stale value from a previous session — possibly a different
account — and caches that wrong record into IDB.
2.**IDB has a stale record:**`liveQuery` returns a cached record from a different account as
a valid hit, so the trigger condition (`!entry`) is never true and the correct record is
never fetched.
**Rule:** Gate any trigger `$effect` that loads account-scoped data on `$slct.account_id`,
not `$ae_loc.account_id`. `$slct` is a plain writable store (not persisted), initialized to
`null` and set _only_ by the bootstrap Sync Effect. `$ae_loc` is a persisted store that
hydrates from `localStorage` before effects run and may carry a stale `account_id`.
Also treat a non-null, non-matching `account_id` in an IDB record as a cache miss:
```typescript
$effect(()=>{
constaccount_id=$slct.account_id;// null until bootstrap Sync Effect runs
constapi_ready=!!$ae_api?.base_url;
constentry=$lq__objasSomeType|null|undefined;
if(!browser||!account_id||!api_ready)return;
// null account_id on a record = global/shared fallback — still a valid hit.
constentry_is_stale_account=
entry!==undefined&&entry!==null&&
entry.account_id!==null&&
entry.account_id!==account_id;
if(!entry||entry_is_stale_account){
trigger='load...';
}
});
```
See `BOOTSTRAP__AI_Agent_Quickstart.md` → Section 7, entry 14 for the full incident writeup.
### Critical Discovery (2026-02-26): The "try_cache: false" Bug
**Symptom:** Nested data (e.g., Session → Presentations → Presenters) requires multiple manual refreshes to display on cold-start, even when using blocking loads.
**Root Cause:** Two interconnected issues in nested data loaders:
1.**Disabled caching in nested loads**: Parent loads were passing `try_cache: false` to child loads, meaning presentations and presenters were fetched from API but **never written to IndexedDB**.
2.**Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery _before_ IndexedDB writes completed, causing race conditions.
1.**Always preserve `try_cache` through nested loads** unless you have a specific reason to disable caching for that operation
2.**Add `await Promise.resolve()` after IndexedDB writes** to ensure Dexie's liveQuery observers fire before the function returns
3.**Block on nested loads with `await Promise.all()`** instead of fire-and-forget `forEach()` when the page needs complete data for first render
Fixes:
- Prefer the "Blocking Loader" when you can: `await` the API call in `+page.ts` so IDB is populated before Svelte mounts.
- If you cannot block, return an `initial_*` object from `+page.ts` and use it as an immediate fallback in your component so the UI renders from that payload while `liveQuery` takes over for subsequent updates. Example from Aether:
- Ensure store IDs are set before subscribers that depend on them are created. Use `untrack()` (or an equivalent non-reactive assignment) to set IDs in stores during initialization so components subscribe to the correct IDs immediately:
```typescript
$effect(()=>{
if(!ae_acct)return;
untrack(()=>{
$events_slct.event_id=url_event_id;
$events_slct.event_session_id=url_session_id;
});
});
```
- When you have chains (presentations depend on session; presenters depend on presentation.person_id), make the dependent liveQuery explicitly wait for the upstream ID and log inside each query to verify the order — adding a small `await Promise.resolve()` or `await 0` inside the `liveQuery` is sometimes useful during debugging to ensure the JS microtask queue has a chance to settle after DB writes.
## IDB Sort: `build_tmp_sort` Pattern (2026-05)
All Aether objects support `priority`, `sort`, `group`, and `name` fields. Rather than sorting in JS after a Dexie query (which requires `.reverse()` hacks and duplicated logic), pre-compute up to three `tmp_sort_*` string fields during the processing pipeline and store them in Dexie. Then `.sortBy('tmp_sort_2')` does the right thing in one call, with no `.reverse()`.
**Priority encoding:**`priority ? '0' : '1'` — inverted so that `priority=true` sorts first in ascending order. This means:
- **Dexie `.sortBy('tmp_sort_*')`** — always call without `.reverse()` before it (Dexie ignores collection-level `.reverse()` when using `.sortBy()`). If descending is needed for non-tmp_sort fields, call `.reverse()` on the resulting array after `await`.
- **JS `.sort()` comparators** — use **ascending**`a.localeCompare(b)`, NOT `b.localeCompare(a)`. Using descending flips the priority encoding and puts `priority=false` items first.
-`ae_events__event_presentation.ts` — `tmp_sort_1/2`: group → priority → sort → start_datetime → code → name
-`ae_events__event.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on (used by IDAA recovery meetings)
-`ae_journals__journal.ts` — `tmp_sort_1/2/3`: group → priority → sort → name → updated_on
-`ae_journals__journal_entry.ts` — same chain as journal
**Legacy encoding (not yet migrated to `build_tmp_sort`):**`ae_posts__post.ts`, `ae_posts__post_comment.ts`, `ae_archives__archive.ts`, `ae_archives__archive_content.ts`, `ae_sponsorships_functions.ts` use the opposite encoding (`priority ? '1' : '0'`, designed for descending sort). Their current route consumers sort by date/name so there is no visible priority bug today, but they must be migrated before any route starts sorting by `tmp_sort_*`. See `TODO__Agents.md`.
---
## `$derived.by` Dependency Capture for Extra Filter State
When a `liveQuery` has a SCENARIO 2 fallback (broad search with no IDs), it may run before the debounced search fast path populates `event_session_id_li`. If that fallback doesn't apply the same visibility filter as the fast path, hidden items will briefly appear then disappear ("blink").
**Fix:** capture the filter flag as a `$derived.by` dependency in the outer closure so Svelte recreates the liveQuery instance whenever it changes — SCENARIO 2 then uses the correct filter from first render.
```typescript
letlq__event_session_obj_li=$derived.by(()=>{
constids=event_session_id_li;// drives SCENARIO 1 vs 2
constevent_id=$events_slct?.event_id;
constqry_hidden=pres_mgmt_loc.current.qry_hidden;// extra dependency
returnliveQuery(async()=>{
// SCENARIO 1 — specific IDs (fast path or API result)
**Key rule:** anything read inside `$derived.by()`'s outer closure (but outside the `liveQuery` callback) becomes a Svelte reactive dependency. Changes to it recreate the liveQuery. Use this to synchronize filter flags that Dexie doesn't track.
**Also fix the API call:** use the snapshot value from `params` (captured at debounce time) rather than the live store, so rapid toggling doesn't create a mismatch between fast path and API results:
```typescript
// Bad — uses live store value, can race if user toggles during pending call:
- Journals: The journaling pages use SWR-style background refreshes but reliably render because either (a) the page `+page.ts` blocks to populate DB for critical views, or (b) components accept `data.initial_*` fallback values until `liveQuery` emits. This hybrid approach avoids the "refresh twice" problem while keeping navigation snappy.
- Journals broad views: if text search is empty, let the local IDB result set drive the visible list. The API can revalidate the cache in the background, but it should not replace a broad "All" view with a limited slice that hides valid rows.
- Sessions / Presentations: The session page demonstrates several best practices:
- Use `url_*` constants (derived from `data.params`) so the `liveQuery` closure captures a stable value instead of the reactive store directly.
- Provide `initial_session_obj` from `+page.ts` as a first-draw fallback to child components.
- Use `$derived.by(() => liveQuery(...))` for presentation lists so the observable instance is stable across renders and recreated only when `event_session_id` or `search` changes.
- Search pages with persisted filters or saved query text should keep the auto-search trigger in a page-level `$effect`, but the duplicate guard should live inside the actual search executor. That preserves the first page-load search while blocking repeated identical reruns from localStorage-backed rerenders. In practice:
- derive a single `qry_key` from the search inputs
- debounce in the `$effect`
- compare `qry_key` against a `last_executed_key` inside `handle_search_refresh()`
- keep transient loading flags and trigger counters in session state when the value is only used to force a refresh, not as a persisted preference
Example (presentation list pattern):
```typescript
letlq__event_presentation_obj_li=$derived(
liveQuery(async()=>{
if(!url_session_id)return[];
console.log('[LQ] Querying Presentations for Session:',url_session_id);
- Add a small `console.log` inside each `liveQuery` closure to confirm when it runs and what `id` it sees.
- Verify that `+page.ts` either `await`s critical loads or returns `initial_*` payloads for first-render hydration.
- Confirm that dependent store values (selected IDs) are assigned before components subscribe — use `untrack` to prevent extra reactive cycles.
- If a search page stops auto-loading after a localStorage change, check whether the duplicate guard was placed in the `$effect` instead of the executor. Guarding too early can suppress the initial search; guard at execution time instead.
- If a broad Dexie-backed list shows fewer rows than a narrower filter, look for a limit or revalidation step overwriting the local IDB result set. Broad views should stay unbounded unless the user is actually narrowing by text.
- Ensure your `liveQuery` closures return quickly and do not throw; any exception inside the query can stop updates.
- If a dependent query appears stale, temporarily add `await 0` in the upstream query or an explicit `Promise.resolve()` after the IDB write to force the microtask queue to flush during debugging.
## Summary Recommendations
- Prefer blocking loads for primary views when first-render correctness matters.
- Use `initial_*` fallback data when non-blocking loads are required.
- Wrap `liveQuery` in stable `$derived` instances and only recreate when explicit inputs change.
- Use `untrack` to set selection IDs during initialization to avoid subscribe-order bugs.
- Add targeted logs inside `liveQuery` closures to diagnose ordering and subscription behavior.
These patterns are deliberately conservative — they trade minimal blocking or small explicit fallbacks for predictable first-render behaviour. The Aether app's Journals and Event session pages are working examples of these techniques in practice.
## Examples in this repository
The following files demonstrate stable `liveQuery` usage, `initial_*` fallbacks, and stable `$derived` wrappers used across the Aether app. Inspect these for copy/paste patterns and concrete implementations.
- Event settings example (simple observable): [src/routes/events/[event_id]/settings/+page.svelte](src/routes/events/[event_id]/settings/+page.svelte#L51)
- Badge/detail pages (examples of nested LQ): [src/routes/events/[event_id]/(badges)/badges/+page.svelte](src/routes/events/[event_id]/(badges)/badges/+page.svelte#L66)
Refer to these files when you need concrete code examples to adopt the patterns described above.
## References
This document provides a guide to integrating Svelte (with a focus on Runes) and Dexie.js for building reactive web applications. It covers key concepts and best practices for managing reactivity between Svelte components and the Dexie.js database.
## Svelte 5 Migration Guide
Svelte 5 introduces "runes" as a new way to manage reactivity. This is a major change from previous versions of Svelte, and it's important to understand the breaking changes before migrating.
### Key Breaking Changes
- **`let` is no longer reactive:** In Svelte 4, any `let` variable declared in the top-level scope of a component was automatically reactive. In Svelte 5, you must explicitly declare reactive state using the `$state` rune.
- **`$:` is replaced by `$derived` and `$effect`:** The `$` label is no longer used for reactive statements. Instead, you should use the `$derived` rune for computed values and the `$effect` rune for side effects.
- **`export let` is replaced by `$props`:** Component props are now declared using the `$props` rune, which provides a more flexible and explicit way to define component APIs.
- **Event handling:** The `on:` directive is replaced by event attributes (e.g., `onclick`). Component events are now handled using callback props instead of `createEventDispatcher`.
- **Slots are replaced by snippets:** The `<slot>` element is replaced by the `{#snippet ...}` block, which provides a more powerful and flexible way to pass content to components.
For a complete list of breaking changes, refer to the [Svelte 5 migration guide](https://svelte.dev/docs/svelte/v5-migration-guide).
## Dexie.js Quick Reference
Dexie.js is a lightweight, minimalistic wrapper for IndexedDB that makes it easier to work with client-side databases.
### Key Classes and Methods
- **`Dexie`:** The main class for creating and managing IndexedDB databases.
-`new Dexie(databaseName)`: Creates a new database instance.
-`version(versionNumber).stores({ ... })`: Defines the database schema.
- **`Table`:** Represents an object store (table) in the database.
-`add(item)`: Adds a new item to the table.
-`put(item)`: Adds or updates an item in the table.
-`update(key, changes)`: Updates an existing item.
-`delete(key)`: Deletes an item by its primary key.
-`get(key)`: Retrieves an item by its primary key.
-`where(index)`: Starts a query using an index.
-`toArray()`: Retrieves all items from the table as an array.
- **`Collection`:** Represents a collection of items resulting from a query.
-`toArray()`: Retrieves all items in the collection as an array.
-`first()`: Retrieves the first item in the collection.
-`last()`: Retrieves the last item in the collection.
-`each(callback)`: Iterates over each item in the collection.
-`modify(changes)`: Updates all items in the collection.
-`delete()`: Deletes all items in the collection.
For a complete list of API methods, refer to the [Dexie.js API Reference](https://dexie.org/docs/API-Reference).
## Integrating Svelte Runes and Dexie.js
The combination of Svelte Runes and Dexie.js allows for the creation of highly reactive and efficient web applications.
### The `liveQuery` Function
Dexie.js provides a `liveQuery` function that returns an observable of the query result. This observable can be used to automatically update the UI whenever the data in the database changes.
### Using `liveQuery` with Svelte Runes
To use `liveQuery` with Svelte Runes, you can create a custom readable store that wraps the `liveQuery` observable. This store can then be used in your Svelte components to display and interact with the data.
**1. Create a `liveQuery` store:**
```typescript
import{liveQuery}from'dexie';
import{readable}from'svelte/store';
import{db}from'./db';// Your Dexie database instance
The `createLiveQueryStore` function creates a readable store that automatically updates whenever the data in the `friends` table changes. The `$friends` variable in the component will always contain the latest data from the database.
## SvelteKit Layout Hierarchy: Security and Execution Order
Understanding _when_ SvelteKit code runs is critical for private-data modules like IDAA.
### Execution order on any navigation
```text
1. +layout.ts / +page.ts ← run FIRST — before any component mounts
also fired by SvelteKit link prefetch (on hover)
2. Parent +layout.svelte mounts → its $effect blocks run
3. Child +layout.svelte mounts → only if parent called {#render children?.()}
4. +page.svelte mounts → only if every parent in the chain rendered children
5. $effect blocks in all of the above run after mount
```
### The auth-gate consequence
A `{:else if authenticated} {@render children?.()}` block in a `+layout.svelte`
controls whether **everything below it** ever mounts. If the gate blocks rendering,
no child layout or page component instantiates — their `$effect` blocks, event
Because `$ae_loc` is a Svelte 4 coarse-grained store, any unrelated write to it
(iframe height, SWR reload) re-triggers this `$effect`. The guard prevents a spurious
API call if `$idaa_loc.novi_verified` has been cleared between re-runs (e.g. TTL
expiry mid-session). It is a reactivity guard, not a layout-bypass guard.
---
## Page Load Strategies (Avoiding the "Waterfall")
When loading data for a primary page view (e.g., viewing a specific Journal, Session, or Person), you must choose a synchronization strategy to ensure the UI renders correctly on the first load.
### ❌ The "Fire & Forget" Anti-Pattern (AVOID)
Triggering a background load in `+page.ts` without `await` leads to race conditions.
1.`+page.svelte` mounts immediately.
2.`liveQuery` runs against an empty IndexedDB.
3. API data arrives later and writes to IndexedDB.
4.**Failure:** Svelte 5 + Dexie `liveQuery` may not automatically detect this first "cold start" update without a manual refresh.
### ✅ The "Blocking Loader" Pattern (RECOMMENDED)
Ensure the data is in IndexedDB **before** the component mounts.
1. In `+page.ts`, `await` the API load function.
2. In `+page.svelte`, the `liveQuery` will see the data immediately upon mount.
**Example (+page.ts):**
```typescript
exportasyncfunctionload({params}){
// Blocking await ensures IDB is populated
awaitjournals_func.load_ae_obj_id__journal({
journal_id: params.journal_id,
try_cache: true
});
return{};
}
```
### ✅ The "Hydrate & Subscribe" Pattern (ADVANCED)
If you must use non-blocking loads, you must pass the initial data to the component to "hydrate" the state before the subscription takes over.
1. In `+page.ts`, `await` the load and **return the object**.
2. In `+page.svelte`, use the returned object as a fallback or initial state.
<!-- Use fallback to handle the gap before liveQuery emits -->
{#if$lq__obj||data.initial_obj}
<Viewobject={$lq__obj??data.initial_obj}/>
{/if}
```
## The `untrack()` Reactive-Tracking Trap
`untrack()` is used inside `$effect` to read reactive values without registering them as tracked dependencies of that effect. This is correct for most "read-once" values (params, IDs) where you don't want the effect re-running on every change. But it has a silent failure mode: if a value you _need_ to re-read is consumed inside `untrack()`, the effect becomes a one-shot and never retries when that value changes.
### Symptom
An effect runs once, reads a store value inside `untrack()`, takes an early-exit path (e.g. "no API key → skip"), and never retries — even after the store value is updated by a background process.
### Real Example (IDAA Novi Verification Bug — 2026-03-25)
The IDAA layout verifies Novi UUIDs. `site_cfg_json` (which contains the Novi API key) was read **inside**`untrack()`:
```typescript
// BUG: site_cfg_json read inside untrack → one-shot, never retries
if(!api_key)return;// exits silently on first load with stale cache
verify_novi_uuid(uuid,api_key,...);
});
});
```
On first load, the Dexie cache returned a stale `site_cfg_json` missing the API key. The effect exited early. The background refresh later updated `$ae_loc.site_cfg_json`, but because `site_cfg_json` was consumed inside `untrack()`, the effect never re-ran.
**Fix:** Move the dependency read **outside**`untrack()`:
```typescript
// FIX: site_cfg_json tracked outside untrack → effect re-runs when it changes
When preparing data for IndexedDB, especially when creating composite sort keys, it is critical to handle `null` or `undefined` values safely to prevent runtime crashes that can interrupt the data synchronization process.
### 1. Safe String Padding
Attempting to call `.toString()` or `.padStart()` on a `null` or `undefined` value will throw a `TypeError`. This is a common pitfall when processing optional fields like `sort` or `group`.
Dexie's `sortBy()` method returns a new array sorted by the specified key. It **ignores** previous `reverse()` calls on the collection. To achieve a descending sort, you must sort first and then reverse the resulting array.
Layout CSS files live in `src/lib/ae_events/badges/css/` and are imported by
`ae_comp__badge_obj_view.svelte`. Rules are scoped under `[data-layout="..."]` on the
wrapper so multiple layouts can coexist in the bundle without conflict.
`@page` paper size rules are injected per-layout from `print/+page.svelte <svelte:head>`
(attribute selectors cannot scope `@page` rules, so they're handled dynamically).
---
## cfg_json Reference
All keys are optional. Unknown keys are preserved on save (forward-compatible). Managed via the template form's **Advanced** and **Header & Branding** sections, or directly in phpMyAdmin.
### Visibility
| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `hide_badge_header` | bool | `false` | Hides the entire header section (image + logo/text fallback). Auto-true when `background_image_path` is set, unless explicitly overridden. |
| `hide_badge_footer` | bool | `false` | Hides the badge type footer stripe. |
| `hide_title` | bool | `false` | Suppresses the professional title field on the badge front. |
| `header_margin_top` | CSS length | `2rem` | Vertical offset of the header image. Negative = shift up. e.g. `"-0.25in"`, `"1rem"`. |
| `header_border_color` | hex color | none | Bottom border drawn below the header div. **Empty = no border.** e.g. `"#FE6111"`. |
| `header_border_width` | CSS length | `2px` | Thickness of the header bottom border. Only applied when `header_border_color` is set. |
| `header_padding_bottom` | CSS length | none | Space between the header image and the bottom border line. e.g. `"1.45in"`. |
### Appearance
| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `body_text_color` | hex color | `#000000` | Inline color applied to all badge body text. |
| `bleed` | CSS length | none | Extends background image past card edges on all sides. Prevents white borders on printers that clip slightly inside the card. e.g. `"0.125in"`, `"3mm"`. |
### Text Zone Heights (`fit_heights`)
Per-layout height overrides for the auto-scaling text zones. Set any subset — unset keys fall back to the layout default. Useful when `background_image_path` is set and the designed zones don't align with code defaults.
| `grp_aff_loc` | Height of the affiliations+location container |
| `grp_aff_loc_flex` | Flex distribution (same values as above) |
| `affiliations` | Height of the affiliations text zone |
| `location` | Height of the location text zone |
### Punch-Out Hole Markers (`punch_holes`)
Enables X overlays at the physical badge clip slot positions. Slots are pre-perforated on the badge stock — the markers print on the badge so attendees know where to push them out.
**Slot dimensions:** 5/8″ wide × 1/8″ tall, 1/4″ from top edge, 3/8″ from left/right edges. Center slot is horizontally centered.
Controls which fields appear in the print controls panel for non-trusted users, and which fields authenticated users may edit. Trusted + Edit Mode always sees and can edit all fields regardless of this config.
In print, `position: fixed` anchors relative to the `@page` content area, bypassing the entire
ancestor hierarchy (no containing-block height dependency, no overflow-clip interference).
**Future per-template margins:**`print_margin_cfg` is already parsed from `cfg_json`
in `print/+page.svelte`. A dynamic `@page { margin: ... }` injection can be built from
that value when a UI for it exists.
---
### Cross-browser print behavior — IMPORTANT
Verified 2026-03-12 by comparing print-to-PDF output from both browsers across multiple
print dialog settings.
#### `@page { size }` — paper size
| Browser | Save to PDF | Physical printer |
|---|---|---|
| **Firefox** | Paper size locked — cannot change in dialog; CSS `@page { size }` used ✅ | Can select paper size in dialog |
| **Chrome/Chromium** | Paper size locked — cannot change in dialog; uses system default (letter, A4, etc.) ❌ | Can select paper size under "More settings" |
Chrome intentionally does not honor `@page { size }` for Save as PDF. It uses the system
default paper size. This is a Chrome design decision, not a bug in our code.
For actual printing to Epson/Zebra hardware: the printer driver controls paper size from
the loaded badge stock. CSS `@page { size }` is advisory only. Real badge printing is
unaffected by Chrome's behavior.
Use Firefox for accurate print-to-PDF proofing — it produces a correctly-sized PDF that
| **Default** | ❌ Adds URL, date, and page-number headers/footers into the printable area. These eat into the space that `position: fixed; top: 50%` references, making the badge appear off-center or clipped against the footer. |
| **None** | ✅ Correct — badge centered cleanly |
| **Minimum** | ✅ Correct — small margins, badge still centered |
| **Custom (reasonable values)** | ✅ Correct |
The badge content itself is **not** distorted. Verified: Chrome "None" margins on an A4
page produces the badge perfectly horizontally centered (page center 297.5 pts, badge
content center 297.5 pts). The CSS centering logic is correct.
**Staff guidance for Chrome:**
- Set **Margins → None** (or Minimum) in Chrome's print dialog.
- Optionally set paper size to match badge stock under "More settings" when printing to PDF.
- For physical printer: select correct paper size under "More settings".
Firefox users can use "Save to PDF" directly — it just works.
---
## Related Files
| File | Role |
|---|---|
| `ae_events__event_badge_template.ts` | API + IDB functions; `properties_to_save` |
| `db_events.ts` | Dexie schema for `badge_template` table |
| `templates/+page.svelte` | Template list + create/edit/delete UI |
| `templates/ae_comp__badge_template_form.svelte` | Template create/edit form |
| `[badge_id]/ae_comp__badge_obj_view.svelte` | Badge render — consumes template data |
- [x] Wire `style_href` via `<svelte:head>` in print page — done in `print/+page.svelte`; also in `properties_to_save`. (2026-03-18 verified)
- [x] Add `duplex` to `properties_to_save` — done. (2026-03-18 verified)
- [x] Add `duplex`-driven suppression to `badge_back` section — done in `ae_comp__badge_obj_view.svelte`; `show_badge_back` derived from `duplex` field.
- [x]`badge_4x6_fanfold` layout CSS created (`badge_layout_epson_4x6_fanfold.css`), imported in badge component, `@page 4in 6in` wired in print page. (2026-05-15)
- [x] Template form expanded — `layout`, `style_href`, `badge_type_list`, `duplex`, and all `cfg_json` keys now editable via the form. (2026-06-04)
- [x]`cfg_json.header_margin_top`, `header_border_color`, `header_border_width`, `header_padding_bottom` added — header image position and bottom border are fully configurable without a code deploy. (2026-06-04)
- [ ] Wire `badge_type_list` from the template into the badge search filter — currently the search form uses a hardcoded list. See `ae_comp__badge_search.svelte` TODO comment.
- [ ]`badge_4x5_fanfold` layout CSS exists but is stale (not used in 2+ years) — review against actual hardware before next use.
- [ ] Remove dead `exhibitor_info` / `presenter_info` / `staff_info` / `vip_info` / `vote_info``{#if}` blocks from `ae_comp__badge_obj_view.svelte` (if they were carried over from v1)
The Badges module manages event attendee records and their physical badge configurations. It supports multi-source imports, field protection for onsite edits, and multi-tier access control for self-service review.
---
## Data Model & Hierarchy
### Core Objects
- **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
- **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
- **Badge → Person:** Optional link to core Aether Person record for unified profiles.
---
## Critical Design Pattern: Override Fields
### Purpose
The `*_override` fields pattern (established in 2018) protects data from being overwritten during scheduled cron syncs from external systems (iMIS, Novi, etc.). This ensures that staff corrections or attendee self-updates persist across multiple sync cycles.
### How It Works
1.**Import:** External systems populate **REGULAR** fields only.
2.**Display Logic:** The UI displays the `*_override` field if it has a value; otherwise, it falls back to the regular field.
3.**HTML Rendering:** Certain display fields (Name, Title, Affiliations, Location) support HTML markup for rich text formatting (bold, italics, line breaks) on the physical badge.
### Standard Override Pairs
| Regular Field | Override Field | Editable By | HTML? |
| `badge_type` | `badge_type_override` | Staff Only | No |
---
## External System Integration
Aether acts as a **Pull-Only** consumer for registration data. It does not push changes back to external systems, maintaining them as the source of truth for base registration while Aether handles the "Onsite Truth."
| **Trusted** | Search all badges, view all, reprint existing badges. |
| **Administrator** | Full CRUD, bulk operations, override any field. |
| **Manager** | All Admin + Event/Template configuration. |
### Attendee Self-Service (`/review`)
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`). This allows them to verify their info and provide preferred name/title overrides before printing.
---
## Search & Filter Capabilities
- **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
- **Multi-Word Logic:** Queries like "Scott Idem" are split and treated as `LIKE %Scott% AND LIKE %Idem%`.
- **QR Scan Search:** Scanning an attendee's QR code (from a confirmation email or old badge) immediately jumps to their record.
| **Show Disabled + Hidden** | Manager only | Shows all records regardless of enable/hide flags |
### Result Limit Stepper (Edit Mode)
Controls the maximum number of results returned. Only visible in edit mode.
| Access Level | Range | Step |
| --- | --- | --- |
| Below Trusted | Fixed 25 | — |
| Trusted | 25 – 250 | 25 |
| Manager+ | 25 – 2550 | 25 up to 250, then 100 |
### Badge Type Filter — Known Limitation
The badge type dropdown in the search form uses a **hardcoded list**, not the template's `badge_type_list`. This means the codes shown in the filter may not match the codes used by the current event's template. This is a known gap — the fix requires passing the template object into the search component. Until resolved, staff can still search by name/email and filter results manually.
---
## Print Tracking
Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
| Field | Purpose |
|---|---|
| `print_count` | Increments on every "Print Badge" action. |
| `print_first_datetime` | Timestamp of the very first print. |
| `print_last_datetime` | Timestamp of the most recent print. |
> **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
---
## Route Map (Badges)
| URL | Purpose |
|---|---|
| `/events/[id]/badges` | Main search and attendee list. |
The Launcher module provides the podium display interface that runs on each session room's kiosk machine. It is designed to work in standard browsers but is optimized for the **Aether Desktop (Electron)** native shell.
---
## Operational Modes
| Mode | Use Case | File Handling |
|---|---|---|
| **Default** | Browser on any machine | Files downloaded on demand via browser. |
| **Poster/group view** | Special layout for poster sessions. |
| **Screensaver** | No active session; idle state. |
---
## Sync Engine & File Handling
### Background Sync (File Warming)
When a user navigates to a session in the Launcher UI, the background engine automatically warms the cache for that specific session by downloading all associated files.
### Force Sync Location
To ensure full room readiness (e.g., during SRR setup or overnight), operators can trigger a **Force Sync Location** via the configuration menu. This performs a recursive fetch of all sessions, presentations, and presenters for the room and queues every file for the day for download.
### Download Priority & Room Readiness
To ensure the podium is ready for the day's first sessions, the Launcher sync engine uses a 4-tier chronological sorting priority:
1.**Global Assets:** Event and Location level files (branding, walk-in slides) are cached first.
2.**Session Schedule:** Files for the earliest sessions in the room are prioritized.
3.**Presentation Order:** Within a session block, speakers are prioritized by their scheduled start time.
4.**First-In Fairness:** When times are equal, older uploads are prioritized over late revisions (respecting on-time presenters).
### Native File Opening (Safe Handover)
1. Verify SHA-256 hash in permanent cache.
2. Atomic copy to system `[tmp]` directory.
3. Rename to original filename (e.g., `Abstract_101.pptx`).
4. OS opens the file via a **Launch Profile** (AppleScript or Shell command).
---
## Device & Native Integration
Each Launcher kiosk is registered as an `event_device` record in Aether. The technical specifications for the Electron bridge, hashed cache protocol, and hardware actuators are documented in:
This document provides a detailed inventory of the Launcher's configuration menu settings as of May 2026. This serves as the baseline for the v3.1 reorganization into a modal-based tabbed interface.
---
## 1. UI Architecture & Visibility
The configuration menu currently resides in a slide-out **Drawer** (sidebar).
### 1.1 Visibility Modes
- **Standard Mode:** Default view for onsite operators. Hides advanced technical and destructive controls.
- **Technical Mode (`$ae_loc.edit_mode`):** Toggled via a subtle pencil icon. Reveals advanced diagnostic fields, manual overrides, and debug tools.
- **Native Mode (`$ae_loc.is_native`):** Automatically detected when running in the Electron shell. Shows OS-level controls (Filesystem, Power, Apps).
### 1.2 Section Expansion Logic
- **`collapsed`**: Content hidden.
- **`auto`**: Expanded by default; collapses when another "auto" section opens.
- **`pinned`**: Remains expanded regardless of other interactions.
The Aether Events Launcher utilizes an Electron-based "Native Shell" to provide OS-level capabilities that are normally restricted by browser sandboxing. This enables persistent file caching, direct control of presentation software (Keynote, PowerPoint), and hardware telemetry.
### Operational Modes
| Mode | Purpose | File Handling |
| :--- | :--- | :--- |
| **Default** | Standard web browser access. | Direct downloads; no local caching. |
| **Onsite** | Web access on event networks. | Faster polling; browser-based file management. |
- **Role:** Performs the heavy lifting (Filesystem, Shell, AppleScript).
- **Responsibilities:**
- Managing the **Hashed Cache** directory.
- Executing `osascript` intents for presentation control.
- Spawn/Kill process management.
### 2.2 Layer 2: The Gatekeeper (Preload Script)
- **Namespace:** `window.aetherNative`
- **Role:** Securely exposes whitelisted IPC channels to the Renderer.
- **Standards:** Uses `contextBridge.exposeInMainWorld` to prevent arbitrary code execution.
### 2.3 Layer 3: The Messenger (SvelteKit Relay)
- **File:** `src/lib/electron/electron_relay.ts`
- **Role:** Provides a clean, typed API for Svelte components.
- **Responsibilities:**
- Mapping `camelCase` UI triggers to `snake_case` IPC calls.
- Resolving an extension alias to a canonical Launch Profile, then to a single
`native_template` string before crossing IPC.
The reason for this split is simple: Launch Profiles are policy, while Native Templates are
executable strings. Keeping that distinction explicit prevents the bridge from mixing config
objects with runtime commands.
---
## 3. The "Zero-Config" Lifecycle
To support rapid onsite deployment, the native app requires zero manual setup.
1.**Seed:** On launch, the Main process reads a local `seed.json` (Device ID + API Key).
2.**Identity:** Calls `GET /v3/crud/event_device/{id}` to pull device config and extract `app_base_url` (the event FQDN) and `account_id`.
3.**Site Context:** POSTs to `/v3/crud/site_domain/search?limit=1` with the FQDN to resolve the correct site. No JWT — auth is `x-aether-api-key` + `x-account-id` throughout.
4.**Launch:** Navigates the SvelteKit frontend directly to the assigned Event Launcher route (`/events/{eventId}/launcher/{locationId}`).
---
## 4. Podium Reliability Protocol
The system is designed to ensure that a presentation never fails due to network instability.
### 4.1 Hashed Cache Pattern
Files are stored persistently using their SHA-256 hash to prevent filename collisions and handle versioning.
- **Root:** `~/Library/Caches/OSIT/file_cache/`
- **Subdirectory:** First 2 characters of hash (e.g., `ab/`)
- **Filename:** `{hash}.file`
### 4.2 Background Sync (File Warming)
When a user navigates to a session in the Launcher UI, the `LauncherBackgroundSync` component warms the cache for that specific session. To ensure full room readiness, a **Force Sync Location** trigger is available in the configuration UI.
1.**Metadata Fetch:** The system fetches all sessions, presentations, and presenters for the current location into the local database (Dexie).
2.**Chronological Priority:** Missing files are added to the download queue and sorted to prioritize the event schedule:
-**Tier 1: Global Assets** — Event and Location level files (virtual time 0).
-**Tier 2: Session Schedule** — Earliest sessions are prioritized first.
-**Tier 3: Presentation Order** — Within a session, speakers are prioritized by their start time.
-**Tier 4: Integrity & Fairness** — Tie-breakers use `created_on` (oldest first) to ensure on-time uploads are cached before last-minute revisions.
3.**Download:** Triggers background downloads via `aetherNative.download_to_cache` sequentially to preserve network bandwidth and ensure file integrity.
### 4.3 Safe Handover (Launch Sequence)
When a user clicks "Open", the system follows a non-destructive sequence:
1.**Verify:** Confirm hash exists in the permanent cache.
2.**Copy:** Create an atomic copy in the system `[tmp]` directory.
3.**Restore:** Rename the copy to its original filename (e.g., `Abstract_101.pptx`).
4.**Execute:** Launch the file via the OS.
---
## 5. Automation & Actuators (Phase 5)
The native shell provides specialized handlers for controlling the "Podium Experience."
- **Display Layouts:** `set_display_layout({mode, configStr?})` — Mirror / Extend displays. macOS only. **Primary:** native `display_control` binary (`resources/bin/display_control`) uses CoreGraphics APIs directly — no Homebrew dependency. Built from `scripts/display_control.m` via `scripts/build-display-control.sh` on a Mac; commit the binary to the repo. **Fallback:** [`displayplacer`](https://github.com/jakehilborn/displayplacer) (`brew install displayplacer`) used when binary is absent or `configStr` override is set. Failures are logged to the Electron console but do not block file open. A **Display Mode** toggle (Extend / Mirror) is available in the Launcher config — Native OS section, visible without Technical Mode.
- **Wallpaper:** `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Downloads from URL (cached locally) or applies a local path. Per-display targeting (`'all'`/`'primary'`/`'external'`). macOS only in production; Linux returns a dev-mode preview payload.
> **Note:** `update_app` is implemented as a stub — downloads but does not install. Not yet functional for end users.
---
## 6. Launcher Configuration & Management
The Launcher features a standardized, responsive configuration interface designed for onsite technical management.
### 6.1 UI Architecture
- **Tabbed Navigation:** Categorized into System, Sync, and General settings.
- **Section Wrapper (`Launcher_Cfg_Section`):** A shared component providing a consistent header, icon, and responsive grid container.
### 6.2 3-Way State Logic
To manage screen real estate on varying laptop resolutions, all configuration sections utilize a 3-way visibility state:
- **`collapsed`**: Content is hidden.
- **`auto`**: Expanded by default, but automatically closes if another "auto" section is opened.
- **`pinned`**: Expanded and remains open regardless of other section interactions.
### 6.3 Technical Mode (`edit_mode`)
The UI dynamically filters fields based on the user's focus. Enabling Technical Mode (`$ae_loc.edit_mode`) reveals advanced diagnostic and writeable fields.
| **Update** | Current Version Status | Manual Update Paths, URL Overrides |
---
## 7. Implementation Reference (IPC Whitelist)
All functions below are exported from `src/lib/electron/electron_relay.ts` and safely
no-op when `window.aetherNative` is not present (i.e., in browser/non-native mode).
### Config & Info
-`get_device_config()` — Returns hydrated device settings injected by the native shell on startup.
-`get_device_info()` — Returns OS metadata, IP list, hostname, and path placeholders (`[home]`, `[tmp]`).
### File Cache
-`check_cache({cache_root, hash, hash_prefix_length?, verify_hash?})` — Verifies a file exists in the local hashed cache. `verify_hash: true` re-hashes to confirm integrity.
-`download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` — Streams a file download to the hashed cache with SHA-256 integrity check. Stale `.tmp` files (older than 5 min) from crashed downloads are cleaned up automatically on each call.
-`copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})` — **Preferred primitive.** Copies a cached file to temp and returns `{ success, path }`. The Svelte caller decides what to do next (run a script, open it, etc.).
-`launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, native_template?})` — Combines copy + launch in one call. Executes the provided `native_template` string after the file is copied to temp. If no template is supplied, treat it as an error and do not rely on Electron-side defaults.
> `hash_prefix_length` defaults to `2` throughout. Do not change without coordinating all devices — mismatched values create orphaned cache subdirectories.
### Shell & OS
-`open_folder(path)` — Opens a path in the OS file manager.
-`run_osascript(script)` — Executes an AppleScript string. macOS only. **Hardened (2026-05-11):** writes script to a temp `.scpt` file; multi-line scripts and paths with special characters now work correctly. No shell escaping needed in the passed string.
-`control_presentation({app, action})` — Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript.
### System Management (Phase 5)
-`set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` — Sets desktop wallpaper. Downloads from `url` (cached to `~/Library/Caches/OSIT/wallpaper/`) or applies a local `path`. `url_external` targets the projector/second display separately. macOS only in production; Linux returns a dev-mode preview payload without applying.
-`window_control({action, value?})` — Electron window management: maximize, minimize, fullscreen, kiosk.
-`set_display_layout({mode, configStr?})` — Mirror or extend displays via [`displayplacer`](https://github.com/jakehilborn/displayplacer). macOS only. Auto-detects via `displayplacer list`; `configStr` overrides auto-detection when set. Binary lookup order: bundled `resources/bin/displayplacer` → `/opt/homebrew/bin/` (Apple Silicon) → `/usr/local/bin/` (Intel). Requires `brew install displayplacer` on each venue Mac if not bundled.
-`power_control({action})` — Shutdown, reboot, or sleep the host machine. macOS + Linux.
-`manage_recording({action, options?})` — Aperture capture control (`start`/`stop`/`status`). macOS only.
-`open_external({url, app?})` — Opens a URL in Chrome, Firefox, or the default browser.
-`update_app(args)` — **Stub only.** Downloads but does not install. Not yet functional.
-`list_tools()` — Returns a self-documenting manifest of all available native bridge functions.
### Path Placeholders
All paths passed to native handlers should use tokens rather than hardcoded OS paths:
-`[home]` — Resolved to the user's home directory by the native bridge.
-`[tmp]` — Resolved to the system temporary directory.
---
## 8. Launch Profiles and Native Templates (No-Rebuild File Handling)
This launcher uses two related concepts:
- **Launch Profile**: the Svelte-side config object keyed by file extension. A profile decides
which app to use, whether to extend or mirror displays, whether to use an explicit open
command, whether to run post-open automation, and how long to wait before running it.
- **Native Template**: the single AppleScript or shell command string handed to Electron after
Svelte resolves the profile. This is what Electron actually executes.
The Svelte launcher resolves a profile and then passes a native template string to
`launch_from_cache`. Electron only executes the template it receives. If Svelte has not
resolved a template yet, it should stop before IPC and surface a missing-profile error.
This keeps all fallback logic in Svelte, where it can be edited without rebuilding Electron.
The native layer should not invent or guess a default launch path.
The built-in defaults are organized as canonical profile names plus extension aliases. That
lets multiple file types share one profile without repeating the same app/script details.
The profile object also carries `post_delay_ms`, and a device-specific per-profile
`launch_profiles[profile].post_delay_ms` override can tune the delay without changing the bridge
contract. URL-based presentations remain a special pseudo-extension handled separately from
- **`launch_presentation`** — hardened (2026-05-11, follow-up fix; was the last handler still using `-e`)
- **`control_presentation`** — uses single-line scripts with no path interpolation; `-e` is safe here and retained for simplicity
The `-e` approach breaks on (1) multi-line scripts and (2) file paths containing spaces,
quotes, or parentheses — common in conference presentation filenames.
### Not Exposed via Relay (intentional)
-`get_seed_config` / `get_jwt` — Exposed in the preload but not relayed to the UI. The JWT and seed are injected into the environment at startup; components should not call these directly.
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
**Platform:** PWA only — mobile-first, offline-capable.
**Target users:** Conference exhibitors scanning attendee badges at their booths.
### Recent Changes (2026-04-03)
- Migrated Leads persisted state to Svelte‑5 PersistedState: `leads_loc` now implemented at `src/lib/stores/ae_events_stores__leads.svelte.ts` and the store version constant `AE_LEADS_LOC_VERSION` added to `src/lib/stores/store_versions.ts`.
- Payment UI adjustments: `ae_comp__exhibit_payment.svelte` now accepts a `leads_require_payment` prop and enforces the event-level `mod_exhibits_json.leads_require_payment` flag; a loading guard was added so the component waits for the exhibit record (Dexie `liveQuery`) before deciding which UI to show.
- Tests: update `tests/_helpers/leads_helpers.ts` to seed `leads_loc` defaults and `__version` when needed to avoid localStorage wipe caused by store version checks.
---
## What It Does
The Exhibitor Leads module lets conference exhibitors capture and manage attendee leads directly
from their booth. Exhibitors scan or search attendee badges and build a list of contacts they met.
All data is cached locally (IndexedDB / Dexie.js) for spotty or offline venue Wi-Fi, with
background SWR revalidation against the API when the network is available.
Key capabilities:
- **Badge scanning** — QR scan or text search (name, email, affiliations, badge ID)
- **Lead list** — filterable/sortable, per-exhibitor or per-staff-member view
Both aggregated into `events_func` via `src/lib/ae_events/ae_events_functions.ts`.
---
## Offline / PWA Notes
- All data is stored in `db_events` (Dexie.js) — `exhibit` and `exhibit_tracking` tables
- SWR pattern: IDB cache returned immediately; background API fetch updates IDB and triggers UI refresh
- Search: local IDB first pass (fast), then API revalidation via `search__exhibit_tracking`
-`beforeinstallprompt` event captured at module load time (`src/lib/pwa/pwa_install.svelte.ts`)
— fires within ~1 second of page load, before any Svelte `$effect` runs
- iOS Safari: no native install prompt; shows "Share → Add to Home Screen" instructions instead
---
## Capture Identity
`external_person_id` and `group` on every `event_exhibit_tracking` record record who captured the lead. Resolved at capture time in all three lead capture components (single scanner, multi scanner, manual search):
| Auth type | `kv.type` | Value stored |
| --- | --- | --- |
| Licensed exhibit user | `'licensed'` | Their email address (`kv.key`) |
| Shared exhibit passcode | `'shared'` | `'shared_passcode'` (label — raw passcode is NOT stored) |
| Aether user (admin bypass, no kv) | `undefined` | `$ae_loc.access_type` — e.g. `'trusted'`, `'manager'`, `'super'` |
Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors:
- **Leads list** always filters out `enable = false` records (both IDB fast-path and API results) — no flash of removed records
- **QR scanner**: if a previously-removed badge is scanned, the scanner detects it via `existing_leads_map` (IDB) or API fallback search (`search__exhibit_tracking` with `qry_badge_id` + `enabled: 'not_enabled'`) and shows the reenable card instead of an error
- **Lead detail page**: "Remove Lead" button (two-click confirm in header) sets `enable = false` and navigates back. "Restore Lead" card appears at the bottom of the right sidebar when `enable` is falsy.
-`search__exhibit_tracking` supports `qry_badge_id` param (added) and `enabled: 'not_enabled'` to find disabled records for a specific badge + exhibit combination
---
## Known Gaps
None currently. See TODO__Agents.md for remaining smoke test items.
## Implemented (previously listed as gaps)
### Payment / Stripe
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
confirmation card), Stripe not configured (admin hint), payment form with license tier selector.
Visibility is event-wide: set `event.mod_exhibits_json.leads_require_payment = true` in the event
settings JSON to enable. When `false` (default), both the header CreditCard button and the
"Licenses & Billing" accordion in the Manage tab are hidden. The Stripe component itself is
unchanged — gating is done in `+page.svelte` and `ae_tab__manage.svelte`.
### License Management — Shared Passcode Access
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
### "My Leads" filter for shared-passcode users
Fixed. `external_person_id` is stored as the literal `'shared_passcode'` for shared users (not
the raw passcode string). The `search_params` derived in `+page.svelte` now checks `kv.type ===
'shared'` and resolves to `'shared_passcode'` instead of `kv.key`, so the "My Leads" filter
correctly returns their captured records.
---
## OSIT Admin Notes
- Mark `priority = 1` on an exhibit to make it visible in public search and to enable lead capture
-`license_max` controls how many licensed staff slots an exhibit can have
The Presentation Management module handles the full lifecycle of conference content: sessions, presentations, presenters, presentation files, and room/location assignments. It serves as the "Back Office" interface for event staff.
---
## Data Model
### Object Hierarchy
```text
Event
├── Event File (walk-in/out, hold slides for the whole event)
├── Location (physical room — assigned to Sessions, not the other way around)
├── Track (optional grouping; rarely used)
└── Session (time block; Location assigned here, but may be unset initially)
├── Session File (moderator slides, group/hold slides for this session)
└── Presentation (a talk within the session; must belong to exactly one Session)
└── Presenter (belongs to exactly one Presentation)
└── Presenter File (their slides/materials — the common case)
```
> **Import note:** When program data is initially imported (sessions, presentations,
> presenters), Locations are often not assigned to Sessions yet — rooms may not be
> finalized or the venue's room list may not be set up in Aether. Location assignment
> typically happens as a separate step once the room list is confirmed.
### Relationships
- **Session → Location:** Many-to-one. A Session is assigned to one Location; a Location
hosts many Sessions across the event timeline. Location may be null initially.
- **Presentation → Session:** Many-to-one. A Presentation belongs to exactly one Session.
A Session can have many Presentations (or none, for session-only setups).
- **Presenter → Presentation:** Many-to-one. A Presenter belongs to exactly one Presentation.
Optionally linked to an `event_person_id` for cross-referencing the person record.
- **Event File:** Can be attached at any level — Presenter, Presentation, Session,
Location, or Event. See the File Attachment Levels table.
### File Attachment Levels
Files (`event_file`) can be attached at five levels:
| Level | When Used | Typical Content |
|---|---|---|
| **Presenter** | 99% of the time for individual speakers | Their PowerPoint/PDF/video |
| **Session** | Moderator slides; group/hold content for a specific session | "Session 3 — Group Discussion.pptx" |
| **Location** | Walk-in/out or hold slides for a room across all sessions | Looped PPTX playing between sessions |
| **Event** | Walk-in/out or hold slides used everywhere | Looped PPTX; branding overlay |
| **Presentation** | File attached to the presentation record itself (less common) | Varies |
### Key Objects
| Object | Table | Purpose |
|---|---|---|
| Session | `event_session` | Time block; Location and datetime range assigned here |
| Location | `event_location` | Physical room |
| Presentation | `event_presentation` | A talk within a session; belongs to exactly one Session |
| Presenter | `event_presenter` | Person linked to exactly one Presentation |
| Event Person | `event_person` | Person record within the event context |
| Event File | `event_file` | Uploaded file; attached at Presenter, Presentation, Session, Location, or Event level |
---
## Client Setup Variation
There are no rigid "modes" — events are configured with as much or as little structure
as needed. The platform handles the full range:
**Minimal setup (BGH):**
Sessions have room and time info. No Presentations or Presenters defined.
Staff upload files directly at the session or location level onsite.
**Mid-range setup:**
Sessions defined with named Presentations. Presenters may or may not be tracked.
Mix of pre-uploaded and onsite files. QR codes may be used for quick session/presenter lookup.
**Full setup (LCI):**
Sessions, Presentations, Presenters all defined and managed. External ID labeling
(e.g., "LCI Member ID"). Agreement tracking for presenters. Files managed per-presenter.
The config that drives this is `event.mod_pres_mgmt_json` — see the Configuration section.
---
## Configuration — `mod_pres_mgmt_json`
The event's Presentation Management behavior is controlled by `event.mod_pres_mgmt_json`.
### Convention
| Prefix | Default state | Meaning |
|---|---|---|
| `hide__` | `false` = visible | Feature is ON by default; set `true` to suppress |
| `show__` | `false` = hidden | Feature is OFF by default; set `true` to enable |
### Common Config Keys
| Key | Default | Notes |
|---|---|---|
| `lock_config` | `false` | `true` = force remote→local sync; prevents user overrides of local config |
**Future:** Auto font scaling using mm/inch units (physical paper stock measurements).
Will likely need to revisit the inch ↔ mm conversion and potentially expose the auto-sizing
logic as adjustable rather than replacing it with px overrides.
---
## Context
The Events Badges module is mostly complete for navigation and search. Two key pieces of
functional UI were needed before the first show:
1. **Badge Review Form** ✅ — `ae_comp__badge_review_form.svelte` now has complete field
rendering, edit inputs gated by access level, save/cancel API calls, and display-only
sections (QR code, print status, option/ticket checkmarks). Also includes accessibility
features (text enlargement) and help modal for attendee guidance.
2. **Badge Print Font Controls** ⏳ — The print page header needs screen-only controls
(hidden during `window.print()`) to bump font sizes for the name, professional title,
affiliations, and location sections before printing. These only affect the `ae_comp__badge_obj_view.svelte` render — not the page layout/template structural dimensions.
Read `documentation/MODULE__AE_Events_Badges.md` for full module context before starting.
---
## MANDATORY: Before You Start
1. Run `ae_describe event_badge` (MCP tool) to confirm which fields actually exist in the
DB. Several fields in the spec below may need to be added to `properties_to_save` in
`src/lib/ae_events/ae_events__event_badge.ts` if they are not already saved to IDB.
2. Fields to specifically confirm exist in `event_badge` schema:
- `pronouns`, `pronouns_override`
- `phone`, `phone_override`
- `allow_tracking`
- `agree_to_tc`
- `other_1_code` through `other_8_code` (the "option" fields)
> **Replaces:** `element_ae_crud.svelte` and `element_ae_crud_v2.svelte`
## 1. Overview
Consolidate the legacy CRUD components into a single, high-performance "Aether Object Field Editor" (v3). This component will be the standard for single-property editing across the platform, fully aligned with the FastAPI V3 CRUD patterns and Svelte 5 Runes.
## 2. Strategic Objectives
- **Consolidation:** Retire `v1` and `v2` components in favor of a single, unified codebase.
- **API Alignment:** Native support for `PATCH /v3/crud/{obj_type}/{obj_id}`.
- **Svelte 5 Runes:** Pure `$props`, `$state`, and `$derived` implementation. No legacy imports.
# PROJECT: Site Passcode Security — API-Verified Auth
**Last updated:** 2026-04-10
**Status:** Backend work in progress — frontend pending backend completion
**Priority:** High — passcodes for trusted/administrator access currently in localStorage plaintext
---
## Problem Statement
When a user loads the Aether frontend, the site bootstrap response includes `access_code_kv_json` — a JSON object containing all passcodes for all access levels (administrator, trusted, public, authenticated). The frontend stores this verbatim in `$ae_loc.site_access_code_kv`, which is persisted in localStorage.
**Result:** Anyone with DevTools → Application → Local Storage can see every passcode for every access level on any Aether site. For public/authenticated this is low risk, but for trusted and administrator this is a real exposure — these passcodes can grant control over event data, badge printing, edit mode, etc.
The passcode check (`handle_check_access_type_passcode` in `e_app_access_type.svelte`) is entirely local — it reads the cached values and compares directly. No API call is made. The backend already has a `/authenticate_passcode` endpoint that verifies server-side, but it needs the fixes described below before the frontend can rely on it.
### Source of Truth
`site.access_code_kv_json` is the single source of truth for all passcodes. The `v_site_domain` DB view joins this field from the site table — there is no separate copy. Both the bootstrap response and `/authenticate_passcode` read from the same data.
---
## Threat Model
| Threat | Current | After Fix |
|---|---|---|
| Attacker inspects localStorage | Sees all passcodes in plaintext | Sees a JWT (opaque, no passcode) |
| Attacker uses stolen trusted passcode | Trivial if they have localStorage access | Still possible if they enter the passcode — unavoidable |
| Attacker replays an old passcode after it changes | Works forever (cached value never refreshes) | Fails — API verifies against current DB value |
| Attacker tampers with `access_type` in localStorage | Grants apparent permission but API calls still fail | Same — `access_type` is still persisted separately |
| Passcode reuse across sessions | Works indefinitely | JWT TTL enforces session expiry per role |
| Offline / API-unavailable entry | Works (local cache) | **Blocked** — requires API to verify |
### The fundamental constraint
Passcode-based access is inherently weaker than username/password login with a hashed credential. The system's security model layers passcode access below user login, and API calls themselves are still gated by `x-aether-api-key` + `x-account-id`. The passcode primarily controls **what the frontend shows** and some API-level permission gates for trusted routes.
1.**Never send passcodes to the client.** The frontend stops reading/storing `access_code_kv_json` from the bootstrap response.
2.**Passcode entry triggers an API call** to `/authenticate_passcode`. API verifies server-side against the DB.
3.**On success, the API returns a JWT** — the JWT contains the role, account context, and expiry.
4.**Store the JWT in `$ae_loc.jwt`** (already a field, already wired into `$ae_api`).
5.**On page reload**, check the JWT's `eat` (expires-at) claim locally (base64 decode, no signature verification needed client-side). If expired, drop to anonymous. If valid, `access_type` is already persisted in `$ae_loc`.
### Session restore on reload
-`access_type` still persists in localStorage (no change here)
- The JWT is the **proof** that the access was legitimately granted and is still valid
- On page load: decode JWT payload (base64 the middle segment), check `eat` vs `Date.now()/1000`
- If JWT expired → reset `access_type` to anonymous, clear JWT
- If JWT valid → no action needed, `access_type` is already correct
This gives session expiry without a network call on every page load.
**No passcode caching.** Every passcode entry makes one API call. The JWT handles session persistence — no passcode ever touches localStorage. Performance impact is only at the moment of entry (~50–150ms), which is acceptable for a once-per-session action.
---
## Backend Changes Required
**Note:** The backend fixes described below have been implemented and tested in the `aether_api_fastapi` repository (the `/authenticate_passcode` endpoint now uses explicit role priority, returns a full passcode JWT with `auth_type: 'passcode'`, applies per-role TTLs, and validates passcode length). Frontend changes can proceed once the backend deployment with these fixes is available.
### Backend Agent Follow-Up
If the backend team revisits this area, keep the next round focused on narrowing escape hatches rather than adding new ones:
1. Audit every `x-no-account-id` use and decide whether it is still required for bootstrap, public delivery, or a global-default fallback.
2. Prefer JWT-backed auth once a session exists; do not add new transport-level bypass paths for authenticated UI flows.
3. Mark any remaining bypass-only helper as temporary and add a removal target.
4. Plan the eventual removal of `access_code_kv_json` from public bootstrap payloads once passcode auth is fully deployed.
### Frontend special-case endpoints to review
These are the current frontend-facing exceptions that the backend work should assume are special-cased. None require a frontend/client code change today, but some are intentionally temporary.
| Frontend path / helper | Status | Notes |
| --- | --- | --- |
| `src/routes/+layout.ts` | Keep | Bootstrap site-domain lookup before account context is known. |
| `src/routes/testing/+page.svelte` | Dev-only | Useful for trace testing; do not add to any production allowlist. |
**Phase 2 status:** Not started — removing `access_code_kv_json` from the public site model remains pending.
**File:**`aether_api_fastapi/app/routers/api.py`
The `/authenticate_passcode` endpoint exists and is structurally correct but has four issues that must be fixed before the frontend migrates to using it.
### Fix 1: Passcode matching must use explicit priority order
**Current (wrong):**
```python
forrole,codeinaccess_codes.items():# dict insertion order — not guaranteed
This ensures that if a config mistake causes two roles to share a passcode, the higher-privilege role always wins. It also makes the intent explicit and independent of JSON storage order.
### Fix 2: JWT payload must include all six role flags
**Current (incomplete):**
```python
payload={
'account_id':account_id_random,
'administrator':(matched_role=='administrator'),
'manager':(matched_role=='manager'),
'super':(matched_role=='super'),
# trusted / public / authenticated missing
...
}
```
**Required:**
```python
payload={
'account_id':account_id_random,
'super':(matched_role=='super'),
'manager':(matched_role=='manager'),
'administrator':(matched_role=='administrator'),
'trusted':(matched_role=='trusted'),
'public':(matched_role=='public'),
'authenticated':(matched_role=='authenticated'),
'json_str':json.dumps({
'auth_type':'passcode',# distinguishes from user login JWTs
'site_id':site_id,
'role':matched_role# canonical role string — frontend uses this
})
}
```
The `auth_type: 'passcode'` marker is critical — it allows the frontend and any future backend consumers to distinguish a passcode JWT from a user login JWT.
### Fix 3: Per-role TTL
**Current:**
```python
token=sign_jwt(
secret_key=settings.JWT_KEY,
ttl=3600*24,# hardcoded 24h for all roles
**payload
)
```
**Required:**
```python
ROLE_TTL={
'super':8*3600,# 8 hours
'manager':24*3600,# 24 hours
'administrator':48*3600,# 48 hours
'trusted':48*3600,# 48 hours
'public':24*3600,# 24 hours
'authenticated':12*3600,# 12 hours
}
token=sign_jwt(
secret_key=settings.JWT_KEY,
ttl=ROLE_TTL[matched_role],
**payload
)
```
### Fix 4: Add minimum length validation to `passcode` field
**Current:**
```python
passcode:str=Field(...,description="The passcode to verify")
```
**Required:**
```python
passcode:str=Field(...,min_length=5,description="The passcode to verify")
```
This matches the frontend's 5-character trigger and prevents empty/trivial submissions.
log.warning(f"Auth Failed: Site {site_id} not found.")
returnmk_resp(data=False,status_code=404,response=response,status_message="Site not found.")
```
### Backend Phase 2 (follow-up — not blocking frontend)
**Remove `access_code_kv_json` from the `Site_Domain_Base` response model** (`site_domain_models.py`). This ensures passcodes are never sent to the client even if future code reads from the bootstrap. Requires confirming no other endpoint consumers rely on `access_code_kv_json` being in the base response before making this change.
---
## Frontend Changes Required
**These depend on the backend fixes above being deployed first.**
Remove `site_access_code_kv` from the `AuthLocState` interface and the `auth_loc_defaults` object. The field is unused after 1a. Confirm no other component reads from it first (current grep: only `e_app_access_type.svelte` uses it — confirmed).
---
## Migration Notes
- Users with existing localStorage will still have `site_access_code_kv` cached — this is harmless after the frontend stops reading it. No forced cache clear needed.
- Existing persisted `access_type` is unaffected — users keep their current session level until their JWT expires or they manually clear storage.
- The `$ae_loc.jwt` field is already used by the user login flow. The `auth_type: 'passcode'` marker in `json_str` ensures the expiry logic only targets passcode sessions, not user login sessions.
---
## Files Affected
| File | Repo | Change |
| --- | --- | --- |
| `app/routers/api.py` | `aether_api_fastapi` | **Backend — do first.** Priority ordering, full JWT payload, per-role TTL, min_length on passcode |
| `app/models/site_domain_models.py` | `aether_api_fastapi` | Phase 2: remove `access_code_kv_json` from public model |
| `src/lib/app_components/e_app_access_type.svelte` | `aether_app_sveltekit` | Replace local check with async API call; loading/error UI |
This review covers all modules and their distinct use cases. Changes must be sequenced to avoid breaking live-production systems (Events Launcher, IDAA).
### Scope of Modules
| Module | Routes | Primary Users | Notes |
|---|---|---|---|
| Core / App Shell | `+layout.svelte`, `e_app_sys_bar.svelte` | All users | Foundation — impacts everything |
Skeleton's `.input`, `.select`, `.textarea` classes do not include dark mode styles. This causes white text on white backgrounds in dark mode. Currently patched with inline `<style>` blocks per-component (see `e_app_sys_bar.svelte` lines 693–707).
**Fix:** Global utility in `app.css` (see Phase 1, Step 1). Once added, remove per-component patches.
> **Note:** All legacy `variant-*` classes have been fully removed from the codebase. Use only `preset-*` classes for all buttons and interactive elements.
### Custom `ae_btn_*` Classes (app.css)
These exist in `app.css` and wrap the `preset-*` system. They are valid but underused. Consider adopting where button groups need reuse.
### Rule: Opacity Over Fixed Colors for Muted Text
```html
<!-- ✅ Theme-aware muted text -->
<spanclass="text-sm opacity-60">Note</span>
<!-- ❌ Fixed muted text — breaks in dark mode -->
<spanclass="text-sm text-gray-500">Note</span>
```
> **Exception:** `text-gray-*` is acceptable in components that intentionally use plain Tailwind grayscale for neutrality (e.g., Journals list cards), as long as `dark:text-gray-*` counterpart is always included.
---
## 8. Card & Layout Patterns
See `documentation/AE__UI_Component_Patterns.md` for the full pattern reference.
**Step 1 — Global form dark mode utility** ✅ Target: `src/app.css`
Add a global utility so Skeleton `.input`, `.select`, `.textarea` classes render correctly in dark mode. This eliminates all per-component `<style>` patches.
**Step 2 — FontAwesome → Lucide in events nav/layout files**
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Journal Entry config rework in progress)
> **Last Updated:** 2026-05-05
> **Primary Agent:** Frontend SvelteKit Agent
## 1. Project Overview
This document outlines the modernization of the Journals module UI in the SvelteKit frontend (`aether_app_sveltekit`). The primary goals are to fully leverage the generic V3 API architecture and introduce high-velocity productivity features for journal management.
**Context:** The backend transition to the generic `api_crud` router is complete. Custom legacy routers have been removed. The frontend must now fully align with this pattern and provide a frictionless user experience.
---
## 2. Core Objectives
### 🎯 Primary Goals
1.**V3 API Verification:** Ensure all CRUD operations utilize the generic `api_crud` endpoints (Verified).
2.**Quick Add UI:** Implement a specialized interface for rapid, friction-free entry creation.
3.**Append/Prepend UI:** Allow users to quickly add text to the beginning or end of existing entries without full edit mode.
- [x] Implement Auto-Save toggle and visual status indicators.
- [x] Extract decryption workflow to non-reactive helper.
- [x]**Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
- [x]**Journal Entry Config cleanup:** Summary now lives in Metadata; Alert lives in its own Alerts & Messaging section; Privacy Flags is visibility-only; Admin controls are split out and gated to trusted-access and above.
- [x]**Shared Flags widget:**`AE_Object_Flags` now shows visible button text and hover titles instead of icon-only controls.
- [x]**Modal sizing:** Entry config modal now expands to viewport height instead of stopping at a fixed 60vh body cap.
- [x]**Delete/Remove behavior:** Entry config Admin section now uses the real delete helper. Managers/admins see Delete (hard delete); trusted access sees Remove (disable semantics).
- [x]**RESOLVED:** Decryption workflow stability (Fixed via dependency isolation).
- [x]**Style Standardization (2026-03-06):** Full Skeleton v4 `preset-*` class pass across all 17 journal components. See style token table in Lessons Learned below.
- [x]**Dark mode fixes:** Entry content hover, journal view section/description background and text colors.
- [x]**Modal close button:** All 3 config modals use `dismissable={false}` + explicit `<X>` button in header snippet for correct right-aligned placement.
- [x]**Global select padding:** Added `padding-inline: 0.5rem` to `@layer base` in `app.css` (safe — utility `px-*` classes override it where intentional).
- [ ] Solidify E2EE passcode system for Journals and Entries.
- [ ] Audit encryption flow for Quick Added and Imported entries.
- [ ] Integrate Outbound Email sharing.
---
## 🧠 Lessons Learned: Solving the Svelte 5 Reactivity Hang
During the implementation of the Privacy/Decryption toggle and the new Configuration Modals, we encountered critical browser hangs caused by infinite reactivity loops. Here is how we resolved them:
***The Problem:** An effect would read `save_status` or `tmp_entry_obj.content` to decide if it should sync, but the act of syncing would update those same variables, re-triggering the effect.
***The Fix:** Wrap any "check-only" state or store reads in `untrack(() => ... )`. This allows the effect to use the value without becoming a dependency of it. This is **CRITICAL** when initializing local state from props inside an effect.
We have established a unified design language for configuration interfaces and all Journals UI. **Use these as the module template.**
#### Modal Chrome
***Header/Footer:**`bg-orange-100 dark:bg-orange-900` with consistent orange borders.
***Close button:** Always use `dismissable={false}` on the `<Modal>` and add an explicit `<button>` with `<X>` inside the `{#snippet header()}` so placement is fully in our control. The `flex-1` class on the `<h3>` pushes it right.
***Tabs:** Center-aligned `btn btn-sm` with `preset-filled-primary` (active) / `preset-tonal-surface` (inactive).
***Icons:** Every tab and primary action should have a Lucide icon for better scannability.
***Button titles:** Any button that uses icon+text or icon-only must include a descriptive `title` for hover clarity.
***Explicit Persistence:** Follow "Edit working copy → Save Changes" pattern to prevent accidental store/API churn.
- Any `bg-{color}-100` dynamic background **must** have a `dark:bg-gray-800` (or similar) override — light shades are unreadable in dark mode.
- Hover states on content areas need both light and dark variants: `hover:bg-blue-100 dark:hover:bg-blue-950`.
- Text locked to `dark:text-gray-900` is almost always wrong — use `dark:text-gray-100`.
### 3. Dexie LiveQuery Subscriptions
***The Problem:** Accessing `liveQuery` observables directly in templates results in `[object Object]` or `undefined` property errors.
***The Mandate:** ALWAYS use the `$` prefix (e.g. `$lq__obj`) when passing or using data from a Dexie `liveQuery`.
### 4. Manual Deep Copying vs. Proxies
Svelte 5 state is backed by Proxies.
***The Problem:** Using `JSON.parse(JSON.stringify(proxy))` can sometimes trigger unexpected behavior or loops when used inside a reactive context.
***The Fix:** Implement a manual `deep_copy` helper or selective property assignment when syncing "Original" vs "Temporary" state. This ensures `orig_entry_obj` is a plain JS object, making the `has_unsaved_changes` check stable.
### 5. Journal Entry Config Layout Notes
The Entry Config modal now follows a stricter section grammar:
*`Metadata` contains category, tags, summary, archive date, and template.
*`Visibility & Audience` contains only visibility/audience toggles.
*`Alerts & Messaging` contains alert flag + alert message.
*`Admin` is gated to trusted access and above, and is the only place for notes plus delete/remove actions.
### 3. Concurrency Locking (`is_processing`)
***The Problem:** Decryption (Async) and Auto-Save (Debounced Async) can fire nearly simultaneously.
***The Fix:** Use a simple `is_processing` boolean flag. If any async workflow is active, block others from starting and prevent the `has_unsaved_changes` derived rune from reporting `true`.
### 4. Comparison Normalization
***The Problem:** Trivial differences (e.g., `null` vs `""` or trailing whitespace) would trigger "unsaved changes" and fire the save loop.
***The Fix:** Use a `normalize()` function in the `has_unsaved_changes` derived rune to trim strings and treat `null/undefined` as empty strings during comparison.
---
## ⚠️ Technical Blocker: The "Decryption-Sync" Loop (Resolved)
### The Issue
The component is suffering from a **Reactive Feedback Loop** between decryption, auto-save, and background IDB refreshes.
1.**Decrypting content** triggers a change in `tmp_entry_obj.content`.
2. The **Auto-Save effect** sees this as a manual user edit and saves to the database.
3.**Dexie LiveQuery** detects the DB update and refreshes the object.
4. The **Sync effect** resets the entry to its encrypted state (from DB).
5. The **Auto-Decryption effect** fires again, starting the loop over.
### What we tried:
***`is_processing` flags:** Attempted to block reactivity during decryption.
***`untrack()`:** Attempted to isolate store updates.
***Reference Sync:** Attempted to update `orig_entry_obj` simultaneously with decryption to fool the change detector.
***Direct Store Updates:** Switching from property assignment to `journals_sess.update()` to fix Svelte 5 notification failures.
### Future Fix Ideas:
1.**Native Svelte 5 State:** Refactor `journals_sess` from a Svelte 4 `Writable` to a class using `$state`.
2.**Logic Extraction:** Move decryption logic into a non-reactive class/helper to isolate side effects from the UI render cycle.
3.**Hash Comparison:** Use content hashes for change detection instead of string comparisons to avoid whitespace/normalization loops.
> **Goal:** Eliminate all dependency on legacy API wrappers (`create_ae_obj_crud`, `get_ae_obj_id_crud`, etc.) and ensure 100% adoption of the V3 Standard (`/v3/crud/...`).
---
## 1. Executive Summary
While the **Journals** and **Identity (User/Account)** modules have been successfully migrated to the V3 architecture, a significant portion of the **Events**, **Sponsorships**, and **IDAA** modules still rely on legacy V1/V2 wrappers. This document serves as the master checklist to reach 100% V3 compliance.
**Why this matters:**
***Security:** V3 enforces strict multi-tenant isolation via JWT.
***Maintenance:** Legacy wrappers in `api.ts` contribute to technical debt and "God Object" anti-patterns.
***Performance:** V3 offers optimized search and partial updates (PATCH) that legacy endpoints lack.
---
## 2. Migration Audit (Findings)
The following files have been identified as using legacy CRUD wrappers.
* Verify the module still loads data (check Network tab for `/v3/` requests).
* Verify saving works (check for 400 Bad Request errors).
## 4. Standard Practices (V3)
### A. Permissive Update Mode
To simplify frontend state management, V3 supports ignoring unknown fields in update payloads.
- **Header:** `x-ae-ignore-extra-fields: true` (Enabled by default in `api_patch_object`).
- **Use Case:** Allows syncing objects that contain read-only metadata (e.g. `created_on`, `_lq_id`) without manual scrubbing.
### B. Structured Error Handling
V3 returns detailed error metadata in the `meta.details` object.
- **Implementation:** Core helpers automatically extract this metadata.
- **FastAPI Fallback:** Standard `{"detail": "..."}` responses are automatically wrapped into the `meta.details` format by the frontend helpers.
---
## 5. Known Pitfalls
### A. The "Integer Trap" (Search Mapping)
**Issue:** The backend automatically maps certain fields (like `account_id`) from string IDs to internal integers.
**Symptom:** Providing a string ID in a search body that the backend maps to an integer can result in **Zero Results** if the underlying view expects a string.
**Final Solution (Body + Header Injection):**
1.**Body:** Inject the raw field name (e.g. `account_id`) into the `search_query.and` array to bypass automatic backend mapping.
2.**Headers:** Pass `headers: { 'x-account-id': ... }` manually to provide context for Auth validation.
3.**Isolation (IDAA):** Due to specific bugs in the IDAA module, it has been temporarily isolated to a legacy V2 search function (`qry_ae_obj_li__event_v2`) using `default_qry_str` for text searching, while the main module continues to use the V3 implementation.
---
## 6. Final Cleanup
Once all checkboxes above are completed:
1. [x] Remove legacy exports from `src/lib/api/api.ts`.
This document outlines proposed enhancements for the IDAA Recovery Meeting module. The goal is to make it easier for members to find and attend meetings, especially on mobile devices, while providing IDAA staff with better tools to manage meeting data quality.
## 🏆 The "Big Wins" (Highest Member Impact)
### 1. Automatic Timezone Conversion
***The Problem:** Meetings currently show their "native" time (e.g., 7:00 PM Central). Members must manually calculate the time for their own location.
***The Fix:** The app will automatically detect the member's local timezone and show a converted time side-by-side (e.g., *"7:00 PM Central — 8:00 PM your time"*).
### 2. "Live Now" & "Today’s Meetings"
***The Fix:**
***Live Now:** A high-visibility green "LIVE" badge will pulse next to meetings currently in progress.
***Today’s Section:** A dedicated section at the very top of the list will show only meetings happening today, sorted by time, so members don't have to scroll through the full 140+ meeting list.
### 3. Clearer Meeting Schedules
***The Problem:** Days of the week are currently listed as a flat string (Sunday Monday Wednesday).
***The Fix:** Convert schedules into natural language one-liners: *"Mondays, Wednesdays, and Fridays at 7:00 PM."* This is much faster for the human eye to scan.
### 4. Favorites ("My Meetings")
***The Fix:** Members can "Star" their regular meetings. These favorites will be pinned to the top of their list for one-tap access every week.
### 5. "Add to Calendar"
***The Fix:** A button to automatically add a recurring meeting to a member’s Google, Apple, or Outlook calendar, including the Zoom/Jitsi link in the calendar event description.
---
## 🛡️ Staff Tools & Data Quality
### 6. "Confirmed Only" Default View
***The Strategy:** To encourage meeting chairs to keep their information current, we propose defaulting the list to show **only** meetings confirmed by the Central Office.
***Member Benefit:** Higher confidence that the meeting they are about to join is active and the link is correct.
***Staff Benefit:** Creates a natural incentive for chairs to contact IDAA to get "Verified," as unverified meetings would require an extra click to see.
***The Problem:** On mobile, the warning badge for unconfirmed meetings doesn't explain *why* it's there or *how* to fix it.
***The Fix:** Tapping the badge will show a simple popup: *"This meeting hasn't been verified recently. If you are the chair, please email info@idaa.org to confirm."*
---
## 📱 Ease-of-Use & Mobile Polishing
### 8. Prominent "Join" Buttons & Easy Sharing
***The Fix:** For virtual meetings, we will move the "Join Zoom" button to a prominent, full-width position at the top of the card. We will also add a "Share" button so members can easily text a meeting link to a sponsee.
### 9. Simplified "Quick-Filter" Chips
***The Fix:** Instead of small checkboxes, we will add large "Chips" (buttons) for common filters: `[🖥 Virtual]``[🏠 In-Person]``[🩺 IDAA]``[Caduceus]`. These are much easier to tap on a phone screen.
### 10. Intelligent "No Results" Guidance
***The Problem:** If a member filters too narrowly (e.g., "Caduceus meetings in Hawaii on Tuesdays"), they just see a blank screen.
***The Fix:** A helpful prompt will appear: *"No meetings found for these filters. [Clear all filters →]"* to prevent members from thinking the app is broken.
---
### Next Steps
1.**Feedback:** Staff identifies which 3–4 items are the highest priority for the next update.
2.**Prototype:** We implement the high-priority items in the testing environment for staff review.
3.**Deployment:** Changes are pushed live to the IDAA website.
Clean up inconsistent access-denied and session-expired UX across the app:
1.**Session Expired banner** — When the API returns 401/403, show a non-blocking dismissible banner in the root layout rather than silently failing. The `flag_expired` placeholder in the root layout is already wired for this but nothing sets it.
2.**Standardize Access Denied display** — Replace the one-off browser `alert()` in event settings with a proper in-page gate. Create a small reusable component for inline denial cards.
3.**Maintain intentional special cases** — IDAA Novi UUID gate and the Root Site Access Key gate are correct and must not be touched.
---
## 2. Current State Inventory
### Pattern A — Root Layout: Site Access Key Gate ✅ Working, keep as-is
**Verdict:** Works correctly. The minimal styling is intentional (it's a hard site gate). Keep but no code change needed unless you want to polish the styling later.
---
### Pattern B — Root Layout: Session Expired Banner 🔴 Declared, never set
**File:**`src/routes/+layout.svelte` line 63
**Variable:**`let flag_expired: boolean = $state(false)` — **never set anywhere.**
**Intent:** This should show a non-blocking dismissible banner whenever the API returns 401/403, signaling a stale JWT/session.
**Gap:** API helpers detect 401/403 and log diagnostic info but never fire any store event.
-`onMount`: 500ms delay then `goto('/')` if still not `manager_access` (handles slow hydration)
-`{:else}` block: Shows a styled "Access Restricted" card with Lock icon, description, "Return Home" button while waiting/denied
**Verdict:** This pattern is correct and consistent. The `{:else}` visual gate prevents flashing. The delayed redirect is a graceful fallback. **No changes needed.**
---
### Pattern D — IDAA Layout: Novi UUID Gate ✅ Intentionally custom, keep as-is
**File:**`src/routes/idaa/(idaa)/+layout.svelte`
**Logic:** Async POST to `https://www.idaa.org/api` to verify Novi UUID. `novi_verifying` flag prevents Access Denied flash during network round-trip.
**Verdict:** Intentionally custom to IDAA's member verification flow. **Do not standardize or touch this.**
**Verdict:** Contextually appropriate and functional. The "Try Again" button is good UX. This is a prime candidate to be replaced with a reusable component once one exists, but it is not broken.
---
### Pattern G — API Helpers: 401/403 Detection Without UI Feedback 🔴 Gap
**Current behavior:** Logs auth diagnostics to console, returns `false` or `null`. No store event fired.
**Gap:** When a JWT expires mid-session, the user sees requests silently fail (data doesn't load/save) with no explanation. They may think the app broke.
**Fix:** On 401/403, set `ae_auth_error` store → root layout watches it and sets `flag_expired = true`.
---
### Pattern H — Presenter Auth (`auth__person`) — Existing system, no UX issues to fix now
**Anonymous toggle:** Per-event config allows presenters to upload files without signing in
**Verdict:** Auth system is working. The gating UI in the presenter pages is contextually managed. Not in scope for this cleanup. Revisit when building out the Leads feature or a future auth refactor.
---
## 3. Issues Ranked by Priority
| # | Severity | Issue | File | Fix |
|---|---|---|---|---|
| 1 | 🔴 High | API 401/403 silently fails — users have no feedback | `api_*.ts` | Wire to `ae_auth_error` store |
| 2 | 🔴 High | `flag_expired` never set — session expired banner never shows | `+layout.svelte` | Watch `ae_auth_error`, render banner |
| 3 | 🟡 Medium | `alert()` in event settings — ugly, blocking, not idiomatic | `settings/+page.svelte` | Replace with `onMount` gate + reusable component |
| 4 | 🟢 Low | Badge review inline card — not reusing a component | `badges/.../review/+page.svelte` | Replace when `element_access_denied.svelte` is ready |
---
## 4. Design Decisions
### 4a. Session Expired Banner Design
- **Non-blocking top bar** — similar to the existing `is_offline` and `api_unreachable` banners in the root layout
- **Dismissible** — user clicks X to clear; or auto-hides after signing back in
- **Message:** "Your session has expired. Please reload or sign in again." with a Reload button
- **Trigger:** Any 401 or 403 from any of the three API helpers
**Note:** Only import `ae_auth_error` — no other store changes. Do NOT import `ae_auth_error` at module level if the API helpers are used SSR-side. Use a dynamic import or guard with `browser` check if needed.
- **Session expired banner:** Force a 401 by testing with an expired JWT or by calling an API with wrong credentials. Banner should appear. Dismiss should clear it. Reload should reload browser.
- **Event settings gate:** Navigate to `/events/{id}/settings` without `administrator_access`. Should redirect without any `alert()` dialog.
- **Badge review:** Enter a bad passcode — Access Denied card should appear with "Try Again".
- **IDAA, /core, root site key gate:** Verify no regressions.
---
## 8. Risks & Notes
- The API helpers (`api_get_object.ts` etc.) run in a module context that is imported across many components. The `ae_auth_error` store must only be imported/used inside a `browser` guard or dynamically if the helpers are ever called SSR-side. Check `src/lib/ae_core/ae_core__site.ts` (which uses these helpers during SSR hydration) — **do not set the store from SSR context.** Add `if (browser)` before the `ae_auth_error.set(...)` call.
- The root layout sync effect runs `untrack()` to prevent circular store updates. The new `$effect` for `ae_auth_error` must also use `untrack()` to be safe.
-`flag_expired` should NOT permanently gate the UI — it should only show a top banner. The user may have been mid-editing and should not lose their work. The current `flag_denied` full-screen block is for site key access only.
**Goal:** Make Docker image builds for Aether cache-friendly using BuildKit/buildx and CI registry caching, while keeping local developer caches small and manageable.
Summary
- Implement a BuildKit-friendly multi-stage `Dockerfile` pattern for frontend and API images.
- Add CI `buildx` examples that push/read registry-based cache to avoid local disk bloat.
- Provide cache retention/rotation guidance and developer commands for safe pruning.
Scope
- Repository areas: `aether_container_env/`, root `Dockerfile` (if present), and CI pipeline definitions (Gitea/Drone or other).
- Non-goal: full CI pipeline migration to a new provider. This work provides CI snippets and a PR-ready set of files for your CI team.
- [ ] Add CI step using `docker buildx build` with `--cache-from` and `--cache-to` pointed at a registry cache.
- [ ] Add a scheduled job or registry lifecycle rule to delete old cache images (30 days default).
- [ ] Document required CI secrets and permissions (registry write/read) for the operations team.
- [ ] Run verification builds (dev local with BuildKit; CI runs with cache) and record timings.
Verification
- Local dev: `DOCKER_BUILDKIT=1` build with `--cache-to`/`--cache-from` shows cache hits on second run and faster build time.
- CI: subsequent CI runs log `cache hit` from `buildx` and total build time reduced vs baseline.
- Confirm registry contains `cache` image tags and that rotation job/prune removes old entries.
Notes about Gitea/CI
- Gitea does not include native Actions like GitHub; teams typically use Drone CI, Tekton, or a self-hosted runner that can execute the `docker`/`buildx` CLI.
- The provided `ci_buildx_example.sh` is intentionally provider-agnostic — pasteable into Drone, Jenkins, GitLab CI, or any shell-capable runner.
Risks & Mitigations
- Risk: Unbounded registry cache growth. Mitigation: enforce retention policy and rotation job; prefer a single `cache` tag reused by CI.
- Risk: Developers unfamiliar with BuildKit. Mitigation: examples show simple `DOCKER_BUILDKIT=1` usage and local cache prune commands.
Next steps for the container team
1. Review examples in `aether_container_env/` and adapt the Dockerfile to your runtime constraints (ssl certs, env injection, secrets).
2. Add a CI job using the `ci_buildx_example.sh` snippet; configure registry credentials as secrets.
3. Add a scheduled job to rotate/delete old cache images or configure registry lifecycle rules.
4. Run a before/after benchmark of `time npm run build:prod` inside the build stage to quantify improvement.
The badges module has accumulated the same class of problems as pres_mgmt before its cleanup:
-`mod_badges_json` is typed as `any` in `ae_types.ts` and `key_val | null` in `db_events.ts` — no canonical TypeScript interface exists.
- Badge search and UI state still lives in `events_loc.badges` (Svelte 4 nested store), with manual `typeof x === 'undefined'` guards in `+page.svelte` and `ae_comp__badge_search.svelte`.
-`ae_events_stores__badges_defaults.ts` already has typed `BadgesLocState` and `BadgesSessState` interfaces wired into `events_loc` — but these have not yet been promoted to a standalone `PersistedState` like pres_mgmt's `pres_mgmt_loc`.
- The `edit_permissions` sub-object (which controls which badge fields each access level may edit) is documented and wired up in `ae_comp__event_settings_badges_form.svelte`, but the review page (`[badge_id]/review/+page.svelte`) still uses hardcoded defaults with `TODO` markers instead of reading from `mod_badges_json`.
-`trusted_passcode` and `administrator_passcode` are stored in `mod_badges_json` and managed via the legacy settings form — no dedicated config UI exists.
- Admin must edit the settings form (or DB directly) to change badge config — no standalone, grouped config page exists (unlike pres_mgmt, which now has `/pres_mgmt/config`).
---
## Goals
1.**Canonical config schema** — define `BadgesRemoteCfg` TypeScript interface for `mod_badges_json`
2.**New Svelte 5 store** — promote `events_loc.badges` to a standalone `PersistedState` (`badges_loc`) with its own localStorage key
3.**Wire `edit_permissions`** — connect the review page to `mod_badges_json.edit_permissions` (remove hardcoded defaults)
4.**Config UI** — dedicated admin page at `(badges)/badges/config/` for managing `mod_badges_json`
5.**Security review** — ensure passcode fields are never exposed to non-administrator access
---
## Canonical Remote Config Schema
`BadgesRemoteCfg` — the authoritative TypeScript interface for `event.mod_badges_json`:
```typescript
interfaceBadgesRemoteCfg{
// Search & UI behaviour
enable_mass_print: boolean;// show the mass-print controls
enable_add_badge_btn: boolean;// show the "Add Badge" button
enable_upload_badge_li_btn: boolean;// show the "Upload Badge List" button
enable_search_qr: boolean;// enable QR scan search
// QR code configuration
qr_type: string|null;// QR payload format (e.g. 'badge_id', 'url')
// Access control — passcodes for attendee / staff tiered access
// WARNING: Only expose to administrator_access. Never render client-side for lower levels.
The old `ae_comp__event_settings_badges_form.svelte` can be retired after the config page is live —
keep the file for now but stop importing it from the settings page.
---
## Security Notes
-`trusted_passcode` and `administrator_passcode` are sensitive credentials.
- The config page must be gated at `administrator_access` (not just `manager_access`).
- Input fields should use `type="password"` with a show/hide toggle — do not render as plain text.
- Never include passcode values in client-side logs or error messages.
-`edit_permissions` affects what data attendees can self-modify — changes take effect on the next page load (no caching concern since it's read from `mod_badges_json` on load).
---
## Migration Path
Safe and backward compatible — the review page already falls back to hardcoded defaults.
1. New `BadgesRemoteCfg` interface — no DB changes needed
2.`ae_events_stores__badges.svelte.ts` — new file, new localStorage key (`ae_badges_loc`)
3. Migrate `$events_loc.badges.*` → `badges_loc.current.*` in two files (~48 refs)
4. Wire review page `can_edit_fields` to `mod_badges_json.edit_permissions`
5. Build config UI page and update settings page
---
## Implementation Steps
- [x]**Step 1** — Define `BadgesRemoteCfg` TypeScript interface (added to `ae_events_stores__badges_defaults.ts`; also extracted `default_authenticated_can_edit` and `default_trusted_can_edit` constants)
- [x]**Step 2** — Created `ae_events_stores__badges.svelte.ts` with `PersistedState`; added `AE_BADGES_LOC_VERSION` to `store_versions.ts`
- [x]**Step 3** — Migrated `$events_loc.badges.*` → `badges_loc.current.*` in `+page.svelte` and `ae_comp__badge_search.svelte`; removed all manual `typeof` guards
- [x]**Step 4** — Wired `edit_permissions` into review page `can_edit_fields`; the two TODO blocks resolved
- [x]**Step 5** — Built config UI at `(badges)/badges/config/+page.svelte` (administrator_access gated)
- [x]**Step 6** — Updated settings page `Badges` section with link to config page; retired the old form component import
- [ ]**Step 7** — Update active event(s) via new UI; verify passcode fields function correctly
> **Implementation note (2026-04-02):** Passcode fields use plain `type="text"` inputs, not `type="password"`. This matches the admin UI convention for this codebase.
-`BadgesLocState` already has typed interfaces in `ae_events_stores__badges_defaults.ts` — this is ahead of where pres_mgmt was. Steps 1-3 are therefore lower risk.
- The `BadgesSessState` (in-memory, resets on page load) does **not** need to move — it can stay in `events_sess.badges` inside the main store for now; it contains no persisted user prefs.
-`enable_search_qr` and `qr_type` need validation: verify what QR type values are actually consumed by the scan component before exposing them as free-text inputs. A select with known options is safer.
- Badge type code options (`member`, `non-member`, `guest`, etc.) are defined per Event Badge Template — the config page should not hardcode them. If badge type selects are needed in config, pull from `db_events.badge_template` liveQuery.
- The `agree_to_tc` field in `can_edit_fields` is a placeholder — no Terms & Conditions flow exists yet. Gate it with a note in the UI.
* Sign in and "claim" an assigned license linked to an Exhibitor
* Collect and manage Leads per Exhibitor
* Export Leads data to CSV or XLSX
* Exhibitors will have the link to their Exhibit sent to them or they can use the Exhibit Search to find their Exhibit and sign in with the shared passcode or their assigned licensed user credentials.
---
## Primary Leads Pages/Tabs
1. start - Sign In / Licenses
* Exhibit passcode sign in
* Exhibitor Leads user sign in
* Payment - Leads Payment
2. add - Add (Search/QR)
* Text search
* QR scan
3. leads - Leads List
* List of Attendees (Event Badge ID) linked to Exhibit
* Click to view/edit Exhibit Tracking entry
4. manage - Leads (app and exhibit) Manage
---
## Exhibitor Sign In and Licenses:
* An Exhibit needs to have a "Shared exhibit passcode" for primary sign in.
* A Licensed Exhibit staff person needs to have a License assigned to them. It is important that the email address not be changed per license. Or if they do, be aware of the affects on tracked attendees. They can then sign in with that email address and passcode assigned. And/Or we need to make the email sign in link work as well.
* Once signed in with the Exhibit passcode, they can change it (passcode) and assign Exhibit Leads licenses once they have paid.
* An exhibitor marked as "priority" means they have paid. Only OSIT admins can mark them as paid.
* Should at least claim/assign one initial user license so an Exhibit related staff can sign in (without the shared Exhibit passcode). Then add, update, or remove licenses based on max allowed per Exhitibor.
Payment:
* Not Paid: <span class="fas fa-question text-red-500 m-1"></span> <span class="fas fa-credit-card mx-1"></span> Waiting for payment
A client staff (Trusted Access or above) or someone signed in with an Exhibit passcode can add/edit/remove icenses.
License:
* full_name
* email
* passcode
---
## Structure Overview
* There are 4 primary tabs for the Event Exhibit Leads module.
* The overall header/footer should hide by default once signed in.
* If not signed in, only the first tab, to start and sign in is shown.
---
## [tab 1] Start / Sign In / Payment
* Only shows when not signed in as Exhibit Licensed Leads User
* Sign in with Exhibit passcode
* This will then allow them to Manage the Exhibit License
* Sign in with Exhibit Licensed user
### [tab section] Payment
* Shows if signed in with Exhibit Passcode and not marked as paid / priority
* Field for license info - event_exhibit.license_li_json
* Use Exhibit passcode first?
* Select number of licenses
* Select number of small/large devices
* Make payment (Stripe)
### [tab section] Licensed Users
* Shows if signed in with Exhibit Passcode and are marked as paid / priority
* Fill in Exhibit staff users per max licenses
## [tab 2] Add - Search / QR Scan
* One button that toggles between showing and hiding the QR add mode? The text search is always shown above or below the QR scan camera image area.
* Search - Essentially the same as the Badge search. Full name, email, affiliations, ID, etc
* QR Scan - Allow for auto add toggle instead of confirming per scan. Allow for manual entire of Badge ID
* The QR scan is basically just using the Event Badge ID encoded in the persons QR code on their badge. In fact it can probably just populate the text search field?
* Must include the Leads licensed user's email address when adding to the Exhibitor Tracking list. Linked using event_exhibit_tracking with the Licensed user's email address.
## [tab 3] Leads - List of Attendee Leads for Exhibitor
* Allow for toggle between showing all per Exhibit and per licensed user based on their email address. Not perfect, but works well enough.
* Allow for easy edit or remove
* Button to Export Data - CSV or XLSX
* Toggle for show/hide Hidden records
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
## [tab 4] Manage / Config
### Exhibit Specific
* Priority/payment toggle - Administrator Access or above
* Max licenses (number) - readonly or edit for Administrator Access or above
* Small devices (number) - readonly or edit for Administrator Access or above
* Large devices (number) - readonly or edit for Administrator Access or above
* Exhibit (shared) Passcode
* Same Exhibit Leads License list component as the Start tab's Licensed Users section
### App Specific
* Show/Hide Payment Tab
* Additional Settings:
* List refresh interval in seconds - default 25 seconds; 1 second to 2 minutes (120000)
* Basic reload/refresh
* Clear Indexed DB
* Clear localStorage
* Auto hide header/footer on sign in - default true
* (?) Turn on iframe mode
* (?) Show or hide additional details - Use "$events_loc.show_details"?
* Can add custom exhibitor_notes and custom responses_json
* Can set Priority (Star/Flag)
* Can set Sort (number)
* Can set Hide (toggle)
---
## App Specific - Leads Module Config Options and Stored Data
* Signed in using Shared Exhibit Passcode
* Can make payment and manage user licenses
* Need to be able to sign in with Shared Passcode and Leads License passcode.
* Signed in using one Leads License slot they were assigned to
* Things will be tied to their email address
* Can add, edit, remove, etc the Leads specific to an Exhibit.
* This list is shared among all Licensed staff for a specific Exhibit. It is recommended they stay toggled to only showing their additions by default.
* Need to be able to sign in with Shared Passcode and Leads License passcode.
* Show and Hide payment tab (override sort of)
* Show and Hide header/footer (override sort of)
* Use iframe mode??
* Light and Dark mode??
* List refresh interval
* Tracking list sorting
* Show and hide hidden records
* Show and hide enabled records (Administrator Access or above)
There are essentially 3 ways to "Sign In":
1. Aether Platform in general using a defined Core User or using a Client Site specific passcode.
2. Exhibit shared passcode for general Exhibitor management. Should only be briefly needed to pay and assign license slots
3. Licensed Leads User using the email address and passcode assigned by the Exhibitor manager or OSIT/client staff person.
---
## First Steps for Leads v3
Create stub directories, pages, and other supporting files for each primary tab, and each primary section within each tab, and for viewing a specific Lead (Exhibit Tracking).
Once we have that looking good, then we can add the functionality. So we are kind of laying out the framework and pre-documenting (commenting) the files as we go.
---
## For Reference if Needed
Known goodish reference version of Leads v2ish:
d1021e28227db6d49b16cc48e324872b1a322da3 November 19, 2025 at 10:45 PM
* Has a search for Exhibits section that allows for searching Exhibits by name or code.
* Results can be sortable. They can be sorted by name, booth number, or last created/updated.
* This is for exhibitors to find their Exhibit and sign in with the shared passcode or their assigned licensed user credentials.
* If not signed in with Administrator Access or above, then only show results for Exhibitors that have been marked as "priority" (paid). Otherwise show all Exhibitors (to admins).
* Has a list of Exhibits with basic info and link to Exhibit Leads management page for each Exhibit. Do not show results by default or if less than 3 characters in search and they are not signed in with Trusted Access or above.
## [exhibit_id page] Exhibit Leads Module
* This is minimalistic. Only the tabs and content needed.
### Header/Footer
* Overall header/footer should hide by default once signed in. Use Aether iframe mode?
* Leads module header should show the Exhibit name and booth number. It should also have a button to toggle the Add Lead tab and Lead List tab. There should also be a button to show the Manage/Config tab for the Exhibit.
Header: [Name and Booth # text] [Add Lead / Lead List toggle] [Manage/Config button]
### Tabs:
I am probably using the term "tab" loosely here. It may just be sections that show/hide based on buttons or whatever. The main thing is to keep the UI simple and not overwhelm the user with too many options at once. The main flow should be to easily add leads and then view/manage those leads. The Manage/Config tab is more for the person managing the Exhibit and should be separate from the lead capture and management interface.
1. Start - Sign In / Licenses / Payment
2. Add - Search/QR
3. Leads - List of Attendee Leads for Exhibitor
4. Manage - Leads (app and exhibit) Manage
### [tab 1] Start / Sign In / Payment
* Only shows when not signed in as Exhibit Licensed Leads User
* Show notification and or message that this is a PWA and can be installed on their device for easier access.
* Sign in with Exhibit passcode
* This will then allow them to Manage the Exhibit License
* Sign in with Exhibit Licensed user
* This will then allow them to Manage the Exhibit License
* Payment - Leads Payment
* Not Paid: <span class="fas fa-question text-red-500 m-1"></span> <span class="fas fa-credit-card mx-1"></span> Waiting for payment
* This will then allow them to Manage the Exhibit License
* Change to Sign Out once signed in
* Once signed in, show message to the Exhibit person. Allow them to select number of Licenses, make a payment, and manage the Licenses if paid.
* Sign in with Exhibit Licensed user
* This will then allow them to Manage the Exhibit License
* Change to Sign Out once signed in
* Once signed in, show message to the Leads user. A big button should allow them to then start adding leads.
* Payment (Stripe) - Leads Payment
* Only show if signed in with Exhibit "shared" passcode and not marked as paid.
* Licenses (table):
* A client staff (Trusted Access or above) or someone signed in with an Exhibit passcode can add/edit/remove licenses.
* License:
* full_name
* email
* passcode
* Buttons and Inputs:
* Sign in with Exhibit passcode
* Sign in with Exhibit Licensed user
* Payment - Leads Payment
* Add/Edit/Remove Licenses (if signed in with Exhibit passcode or Trusted Access or above)
### [tab 2] Add - Search/QR
* Show only when signed in as Exhibit Licensed Leads User or Trusted Access or above.
* Allow for text search of Attendee Badge ID, QR code, name, email, or affiliations.
* Allow for QR code scan to add Attendee Badge as Lead.
* Once found, show basic Attendee Badge info and button to "Add as Lead".
* If already added as Lead, show message and button to "View Lead".
* Sections:
* Text search
* QR scan
* Results with "Add as Lead" or "View Lead" button
* Buttons and Inputs:
* Text input for search
* Button to trigger search
* Button to trigger QR scan (opens camera and scans QR code on badge)
* Button to "Add as Lead" if Attendee Badge found and not already a Lead
* Button to "View Lead" if Attendee Badge found and already a Lead
Functions needed:
* Search function to find Attendee Badge by Badge ID, QR code, name, email, or affiliations.
* QR code scan function to read QR code and find Attendee Badge.
* Add Lead function to create Exhibit_tracking entry linking Exhibit and Attendee Badge.
### [tab 3] Leads - List of Attendee Leads for Exhibitor
* Allow for toggle between showing all per Exhibit and per licensed user based on their email address. Not perfect, but works well enough.
* Allow for easy edit or remove
* Sections:
* List of Leads with basic info and buttons to Edit or Remove
* Options:
* Filter by Licensed user email address (dropdown of emails that have added leads for this Exhibit)
* Toggle for show/hide Hidden records
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
* Buttons and Inputs:
* Button to Export Data - CSV or XLSX
* Toggle for show/hide Hidden records
* Select options for sorting: Newest added first, Oldest added first, Alpha ascending, Alpha descending, Last updated first
* Should it have a text search?
* NOTE: It is probably easiest for them to us the search tab to find a lead that has already been added. It will show "View Lead" button if already added.
Functions needed:
* Load Leads function to get Exhibit_tracking entries for the Exhibit.
* Filter function to filter by Licensed user email address.
* Sort function to sort by selected option.
* Export function to export displayed Leads to CSV or XLSX.
- **Summary**: Investigation, targeted repair, and successful integration of the AE Firefly theme family and key UI components. This document records the final resolution, the migration of the Svelte 5 Modal component, and the validation of the `ae_app_3x_llm` branch as the project's stable baseline.
### 1. Root Cause Resolution (Themes)
- **Variables outside selectors**: Fixed. Custom-property declarations in `src/ae-firefly*.css` were moved into proper `[data-theme='AE_Firefly*']` selector blocks.
- **Variable precedence**: Resolved by enforcing `--background: ... !important` within the theme-specific selectors to ensure they override global `:root` defaults.
- **Files Repaired**:
-`src/ae-firefly.css`
-`src/ae-firefly-steelblue.css`
-`src/ae-firefly-indigo.css`
-`src/ae-firefly-rainbow.css`
### 2. Actions Taken (Recovery Phase)
- **Surgical Integration**: Selective harvesting of "90% done" work from WIP branches while preserving the integrity of the Lucide migration and Svelte 5 baseline.
- **`element_modal_v1.svelte`**:
- Successfully imported from `wip-modal-fix-attempt`.
- **Full Refactor**: Migrated from deprecated `svelte/legacy` and `<slot>` patterns to **Svelte 5 Snippets** and `{@render}` tags.
- Verified and tested as the new standard modal component.
- **Selective Vetting**:
- **Abandoned**: `element_input_v2.svelte` and legacy Badge View v1 (rejected due to legacy FontAwesome regressions and high error counts).
- **Removed**: Redundant `element_data_store_v2.svelte` (superseded by `v3`).
- **Kept**: Clean, Lucide-based versions of all core components already present on `ae_app_3x_llm`.
### 3. Repository State (Final Validation)
- **Baseline**: `ae_app_3x_llm` is now the unified, verified "known-good" state.
- **Validation**: `npx svelte-check` performed on the merged state returned **0 errors and 0 warnings**.
- **Cleanup**: Temporary integration branches have been deleted.
- **Backups**: `wip-modal-fix-attempt` and `wip/theme-fix` remain as reference points but are no longer active.
### 4. Merged Files (Key Updates)
-`src/ae-firefly*.css` (Repaired themes)
-`src/lib/elements/element_modal_v1.svelte` (New Svelte 5 Modal)
- **Verification**: Verified via `svelte-check` and theme inspections.
---
*Prepared by: Gemini CLI (March 17, 2026)*
---
*Archival note (2026-03-20): `element_modal_v1.svelte` (referenced in §2 as "new standard modal") was subsequently retired — it had zero active importers. Modal usage in the codebase relies on Flowbite `<Modal>` component. See `AE__UI_Component_Patterns.md` §11.*
2.**Missing microtask yields**: Added `await Promise.resolve()` after each `db_save_ae_obj_li__ae_obj()` call to ensure Dexie liveQuery observers fire before functions return.
3.**Fire-and-forget nested loads**: Changed `forEach()` to `await Promise.all()` in presentation loader to block until all presenter loads complete.
**Result:** Session view now renders presentations AND presenters on first load without manual refresh.
**Performance Impact:** Adds ~100-200ms to initial navigation (time to write 1-5 presenter records + microtask yields), but guarantees correct first-render.
-`documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` - Added "try_cache: false" bug section with detailed explanation
- Code comments added explaining the critical fixes in all modified loader functions
- Journals module updated with microtask yields for consistency (already preserved try_cache correctly)
**Test Status:**
- Manual testing: ✅ Confirmed working (session + presentations + presenters render on first load)
- Playwright test: Created at `tests/coldstart_event_session.test.ts` (requires valid session ID to run)
**Lessons Learned:**
1.**Always preserve `try_cache` through nested data loads** - Disabling caching at any level breaks the entire chain
2.**Add microtask yields after IndexedDB writes** - Ensures liveQuery observers fire before functions return
3.**Block on nested loads with `await Promise.all()`** - Fire-and-forget forEach() causes race conditions
4.**Journals module was already correct** - It preserved `try_cache` for nested entry loads; only added yields for consistency
5.**The existing blocking pattern was sufficient** - No special helper function needed; the bug was in the loader implementation, not the architecture
**What We Implemented:**
We effectively implemented **Option A (Blocking Hydration)** but at a lower level than originally planned. Instead of creating a new `load_session_with_relations()` helper, we fixed the existing loader chain to properly block and cache nested data. The `+page.ts` already used `await` for the session load - it just wasn't working because the underlying loaders weren't caching nested data.
---
## Original Plan (For Reference)
Goal
Make the Presentation Management Session view deterministic on cold-start (empty IndexedDB). The page must render Presentations, Presenters, and Hosted Files without requiring manual refreshes.
- Minimize user-perceived latency — keep navigation snappy where possible.
- Avoid large architectural changes unless necessary.
Options (high level)
A) Blocking Hydration (recommended for correctness)
- Block the route `+page.ts` load until the session and all directly required related objects are fetched from the backend and written into IndexedDB. Return `initial_session_obj` in the load data for immediate rendering.
- Pros: simplest to guarantee first-draw correctness; minimal component changes.
- Cons: adds latency to navigation (can be mitigated with optimistic UI or progress indicator).
B) Prefetch Related Records + Hydrate Fallback (hybrid)
- Non-blocking load but `+page.ts` returns `initial_session_obj` and small related-objects payloads (presentations, presenter IDs, hosted_file metadata). Components use these fallbacks while `liveQuery` takes over.
- Pros: keeps navigation responsive; often sufficient.
- Cons: requires careful payload shaping and DB write ordering.
C) Explicit Dependency Chaining in UI (advanced)
- Keep non-blocking loads and use explicit dependency chaining: write session -> await write completion -> then write presentations -> await -> then presenters, ensuring microtask queue flushes between writes. Use targeted `liveQuery` re-creation only when upstream dependency fully resolved.
Start with Option A (Blocking Hydration) for the session page to restore deterministic behavior quickly. After correctness is achieved, consider converting to Option B or C for improved perceived performance if needed.
Detailed Steps (Option A - Blocking Hydration)
1) Add a small helper in `events_func` (e.g., `load_session_with_relations`) that:
- fetches session by ID from API
- fetches related presentations (limit/filters as needed)
- fetches presenters referenced by those presentations (deduplicate IDs)
- fetches hosted_file metadata for presentation files (if required for the view)
- writes all results to IndexedDB in a controlled order (session -> presentations -> presenters -> hosted_files)
- returns a compact `initial_session_obj` payload containing fields needed for first-draw (session, presentation list, presenter summary)
Implementation note: Use `await db.transaction('rw', db_events.session, db_events.presentation, db_events.presenter, async () => {...})` if atomicity helps. Alternatively write in sequential awaits and call `await Promise.resolve()` after each write to let the microtask queue settle.
2) Update route loader: `src/routes/events/[event_id]/(pres_mgmt)/session/[session_id]/+page.ts` (create if missing) to call and `await` the helper, then return `initial_session_obj` on `data`.
3) Ensure the page component `+page.svelte` uses the `initial_session_obj` as immediate fallback (it already does in Aether).
4) Add instrumentation logs inside `liveQuery` closures and the helper to verify ordering during QA.
5) Add tests (see below) and manual verification steps.
Alternative (Option B - Hybrid) Implementation Notes
- If you cannot block the route, return an `initial_session_obj` that includes minimal related object arrays (IDs + small metadata) and have `+page.svelte` write those into IDB before mounting heavy child components.
- Use `untrack()` to set selection IDs so stores are updated without causing premature reactivity loops.
Explicit Dependency Chaining (Option C) Notes
- Implement a single `prefetch` function that sequentially performs writes and `await Promise.resolve()` between stages.
- For debugging, add microtask delays (e.g., `await 0`) between writes to observe behaviour.
Testing and Verification
1) Integration test (Playwright recommended)
- Clear IndexedDB for the app origin.
- Navigate to `/events/<event_id>/.../session/<session_id>` and assert that the presentation list and presenters are visible within N ms without manual refresh.
- Repeat on subsequent navigations to ensure no regressions.
2) Unit tests
- For `events_func.load_session_with_relations`, stub API responses and assert DB writes are made in expected order.
3) Manual QA
- With a cold profile or after clearing Site storage, navigate to the session page and confirm content is present after the initial navigation and that no manual refreshes are required.
Migration and Rollout
- Implement Option A behind a feature flag if you want to control rollout.
- Short-term: apply Option A to the single problematic route to reduce blast radius.
- Long-term: consider a library-level helper to standardize "blocking prefetch for nested related records" across other pages.
Rollback Plan
- Because changes are additive and limited to one route and helper, revert the `+page.ts` modification and helper call to restore prior behavior.
Deliverables for tomorrow
-`events_func.load_session_with_relations` helper (TS) + unit tests
- Updated `+page.ts` loader for session route to `await` helper and return `initial_session_obj`
- Small test harness / Playwright test that reproduces the cold-start issue and verifies the fix
- Hybrid or chaining implementations: additional 2-6 hours depending on thoroughness
Notes about Svelte 5 + Dexie specifics
- Keep `liveQuery` closures stable; capture primitive IDs rather than reactive objects.
- Use `$derived` and `$derived.by` to keep observable instances stable across renders.
- Use `untrack()` when setting selection values to avoid premature subscriptions.
- After DB writes, allowing the microtask queue to settle (`await Promise.resolve()`) helps ensure observers are notified in the expected order during development and debugging.
If you want I can implement Option A for the session route tomorrow (create helper, update loader, add test).
> `ae_app` is live in `aether_container_env/docker-compose.yml`. Both frontend and backend deploy together via `npm run deploy:staging` / `npm run deploy:prod`. Internal `ae_api` networking active. Healthcheck wired. Old `ae_env_node_app` is superseded (archive when ready).
> **Goal:** Consolidate the SvelteKit Frontend and FastAPI Backend into a single Docker Compose environment within `aether_container_env`.
## 1. Overview & Benefits
Currently, the platform runs in two separate Docker environments (`ae_env_node_app` and `aether_container_env`). Combining them into one allows for:
- **Unified Lifecycle:** Start/Stop/Update the entire stack with one command.
- **Internal Networking:** The Frontend (SvelteKit) can communicate with the Backend (FastAPI) via the high-speed internal Docker network (`ae_api:5005`) instead of routing through external IPs.
- **Shared Infrastructure:** Single instances of Redis, Dozzle, and Nginx serving both tiers.
- **Simplified Deployment:** Reduces the need for sibling directory dependencies on production servers.
---
## 2. Target Architecture (Workstation & Prod)
The unified environment will reside in `~/OSIT_dev/aether_container_env/`.
### Directory Mapping
- **API Source:** `~/OSIT_dev/aether_api_fastapi` -> Mounted to `ae_api`
- **App Source:** `~/OSIT_dev/aether_app_sveltekit` -> Mounted to `ae_app`
- [x]**[Stores] Phase 1 — Dead code cleanup** (`ae_stores.ts`, `ae_events_stores.ts`, `ae_idaa_stores.ts`): removed `ver_idb`, stale comments, `console.log` lines, Stripe button block (zero consumers), personal Novi UUIDs, dead alternatives. Net: −202 lines across 3 files. svelte-check: 0 errors. (2026-03-16)
- [x]**[Stores] Phase 2a — Split defaults into domain sub-files**: `ae_stores__auth_loc_defaults.ts`; `ae_events_stores__badges/launcher/leads/pres_mgmt_defaults.ts`. Spread-merged back into store structs — zero consumer changes. (2026-03-16)
- [x]**[Badges]** Badge print page svelte-check fix: extracted print CSS to `static/ae-print-badge.css`; fixed unclosed `<script>` tag in `print/+page.svelte`. (2026-03-16)
- [x]**[Svelte/Tests]** svelte-check cleanup: fixed `select_ref_badge_type``$state()` declaration; two `<svelte:component>` deprecations in launcher components; `page.evaluate()` two-arg pattern in `badge_print_layout.test.ts`. (2026-03-16)
- [x]**[Launcher]** Hosted file download button `require_auth` prop — added `require_auth?: boolean` (default `true`) to `ae_comp__hosted_files_download_button.svelte`; all existing consumers unchanged. Launcher `launcher_file_cont.svelte` passes `require_auth={false}` so unauthenticated kiosk users can open/download files without being blocked. (2026-03-16)
- [x]**[Security]** `PUBLIC_AE_API_SECRET_KEY` audit complete. Key is `PUBLIC_*` by design (always in client bundle). Highest-risk anonymous path uses limited-permission `PUBLIC_AE_BOOTSTRAP_KEY`. Full server-side migration not justified given JWT + account_id auth layers. Current state acceptable. (2026-03-11)
- [x]**[UX]** Session Expired banner — `ae_auth_error` store wired to API helpers; root layout sets `flag_expired` on 401/403; non-blocking dismissible banner rendered. (2026-03-12)
- [x]**[UX]** Access Denied UI standardized — `element_access_denied.svelte` created; `/core` layout, `/events/settings`, and `/events/badges/review` updated to use it. (2026-03-12)
- [x]**[Build]** Rollup/Vite circular dependency warnings eliminated — `manualChunks` in `vite.config.ts` colocates all `svelte/*` internals into a single `svelte-vendor` chunk, preventing `runtime.js` / `index-client.js` split (~35 warnings gone). (2026-03-11)
- [x]**[Refactor]** `try_cache` audit + sponsorship/event_file/hosted_file SWR alignment — removed vestigial `try_cache` params from `generate_qr_code`, `ae_core_functions` wrappers; added SWR fast/slow path to sponsorship loaders; changed `event_file` and `hosted_file` single-object loader defaults from `false` → `true` for consistency. (2026-03-11)
- [x]**[DevOps]** Frontend + Backend unified into single `aether_container_env` Docker Compose. `ae_app` service live with healthcheck, single exposed port (`AE_APP_NODE_PORT`), internal `ae_api` networking. Deploy scripts in `package.json` both target `../aether_container_env/docker-compose.yml`. (2026-03-10)
- [x]**[DevOps]** `/health` endpoint live at `src/routes/health/+server.ts`. Docker `HEALTHCHECK` uses it. (2026-03-10)
- [x]**[UI]** Dark mode `color-scheme` fix — `html.dark/light { color-scheme }` in `app.css`; all native browser controls now sync to app dark mode. (2026-03-10)
-`ae_loc` (persisted) — auth flags, site cfg, UI state; ~471 consumer sites across 150+ files
-`idaa_loc` (persisted) — Novi auth, IDAA query prefs
These two cause the most reactive noise. Migrating them also unlocks Phase 2c (separate `ae_auth`
store) since the callsite sweep is now required anyway.
- [ ]**Phase C — Remaining persisted stores:**
-`ae_api` (persisted) — API config / JWT
-`ae_events_stores` persisted entries (badges, launcher, leads, pres_mgmt loc stores)
- [ ]**Phase D — Non-persisted writable stores:**
-`ae_sess`, `idaa_sess`, `slct`, `slct_trigger`, `ae_auth_error`, `ae_trig`, `ae_snip`, etc.
- Lower urgency (no localStorage churn), but fine-grained reactivity still beneficial.
- [ ]**Phase E — Phase 2c (unblocked after B):** Split `ae_loc` into `ae_auth` + `ae_app`
(see entry below — ~471 callsites, but sweep is cheap once already touching every consumer).
**Project plan doc needed:** Yes — scope is app-wide. Do NOT start Phase B without Phase A.
---
### [Stores] Refactor — Phase 2c (deferred)
Phases 1, 2a, 2b are complete (see ✅ Completed below). One phase remaining:
- [ ]**Phase 2c — Actual separate stores (`ae_auth`, `ae_app`):** Requires touching ~471
`$ae_loc.*` auth-field read sites across 150+ files. Deferred until a Svelte runes migration
of the store layer itself (touching every component anyway makes the callsite sweep cheap).
### [Backend] Join event_location_id onto event_presenter API view
The `event_presenter` object currently has `event_session_id` but not `event_location_id`.
When navigating from the Presenter View to the Launcher, the frontend has to do a secondary
session lookup to discover the location (magic redirect in launcher base `+page.svelte`).
Joining `event_session.event_location_id` into the presenter view/response would let the
frontend pass the location directly in the Launcher URL without the extra lookup.
- [x] Backend: added `event_location_id` (and `event_location_id_random`) to the `event_presenter` view or API response (2026-04-09)
- [x] Frontend: updated `ae_EventPresenter` type and `properties_to_save`; now pass as `events__launcher_id` in `presenter_page_menu.svelte` (2026-04-09)
Reads Stripe config from `$ae_loc.site_cfg_json` (`stripe_publishable_key`, `stripe_btn_1/3/6/10_license`).
License tier selector (1/3/6/10 users) with `{#key}` remount pattern for Stripe web component.
3 states: paid confirmation (priority=true), admin setup hint / "contact organizer" (no Stripe config),
payment form. `client_reference_id=exhibit_id`. TypeScript declaration in `app.d.ts`.
Stripe keys verified visible in `$ae_loc.site_cfg_json` on dev/demo site. Keys need validity check in Stripe dashboard.
- [x]**End-to-end smoke test (canceled by client)** — sign in with shared passcode, scan/search a badge, add a lead, view detail, add notes/responses, export CSV; canceled 2026-04-09.
singleton captures `beforeinstallprompt` (Chrome/Android/desktop) and detects iOS Safari
for manual "Share → Add to Home Screen" instructions. Reusable `element_pwa_install_prompt.svelte`
placed on the Leads Start tab between the feature grid and sign-in. `pwa_install.init()` wired
into root `+layout.svelte`; dismiss persists 7 days via localStorage. svelte-check: 0 errors.
### [DevOps] Remaining deployment items
- [x]**Wire AE_APP_REPLICAS:**`docker-compose.yml` line 147 already has `scale: ${AE_APP_REPLICAS:-1}`. (verified 2026-03-11)
- [x]**Archive ae_env_node_app:** Archived as tar.gz under `~/OSIT_dev/backups/`; old history/docs moved to `~/OSIT_dev/for_reference_only/`. (2026-03-11)
- [x]**Build Optimization:** Current state finalized. Local Gitea instance stood up at `git.dgrzone.com` (Docker, home server) — future: migrate repos from Bitbucket, verify Backblaze/restic backups cover Gitea data. (2026-03-11)
- [x]**Remote deploy script:**`aether_container_env/deploy.sh` — SSH-triggered from workstation via `npm run deploy:remote:test/prod`. Handles git pull (ff-only) + docker build + restart. Tested and working on test env. (2026-03-25)
- [x]**`.env.default` cleanup:** Removed 16 dead variables, added missing `AE_NETWORK_NAME`/`CONTAINER_DOZZLE`/`AE_DOZZLE_PORT`, parameterized all container names (`CONTAINER_MARIADB`, `CONTAINER_PMA`, `CONTAINER_AE_OPS`) with `:-default` fallbacks in compose. ("Dozzle" = log viewer container.) (2026-03-26)
- [x]**Prod deploy:** Run `npm run deploy:remote:prod` (off-peak). Prerequisites: both repos pushed to Bitbucket ✓; verify `.env.prod` exists in `/srv/apps/prod_aether_app_sveltekit/` on Linode before running. (2026-03-30)
- [x]**Bitbucket → SSH migration:** Switched all three repos (`aether_app_sveltekit`, `aether_container_env`, `aether_api_fastapi`) to SSH remotes (`git@bitbucket.org`) on workstation. App passwords deprecated — SSH unaffected. (2026-03-27)
- [ ]**Branch strategy cleanup:** All environments (test, prod, bak) currently pull from same branches. `deploy.sh` defaults are `ae_app_3x_llm` / `development` — acceptable for now but should establish proper branch separation (e.g. `main`/`master` for prod).
- [ ]**Tier 2 deploy (Gitea webhook):** Push-triggered deploys via Gitea webhook → listener on Linode → `deploy.sh`. Deferred until Gitea usage is more established.
### [General]
- [x]**Temp Cleanup:**`cleanup_tmp_files` wired in `launcher_background_sync.svelte`; called at launcher startup. Confirmed working. (2026-03-11)
- [x]**`window.print()` for badge print button:** Wired in `ae_comp__badge_print_controls.svelte` — increments count, fires `window.print()`, redirects to badge search. (done)
- **Input Field Audit:** Several input fields are missing `name`/`id` attributes or `data-testid`. Known examples: badge override fields in `ae_comp__badge_obj_view.svelte`; template name input in `ae_comp__badge_template_form.svelte`. Matters for: accessibility, autofill, label associations, and test targeting. (For tests, use `getByLabel()` rather than `input[value*=...]` which only checks the HTML attribute, not the Svelte-bound DOM property.)
## ✅ Completed (2026-03)
## ✅ Completed (archived)
See the full completed history in [documentation/TODO__Agents__ARCHIVE_2026-03.md](documentation/TODO__Agents__ARCHIVE_2026-03.md).
- [x]**[Journals] Summary AI shortcut** — added Quick Actions button in entry config modal and wired it to close modal + scroll to AI tools panel in entry edit view. (2026-06-02)
### [Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`
- [x]**[Cleanup] Migrate remaining `lucide-svelte` imports to `@lucide/svelte`**
Migrated all 5 listed files to `@lucide/svelte` and uninstalled `lucide-svelte` from dependencies. (2026-06-02)
---
### [Pres Mgmt] Sessions hide/show toggle
- [x]**[Pres Mgmt] Hidden sessions blink on initial load** — SCENARIO 2 fallback in
`pres_mgmt/+page.svelte` now captures `qry_hidden` as a `$derived.by` dependency and
applies the filter in the fallback path. No blink on page load. (2026-05-28)
- [x]**[Pres Mgmt] API call uses live store instead of snapshot** — changed
`pres_mgmt_loc.current.qry_hidden` → `params.qry_hidden` in `handle_search_refresh`
API call to be consistent with fast path snapshot. (2026-05-28)
- **Note:** `hide_event_launcher` is still active — used in `menu_session_list.svelte`
(Launcher) to CSS-hide sessions from the list. Button to toggle it is in
`session_page_menu.svelte`. Not used in Pres Mgmt (intentional — Pres Mgmt always shows all).
- **Note:** Non-trusted users always have `!item.hide` applied at the component level
in `ae_comp__event_session_obj_li.svelte` regardless of `qry_hidden`. Toggle is
trusted-access-only in practice; direct session links still work for non-trusted users.
letresponse_data=awaitaxios.delete(endpoint,{'data':data})// not just data?
.then(function(response){
console.log(response.data);
returnresponse.data;
})
.catch(function(error){
console.log(error);
returnerror;
});
returnresponse_data;
}
exportdefaultdelete_object;
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.