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
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.
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.
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
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
- 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
- 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)
- 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
- 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>
- 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
- 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>
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).
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.
- 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
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.
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.
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>