160 Commits

Author SHA1 Message Date
Scott Idem
03b3c84921 docs: archive Field Editor V3 project (complete), update Pres Mgmt status (~70% with regressions) 2026-06-12 17:54:38 -04:00
Scott Idem
e89c982022 docs: archive V3 CRUD upgrade project (100% complete) 2026-06-12 17:46:18 -04:00
Scott Idem
c6ef729c55 docs: audit and archive completed Journals and Badges projects 2026-06-12 17:35:02 -04:00
Scott Idem
fd7ccd7ecc docs: normalize Pres Mgmt cleanup formatting 2026-06-12 17:10:31 -04:00
Scott Idem
7831179970 docs: eighth-pass classify remaining active references 2026-06-12 17:07:57 -04:00
Scott Idem
75e7ca541a docs: seventh-pass archive unsafe legacy references 2026-06-12 17:07:03 -04:00
Scott Idem
e6fb4b289f docs: sixth-pass project routing and link validation 2026-06-12 17:05:13 -04:00
Scott Idem
1e3f541a39 docs: fifth-pass guide metadata and doc ownership cues 2026-06-12 16:58:23 -04:00
Scott Idem
e966261324 docs: fourth-pass metadata normalization and archive linking 2026-06-12 16:54:34 -04:00
Scott Idem
67d2607da1 docs: third-pass overlap consolidation and guide scoping 2026-06-12 16:53:35 -04:00
Scott Idem
7192cbc0af docs: second-pass archive and naming normalization 2026-06-12 16:50:56 -04:00
Scott Idem
a227c6aaa7 docs: restructure bootstrap, add module references, and normalize docs 2026-06-12 16:48:35 -04:00
Scott Idem
e05602b87b fix(pres_mgmt): restore POC + Presenter sign-in email and copy link flows
POC (session) sign-in was silently broken:
- sign_in_out.svelte calls session_sign_in() only when session_id is a query
  param; the URL was missing &session_id=... so the function never fired.
- Fixed in: email_sign_in__event_session URL and poc_sign_in_url derivation
  in session_view.svelte.

Presenter email routed to wrong page:
- email_sign_in__event_presenter built a URL to /presenter/[id] which has no
  sign-in handler. Changed to route to /session/[session_id] (same as the
  existing copy-link on the presenter page), including presenter_id and
  presentation_id params so presenter_sign_in() fires correctly.

Multi-session/multi-presentation person-centric auth:
- poc_is_authed in session_view.svelte now also checks
  auth__person.id === poc_person_id, so a POC signed in via any of their
  session links is automatically auth'd on all sessions where they are the POC.
- presenter_is_authed derived bool added to presenter page; includes person_id
  match so a presenter is recognised on all their presentations after one
  sign-in. All file upload and file list auth gates now use it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 16:27:45 -04:00
Scott Idem
10f7f04fbc docs: fix bootstrap — remove JWT Bearer claim; add mistakes 16-17; add pres_mgmt reading entry
- Auth row in stack table wrongly said "JWT Bearer is auto-injected" — this project
  uses custom headers only (x-aether-api-key + x-account-id, NOT Bearer tokens)
- Mistake #16: $ sigil on plain prop value → store_invalid_shape at runtime
- Mistake #17: *_json / *_kv_json columns start as null — require ?. and ?? {} guards
- Renumbered old #16 (service worker) to #18
- Reading order: added Pres Mgmt module doc pointer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 16:19:52 -04:00
Scott Idem
94bdeb9a26 fix(pres_mgmt): guard poc_kv_json null access in POC profile and agree modals
poc_kv_json is null when a session has never had POC data saved. Any bracket
access on it crashed immediately on modal open. Fix:
- Reads: use ?.[poc_type] optional chaining everywhere (checkboxes, biography_updated_on)
- Writes: initialize from a shallow copy of poc_kv_json ?? {} before indexing into it,
  so the first save on a fresh session creates the structure rather than throwing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:55:25 -04:00
Scott Idem
080ad06a45 fix(pres_mgmt): stop treating lq__event_session_obj prop as a Svelte store in POC modals
Both ae_comp__event_session_poc_profile and ae_comp__event_session_poc_form_agree used
$lq__event_session_obj with the $ sigil throughout, expecting a store subscription. They
receive a plain resolved value from session_view.svelte, so Svelte threw store_invalid_shape
on mount. Replace all $lq__event_session_obj → lq__event_session_obj in both files.

Also clean up poc_profile: remove event_session_id_random (legacy alias) → event_session_id
in the auth check and the biography save call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:53:13 -04:00
Scott Idem
45f8bb5e58 fix(pres_mgmt): restore session view — name always visible, POC in hero card, email/copy links
- session_view.svelte: session name/code were only rendered in edit_mode — non-editors
  saw a blank card. Now always visible; edit_mode just wraps them in field editors.
- Restructured hero card as a <ul> with datetime, room, and POC as rows inside the card.
  POC no longer floats below as a disconnected block.
- Dynamic POC label (label__session_poc_name) used throughout: row label, modal titles,
  fallback text, and editor label — no more hardcoded "Host:".
- POC "Select Person" flow: gate select editor on person_options_loaded to prevent empty
  dropdown on open; button reads "Reload Person" after list is loaded.
- Restored email sign-in link button in POC row with idle/sending/sent/error feedback.
  Shown when require__session_agree && show__email_access_link && poc_person_primary_email.
- Restored inline copy-access-link for trusted staff (show__copy_access_link).
- session_page_menu.svelte: fix event_session_id prop — was passing event_id instead of
  event_session_id, breaking the Sign_in_out auth grant.
- ae_comp__event_session_poc_profile.svelte: migrate run() to $effect, fix poc_person_id_random
  → poc_person_id, fix events_slct reference in copy link URL.
- +page.svelte: add pres_mgmt config sync so session pages opened directly by URL get
  correct hide__session_poc and other remote config values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:15:58 -04:00
Scott Idem
3085d1dc63 docs: update IDAA idaa_loc migration project doc
Rewrote PROJECT__IDAA_Stores_Svelte5_Migration_2026.md with complete
detail: full 29-file consumer inventory with hit counts per module,
exact files that only reference the localStorage key string (no changes
needed), store_versions.ts wipe note, explicit Phase 1 ordering, test
seed guidance, and updated Risk Register including R3 nested-object
merge gap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 17:33:58 -04:00
Scott Idem
7fc073053b feat(idaa): add PersistedState store for idaa_loc (not yet wired in)
Creates ae_idaa_stores__idaa_loc.svelte.ts — the Svelte 5 PersistedState
replacement for the idaa_loc persisted() store in ae_idaa_stores.ts.

Mirrors the exact shape of idaa_local_data_struct with full TypeScript
interfaces (IdaaArchivesLoc, IdaaBbLoc, IdaaRecoveryMeetingsLoc,
IdaaLocState). Drops __version, name, and title (not runtime state).

Same localStorage key (ae_idaa_loc) — existing data loads cleanly.
Not wired into consumers yet; pending review before migration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 17:22:54 -04:00
Scott Idem
582b43da34 fix(types): add file_display_overrides + launch_profiles to LauncherLocState
Both fields were being written to launcher_loc.current dynamically and
read back via 'as Record<string, unknown>' casts because they were absent
from the LauncherLocState interface and launcher_loc_defaults.

file_display_overrides: Record<string, 'extend' | 'mirror' | 'none'>
  — per-device display override map keyed by event_file_id.
  — local-only workaround until event_file.cfg_json is added on backend.
  — TODO comment preserved; migrate once the backend column exists.

launch_profiles: Record<string, Partial<LaunchProfile>> | null
  — local per-device launch profile overrides (device API config takes
    priority; these override the built-in DEFAULT_LAUNCH_PROFILES).

Defaults added to launcher_loc_defaults ({} and null respectively).
All 3 type casts in launcher_file_cont.svelte removed.
2026-06-11 17:17:29 -04:00
Scott Idem
98e31f1528 docs: update docs to reflect events store migration completion
BOOTSTRAP__AI_Agent_Quickstart.md: rewrite store reactivity trap section
to distinguish events sub-stores (PersistedState, fine-grained) from
ae_loc/idaa_loc (svelte-persisted-store, coarse). Add import/read/write
syntax examples and pointer to migration doc.

PROJECT__Stores_Svelte5_Migration.md: rewritten to reflect events module
fully complete; documents established PersistedState pattern with canonical
examples; tables show all 5 sub-stores done + events_loc retired.

TODO__Agents.md: events migration marked complete (2026-06-11); idaa_loc
and ae_loc listed as remaining work; stale events_loc file_display_overrides
ref fixed.

tests/README.md: replace Leads-only store migration note with full events
sub-store table, localStorage keys, and explanation of PersistedState
deserialization (no __version guard needed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:50:06 -04:00
Scott Idem
573d20e574 feat(stores): retire events_loc — Svelte 4 persisted store fully replaced
events_loc has been completely removed after migrating all consumers:

- EVENTS_MODULE_TITLE constant replaces $events_loc.title (was always
  the static default 'OSIT\'s Æ Events', never written dynamically)
- events/+layout.svelte: qry__* writes moved to events_sess; stale-deploy
  ver check block removed (store_versions.ts handles this already)
- 3 stale pres_mgmt stragglers fixed: device_obj_li, location_page_menu,
  event_reports_page_menu now use pres_mgmt_loc.current directly
- testing/+page.svelte: missed launcher ref fixed (launcher_loc.current)
- events_loc export, events_local_data_struct, persisted import, and
  AE_EVENTS_LOC_VERSION import all removed from ae_events_stores.ts
- events_loc import cleaned from all consumer files

events_sess (in-memory writable) stays in ae_events_stores.ts unchanged.
store_versions.ts keeps its _check_and_wipe('ae_events_loc') entry to
clean lingering old data from users' browsers.

svelte-check: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:41:06 -04:00
Scott Idem
83c9b9fd4f feat(stores): promote events auth state to Svelte 5 PersistedState
Creates ae_events_stores__auth.svelte.ts with PersistedState keyed
'ae_events_auth_loc' for auth__person and auth__kv (presenter/session
sign-in state). Migrated 10 component files from $events_loc.auth__* to
events_auth_loc.current.auth__*.

Also fixed stale pres_mgmt stragglers: $events_loc.pres_mgmt.* refs in
presenter_obj_li.svelte, presenter_page_menu.svelte, and [presenter_id]/
+page.svelte now use pres_mgmt_loc.current.* directly.

show_details boolean moved from events_loc to leads_loc (it belongs in
the leads module — one bind in ae_tab__manage.svelte).

auth__person, auth__kv, show_details, events_cfg_json, event_id removed
from events_local_data_struct. events_loc now only carries ver, title,
and qry__* prefs.

svelte-check: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:19:49 -04:00
Scott Idem
27c775d816 feat(stores): promote launcher_loc to Svelte 5 PersistedState
Creates ae_events_stores__launcher.svelte.ts with PersistedState keyed
'ae_launcher_loc', following the same pattern as badges, leads, and
pres_mgmt. All 28 launcher component files migrated from
$events_loc.launcher.* to launcher_loc.current.*.

events_local_data_struct in ae_events_stores.ts now carries no sub-module
objects — all four sub-modules (badges, launcher, leads, pres_mgmt) are
authoritative in their own stores. Session state (events_sess.launcher)
is unchanged.

svelte-check: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:00:40 -04:00
Scott Idem
5823f18161 chore(stores): remove migrated sub-objects from events_loc persisted struct
badges, leads, and pres_mgmt have each been promoted to their own
PersistedState stores (ae_badges_loc, ae_leads_loc, ae_pres_mgmt_loc).
Remove them from events_local_data_struct and drop the now-unused
*_loc_defaults imports so events_loc only carries what it owns.

launcher stays — not yet migrated (244 active refs).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 15:41:54 -04:00
Scott Idem
94e4fad061 chore(stores): remove unused default properties from ae_loc, ae_sess, events_slct, idaa_loc
Audited all three legacy persisted-store files for properties with zero
references in src/. Removed ~119 lines of dead defaults:

- ae_loc: qr_scanner_version, ds, entire mod block (archives/events/journals/posts/sponsorships)
- ae_sess: hub, mod block (archives/events/journals/posts/sponsorships/testing), download
- events_loc: ds
- events_sess: ds_loaded
- events_slct: abstract_*, badge_template_*, device_*, exhibit_tracking_*, lq__presenter_obj
- idaa_loc: ds, idaa_cfg_json, top-level qry__* (each submodule has its own), novi_rate_limited_until, novi_*_base_url (read from site_cfg_json, not idaa_loc)

No version bumps needed — removing fields from defaults is backwards-compatible.
svelte-check passes: 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 15:29:48 -04:00
Scott Idem
9a1ba02b59 Can not use these links within an iframe... 2026-06-11 15:08:50 -04:00
Scott Idem
05841350fe feat(idaa): add /idaa/clear-caches page for Novi iframe cache reset
Clears all IDB databases, localStorage, and sessionStorage for the
prod-idaa.oneskyit.com origin when loaded as an iframe inside www.idaa.org.
Targets the partitioned storage bucket used by IDAA Novi iframes — direct
navigation to the site clears a different partition and has no effect.

Uses Novi-safe styling (explicit bg/text surfaces, no bare h1 elements,
inline styles on links) to survive Bootstrap v3 CSS injected by Novi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:01:34 -04:00
Scott Idem
a5beff4aa8 feat(reports): add created_on timestamps and detail links to file downloads report
Show upload timestamp for every file; bold the newest upload per session/presenter
group so staff can quickly identify the most recent version. Add Session/Presenter
navigation links in each card header for direct access without searching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 04:23:07 -04:00
Scott Idem
246d4f8ef3 fix(pres_mgmt): show session/presenter counts in File Downloads header
The summary was showing file counts by type (session-level files vs
presenter-level files), which made the session count look wrong (e.g.
6 when there are 40 sessions). Now shows unique session and presenter
counts from the grouped data, matching what the label implies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 17:30:19 -04:00
Scott Idem
666b54bd36 fix(badges): add missing updated_on column to CSV export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:36:25 -04:00
Scott Idem
89c1decf8d feat(badges): add CSV export to badge reports
Adds an "Export CSV" report to the badge reports page. Generates
a clean CSV client-side from Dexie cache — no backend call needed.

- 3 filter presets: Printed+Clean (default), Printed all, All badges
- Printed+Clean mirrors the manually-cleaned Axonius export (printed,
  non-hidden, non-test badge type)
- Timezone selector: Eastern (default), Local, UTC — addresses the
  UTC→Eastern conversion needed for post-event client exports
- 24 columns: identity fields, override pairs, print status, created_on
- UTF-8 BOM for direct Excel open without import wizard
- Auto-generated filename from event name + date + filter suffix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:00:18 -04:00
Scott Idem
b9d70b616f feat(reports): add two new filename format options to file downloads report
Adds 'Session Code + Date + Time + Name' and 'Session Code + Date +
Session Time + Presentation Time + Presenter Full Name' format presets.
The second format uses presentation start datetime for the pres_time
part and event_presenter_full_name for the full presenter name part.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:18:39 -04:00
Scott Idem
e8a49562a9 feat(pres_mgmt): prefix/suffix inputs, flex row layout, strip :443 from links
File Downloads report: add Prefix and Suffix inputs for filename customization
(e.g. "2026_06__"), longest-filename length indicator with warning above 120
chars, and switch file rows from table to flex layout so Download/Copy Link
buttons stay right-aligned regardless of filename length.

Strip redundant :443 from https download URLs in both the file downloads report
and the manage event file list clipboard links.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:15:16 -04:00
Scott Idem
e909c34874 Updated documentation. 2026-06-10 14:55:10 -04:00
Scott Idem
48bc52899f fix(pres_mgmt): switch direct download links to event_file endpoint
event_file ?key= auth fix is now deployed. Retire the hosted_file workaround
and use /v3/action/event_file/{event_file_id}/download?key=... as the canonical
form across the file downloads report, manage file list, and download button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 14:41:53 -04:00
Scott Idem
6b122a065e feat(pres_mgmt): File Downloads report with clean filename presets
New report at Pres Mgmt > Reports > File Downloads. Groups all event files
by session and presenter, with 10 filename format presets (original, session
code/date/name combos, presenter variants). Per-file Download button and Copy
Link button using hosted_file endpoint for ?key= auth support.

Also fixes direct download links in element_manage_event_file_li and the
hosted_files download button — event_file endpoint does not yet propagate
?key= auth internally, so all direct links now use hosted_file endpoint
which supports it today.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 14:19:25 -04:00
Scott Idem
1b81b8873c refactor(badges): move reports IDB prefetch to +page.ts load function
Replace the $effect-based background fetch with the canonical
if(browser) fire-and-forget pattern in +page.ts. Runs earlier
in the lifecycle, no store subscription overhead, and immune to
$ae_api store re-trigger side effects. Removes ae_api and
events_func imports from the component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:36:31 -04:00
Scott Idem
4f74cf1353 fix(badges): reports background fetch guard used wrong field name
$ae_api.api_key does not exist — the field is api_secret_key.
The guard evaluated to true on every render, returning immediately
and never calling search__event_badge. Changed to base_url which
is the standard readiness check per the quickstart doc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:32:49 -04:00
Scott Idem
6dc6be9926 fix(badges): reports background fetch was not writing to IDB
try_cache=false skips db_save_ae_obj_li__ae_obj, so the API fetch
completed but liveQuery never saw the data. Removing the override
lets it default to true so results are persisted to IDB as expected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:27:57 -04:00
Scott Idem
97c4c1cd6b fix(badges): Remote First uncheck stuck visually checked
e.preventDefault() was called for both enable and disable clicks.
On disable, it reverted the DOM back to checked before Svelte could
sync the store update, leaving it visually stuck. Only prevent default
when enabling (to hold the unchecked state during confirmation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:15:39 -04:00
Scott Idem
b6481a3507 feat(badges): Remote First confirmation warning + title tooltips
Enabling Remote First now shows an inline warning explaining that it
bypasses the local cache and queries the server on every keystroke.
User must confirm before it activates; Cancel leaves the checkbox off.
Disabling still takes effect immediately. Label and checkbox also get
title tooltips. Active state gets a warning tonal highlight.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:12:28 -04:00
Scott Idem
7b45b548e4 fix(badges): reports page fetches all badges on load instead of IDB-only
Reports were IDB-read-only — no data appeared until the badge search page
had already populated the cache. Added a background search__event_badge
call (limit 5000, try_cache=false) so navigating directly to /reports
always gets a fresh full dataset; liveQuery updates reactively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:06:13 -04:00
Scott Idem
a1057fd776 fix(badges): throughput — weekday-first date header, keyed each block
Separators now read "Tuesday · June 9 — EDT" so day-of-week is the
lead identifier, matching how conference staff refer to the schedule.
Added key (sz) to the window-size each block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 11:28:48 -04:00
Scott Idem
d0286f7868 feat(badges): show full date + browser timezone in throughput separators
Date separators now display "Monday, June 9 — EDT" instead of "Jun 9",
using the browser's local timezone abbreviation resolved at page load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 11:24:25 -04:00
Scott Idem
1c541cd090 fix(badges): print throughput report — descending sort + UTC timezone fix
Buckets now display newest-first. Naive UTC datetime strings from the
backend are normalized with a Z suffix before parsing so times display
in local browser timezone, matching the badge list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 11:16:14 -04:00
Scott Idem
868b4017f2 feat(badges): allow trusted staff to edit name without enabling edit mode
Trusted users can already edit title/affiliations in regular mode via the
auth_editable list. Name was locked to trusted+edit_mode only. Staff at the
badge table frequently need to fix a misspelled name mid-queue without
toggling the full edit mode for the whole app.

Added a targeted guard in field_editable(): is_trusted && field === 'name'
passes before the auth_editable list check, so trusted users get the edit
pencil for name in normal mode. Regular authenticated users (attendees)
are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 09:38:48 -04:00
Scott Idem
3122725610 fix(badges): apply print_count-first sort to all three badge display paths
The default sort (unprinted first, then name) was only wired into the API
order_by_li. Two IDB-local paths were left behind:

1. SCENARIO 2 fallback (no active filters, no text query): used Dexie
   .sortBy('given_name') — bypassed entirely on initial page load.
   Fixed: fetch .toArray() then JS-sort by print_count ASC → given_name ASC.

2. Fast-path IDB sort default case: also sorted by given_name only, causing
   a visible flash of name-ordered results before the API response landed.
   Fixed: same print_count ASC → given_name ASC comparator.

All three paths (API, fast-path IDB, fallback IDB) now agree on sort order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 09:13:47 -04:00
Scott Idem
6e04145514 fix(badges): display print timestamps in local browser time instead of UTC
The backend returns naive UTC datetime strings (no Z suffix). dayjs was treating
them as local time, so staff in ET saw UTC times. Added treat_as_utc param to
iso_datetime_formatter (default false, zero breakage to existing call sites) that
appends Z before parsing so dayjs converts to browser-local time on display.

Applied to all print timestamp call sites in badge list (first/last print visible
row + debug row) and to created_on/updated_on in the debug row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 07:56:46 -04:00
Scott Idem
b8ceed69d0 fix(badges): default sort puts unprinted badges first (print_count ASC, then name ASC)
Previously the default sort was name-only, mixing printed and unprinted badges
together. Staff at registration need unprinted badges at the top so they can
immediately see who still needs a badge without filtering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 07:47:40 -04:00
Scott Idem
71aacb6346 fix(badges): sync events_slct.event_id from URL param on direct navigation
The badges page relied on events_slct.event_id being set by ae_acct.slct.event_id
(populated when the user clicks an event in the events list). Direct URL navigation
— bookmark, shared link, browser history — skipped that code path, leaving
events_slct.event_id null. Any call using that value (upload form, create form)
would fire with an empty event_id and get a 404.

Reads page.params.event_id on mount, same pattern used by the launcher and
session pages, so all entry paths are reliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 07:18:05 -04:00
Scott Idem
04f3b82d59 fix(stores): merge defaults on PersistedState deserialize so new fields get defaults
runed PersistedState hydrates raw stored JSON without merging defaults, so any
field added after a user's first session is undefined rather than its default value.
Apply a shallow spread (defaults → stored) in the custom serializer for badges,
leads, and pres_mgmt stores so new fields always fall back to defaults for
existing sessions — without disrupting any previously saved preferences.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:47:25 -04:00
Scott Idem
cc04411d23 feat(badges): goto() after print with per-device fallback toggle
Switch post-print navigation from window.location.href to goto() for
faster SvelteKit client-side transition back to Badge Search (no full
reload). A nav_to_badges() helper in print controls branches on
badges_loc.print_nav_use_goto (default true).

Badges Config page gains a "Local Device Settings" section with a
checkbox to disable goto() and fall back to hard reload — stored in
localStorage per browser, not synced to the event. Useful as an
on-site escape hatch without a code deploy.

Also fixes the Templates button on the config page: adds the standard
border border-surface-300-700 so it looks like a button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:28:33 -04:00
Scott Idem
55f3e3a5a4 feat(badges): add Hole Marker Inset control to badge template form
Exposes punch_holes.inset_x_mm in the badge template config UI under
the Punch-Out Hole Markers section. Visible whenever at least one slot
is enabled. Number input (0–8, step 0.5) with a Reset button that
clears back to the 2mm default. Saved to cfg_json on submit; parsed
back on load; omitted from the payload when left at default (null).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:13:07 -04:00
Scott Idem
955d28d9c5 fix(badges): shrink punch-hole markers horizontally, add inset_x_mm config
Default horizontal inset increased from 1mm to 2mm per side (width shrinks
by 4mm total) so markers stay inside the physical hole slot on all printers
and badge stock with minor registration variance. Vertical stays at 1mm.

The inset is now driven by punch_holes_inset_x (template_cfg.punch_holes
.inset_x_mm, default 2) so it can be tuned per template without a code
change. Added inset_x_mm field to AeBadgeTemplateCfg type with doc comment.

All 9 slot marker divs (6 rainbow + 3 solid) updated via replace_all.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:07:59 -04:00
Scott Idem
7c5cf53106 feat(badges): extend search result limit to 500 for trusted staff
Trusted users can now step up to 500 results (300 → 400 → 500) via the
existing ± stepper. Manager tier continues to 2500 as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:52:19 -04:00
Scott Idem
24b52b8027 style(badges): reports — consistent buttons, dark mode, 500-row cap
- Report selector now uses a tonal-primary container matching the pres_mgmt
  reports pattern, making Long Names / Print Throughput clearly clickable
- All hardcoded gray-* colors replaced with surface-*-* tokens and
  dark: variants for proper light/dark mode support
- Control buttons (field selector, threshold, window size) use btn-sm with
  visible borders matching the rest of the events module
- Long Names table uses surface tokens on rows, header, borders
- Print Throughput bar rows use surface tokens; expanded badge chips link
  to /print instead of /review
- Long Names caps display at 500 rows with a warning note when hit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:51:31 -04:00
Scott Idem
6b3fb36926 fix(badges): long names report edit button links to print page with icon
Changed href from /review to /print, added UserRoundPen icon, and
extended the title to include badge ID and name for hover context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:43:23 -04:00
Scott Idem
5cad150b0a fix(badges): move Reports link to trusted+edit toolbar, add reprint count
Reports link moved out of the enable_mass_print gate into the main
trusted+edit-mode toolbar, to the right of Templates — visible whenever
staff are in edit mode regardless of mass-print config.

Reports page header now shows reprint count in parentheses when > 0,
e.g. "142 badges · 98 printed (7 reprints)".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:38:25 -04:00
Scott Idem
8a41f02f0d feat(badges): add Long Names and Print Throughput reports
Two IDB-backed reports under /badges/reports:
- Long Names: filter badges by given/family/full name length (threshold
  adjustable), colour-coded by severity, links to review page
- Print Throughput: bucket print_last_datetime into 5/15/30/60-min
  windows with a horizontal bar chart and expandable badge name list

Also adds a "Badge Reports" nav link on the badges main page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:32:53 -04:00
Scott Idem
88ab5b27d4 fix(badges): skip PATCH and print immediately when offline or API unreachable
When navigator.onLine is false or the account is ghost (API unreachable),
bypass the 20-second API timeout entirely and fire window.print() at once.
The existing error state ("Printed — count NOT saved") already covers this
case. Staff can correct the print count manually after connectivity returns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:19:27 -04:00
Scott Idem
b623557795 feat(layout): collapsible offline/API-error banner with improved messages
Replace the static full-width offline banner with a two-state toggle:
expanded banner (default) with a collapse button, and a small chip in
the top-right corner when collapsed. Adds a subtitle line explaining
what still works offline vs. what requires network. Changes "No API"
chip label to "API Error" and "Offline" title to "Device Offline".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:19:22 -04:00
Scott Idem
7d2b30b7ce fix(badges): open badge review access when no passcode is set
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>
2026-06-06 18:35:47 -04:00
Scott Idem
c9b0acfa06 feat(badges): two-step confirm before sending email review link
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>
2026-06-06 18:35:40 -04:00
Scott Idem
29a24812f4 fix(badges): perceptually uniform rainbow via OKLCH + @property
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>
2026-06-04 20:47:43 -04:00
Scott Idem
70fda25c95 feat(badges): slow pulse mode for rainbow punch hole markers
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>
2026-06-04 20:42:23 -04:00
Scott Idem
8f815b7033 fix(badges): tri-phase rainbow cycling for punch hole markers
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>
2026-06-04 20:35:31 -04:00
Scott Idem
ba4a0dc828 feat(badges): static RGB gradient for punch holes on print, cycling on screen
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>
2026-06-04 20:28:56 -04:00
Scott Idem
35c1324824 feat(badges): rainbow animation option for punch-out hole markers
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>
2026-06-04 20:23:37 -04:00
Scott Idem
1a53a20995 feat(badges): per-side header padding configurable via template cfg_json
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>
2026-06-04 20:17:04 -04:00
Scott Idem
04c2042060 feat(badges): per-slot fg/bg colors for punch-out hole markers
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>
2026-06-04 20:01:03 -04:00
Scott Idem
4831f4b81b fix(badges): punch hole markers — z-index above header + 1mm safety inset
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>
2026-06-04 19:56:29 -04:00
Scott Idem
7bf76bf766 feat(badges): configurable punch-out hole markers for badge clip slots
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>
2026-06-04 19:52:59 -04:00
Scott Idem
3ae5b30c37 docs(badges): update badges and badge template documentation
MODULE__AE_Events_Badges: update Search & Filter section — visibility
select (3 options, manager-gated), result limit stepper table, badge
type filter hardcoding noted as known gap.

MODULE__AE_Events_Badge_Templates: add full cfg_json reference section
covering all keys (visibility, QR, alignment, header image, appearance,
fit_heights, controls_cfg). Update TODO — duplex/form/header cfg done;
badge_type_list→search wired as open item.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:46:59 -04:00
Scott Idem
b04202ecec feat(badges): configurable header bottom border and padding per template
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>
2026-06-04 19:40:18 -04:00
Scott Idem
84c4a2aa43 fix(badges): correct templates link URL — /events/[id]/templates not /badges/templates
Route group (badges) is parenthetical and doesn't contribute a URL segment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:05:08 -04:00
Scott Idem
399f98ce8e feat(badges): add Templates link on badge search page for manager access
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>
2026-06-04 19:03:11 -04:00
Scott Idem
f5ccd2e3cf feat(badges): configurable header image vertical offset per template
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>
2026-06-04 19:00:21 -04:00
Scott Idem
94a3cb0644 This is just a version bump because things overall seem to be in a very good state. 2026-06-04 18:40:35 -04:00
Scott Idem
9d904446d4 feat(badges): search filter polish and result limit stepper
- 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>
2026-06-04 18:33:59 -04:00
Scott Idem
b45a27481a Forgot to switch this back to not equals for the last printed timestamp. 2026-06-04 17:08:20 -04:00
Scott Idem
26ab5dda75 fix(service-worker): exclude /fonts/ from SW precache
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>
2026-06-03 20:00:22 -04:00
Scott Idem
0511d9591f docs: document service worker fix, events sort encoding, and slct account_id pattern
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>
2026-06-03 18:39:18 -04:00
Scott Idem
59fc7cabc6 fix(service-worker): add skipWaiting + clients.claim for immediate activation
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>
2026-06-03 18:35:34 -04:00
Scott Idem
41b352bc0a Minor style change 2026-06-03 17:57:15 -04:00
Scott Idem
a4fed750fa fix(idaa-recovery-meetings): fix priority sort, stale account_id gate, and cache clear
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>
2026-06-03 17:54:05 -04:00
Scott Idem
74e0f752a6 More fine tuning of the look and feel. 2026-06-02 20:14:08 -04:00
Scott Idem
be3e56eece fix(badges): cap controls panel width and center badge+controls pair
- 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>
2026-06-02 19:44:16 -04:00
Scott Idem
e2d3c5a822 refactor(badges): replace fixed controls overlay with in-flow flex layout
- 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>
2026-06-02 19:40:26 -04:00
Scott Idem
1b6aeb5b02 fix(badges): polish print page chrome; fix header overlap with controls panel
- 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>
2026-06-02 19:11:37 -04:00
Scott Idem
04018a27ed Fine tuning the styles 2026-06-02 18:14:55 -04:00
Scott Idem
4292aebc56 Fine tuning the layout of things. 2026-06-02 17:47:27 -04:00
Scott Idem
3466d6552c feat(badges): show print status strip for trusted staff on printed badges
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>
2026-06-02 17:27:40 -04:00
Scott Idem
b7969bc46e Making subtle changes based on actual usage 2026-06-02 17:05:34 -04:00
Scott Idem
99b8eb0b5e fix(badges): focus search input when Search is triggered with too-short query
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>
2026-06-02 16:55:38 -04:00
Scott Idem
9e361eae9b fix(badges): improve empty-state search hint to reflect actual search fields
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>
2026-06-02 16:52:35 -04:00
Scott Idem
e735d0c213 refactor(badges): remove redundant loading gate; fix BarChart2 deprecation
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>
2026-06-02 16:48:44 -04:00
Scott Idem
d05cc63459 fix(badges): remove transition from initial loader to fix double-DOM bounce
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>
2026-06-02 16:44:35 -04:00
Scott Idem
ac17417f3c fix(badges): fix search-result layout shift + unify empty/loading states
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>
2026-06-02 16:38:30 -04:00
Scott Idem
3773758eb5 feat(badges): smooth transitions + polish for badge search UI
- 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>
2026-06-02 16:25:31 -04:00
Scott Idem
60bdd2fdba feat(badges): public_access kiosk mode + manager access improvements
- 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>
2026-06-02 16:15:08 -04:00
Scott Idem
72c8f9b502 docs(todo): archive June snapshot and streamline active task list 2026-06-02 15:00:24 -04:00
Scott Idem
1de87b6c5f chore(cleanup): add journal AI shortcut and align posts tmp_sort 2026-06-02 14:43:16 -04:00
Scott Idem
84a9d0fffc chore(core): remove retired site_domain helper and update docs 2026-06-02 14:34:19 -04:00
Scott Idem
87084f0f71 chore: migrate lucide package and close quick TODO cleanups 2026-06-02 14:19:12 -04:00
Scott Idem
de048a084b chore: remove axios + deprecated electron_native.js; track lucide-svelte migration
- Uninstall axios — only consumer was electron_native.js (legacy V2 file)
- Delete electron_native.js — deprecated 2026-02-10, no active imports; replaced
  by aether_app_native_electron repo + electron_relay.ts contextBridge
- Add TODO: migrate remaining lucide-svelte → @lucide/svelte (5 files) then uninstall

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:04:28 -04:00
Scott Idem
ee79e33a2a fix(idb-sort): correct tmp_sort_* comparator direction in journals, IDAA recovery meetings, and BB post comments
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>
2026-06-02 13:50:15 -04:00
Scott Idem
a5243fa820 docs: document bootstrap account_id race condition and liveQuery stale-record pattern
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>
2026-06-02 13:39:11 -04:00
Scott Idem
5fce149808 fix(element_data_store): fix stale account_id showing wrong record on fresh load
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>
2026-06-02 12:04:59 -04:00
Scott Idem
a74effa6ff feat(badges): add Cvent Splash XLSX import mode; fix server-side upload timeout
- 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>
2026-06-02 11:37:48 -04:00
Scott Idem
33d48e7e78 Doc updates and project (sort of) temp files 2026-06-02 09:25:03 -04:00
Scott Idem
65e48c764e docs: document build_tmp_sort pattern and liveQuery filter dependency capture
Added two new sections to GUIDE__SvelteKit2_Svelte5_DexieJS.md:
- IDB Sort: build_tmp_sort Pattern — sort chain, priority inversion
  encoding, anti-.reverse() warning, modules using it
- $derived.by Dependency Capture — SCENARIO 2 filter pattern and
  API snapshot consistency fix

Updated TODO__Agents.md:
- Added anti-.reverse() warning and guide pointer to build_tmp_sort entry
- Added Sessions hide/show toggle section with both fixes marked complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:24:10 -04:00
Scott Idem
f6c950abdf fix(pres_mgmt): fix hidden sessions toggle not showing hidden sessions
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>
2026-05-28 22:24:04 -04:00
Scott Idem
3d6f9035c8 docs(todo): track build_tmp_sort rollout progress
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:59:30 -04:00
Scott Idem
535efd9c4b fix(pres_mgmt): add group as leading sort prefix for event_presentation
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>
2026-05-28 20:48:48 -04:00
Scott Idem
4a39ca1468 refactor(sort): introduce build_tmp_sort utility; apply to event_presentation and journals
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>
2026-05-28 20:38:25 -04:00
Scott Idem
182a066d38 fix(pres_mgmt): use tmp_sort_2 for presentation sort in Pres Mgmt and Launcher
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>
2026-05-28 20:20:50 -04:00
Scott Idem
35fed53e2a fix(launcher): sort presentations by priority > sort > start_datetime > code > name
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>
2026-05-28 20:11:14 -04:00
Scott Idem
322abc2691 fix(pres_mgmt): sort presentations by priority > sort > start_datetime > code > name
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>
2026-05-28 13:38:13 -04:00
Scott Idem
cfaf687717 fix(events): event location file load on mount + launcher pruning scope fix + remove legacy launcher btn
- 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>
2026-05-27 21:41:32 -04:00
Scott Idem
e7620a1c06 fix(events): replace db_events.file.clear() with targeted reload in refresh button
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>
2026-05-27 21:07:00 -04:00
Scott Idem
e4e2174c97 fix(upload): replace db_events.file.clear() with targeted per-object reload
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>
2026-05-27 20:33:12 -04:00
Scott Idem
d3bf314c62 fix(launcher): refresh file lists periodically to prune deleted/hidden files
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>
2026-05-27 20:12:10 -04:00
Scott Idem
d32355a1a2 docs(launcher): add cfg menu inventory and v3.1 design docs
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>
2026-05-26 22:17:07 -04:00
Scott Idem
213eabd8c1 feat(launcher): migrate cfg menu to 7-tab sidebar layout (v3.1)
Replaces the 3-tab horizontal bar (Setup / Device / Dev) with a vertical
sidebar navigation matching the v3.1 design spec. New tab structure:

  General       — App Modes, Screen Saver (operator-facing)
  Connectivity  — Remote Controller & WebSocket
  Sync & Health — Sync Timers, System Health
  Native OS     — OS controls (native or edit_mode preview)
  Wallpaper     — Desktop wallpaper settings
  Advanced      — Launch Timing, Updates (edit_mode only)
  Maintenance   — Local Resets, Debug Panel (edit_mode only)

Layout changes:
- Sidebar nav (w-48) + scrollable main content area replace inline tab bar
- Tab header shows label + description subtitle
- Technical Mode toggle is now a labeled button (not hidden icon)
- Footer shows Account/Device context; Reload moved to header
- {#key active_tab} wrapper ensures clean component remount on tab switch
- Remove unused icons (SlidersHorizontal, HeartPulse, Timer, CloudDownload)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:14:52 -04:00
Scott Idem
872291b0a0 fix(launcher): replace Flowbite Modal with custom overlay for cfg panel
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>
2026-05-26 22:04:34 -04:00
Scott Idem
25d17841e4 fix(launcher): fix cfg modal default-open and outside-click persistence
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>
2026-05-26 21:36:01 -04:00
Scott Idem
6282fb167f fix(launcher): use $derived.by for session liveQueries to fix stale presentation/presenter data
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>
2026-05-26 19:55:17 -04:00
Scott Idem
a90572bcb8 fix(idaa): ensure breakout links preserve site access key and uuid
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.
2026-05-23 11:31:10 -04:00
Scott Idem
194c89f6d1 style(launcher): layout and Tailwind class adjustments
+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>
2026-05-22 19:09:08 -04:00
Scott Idem
469729ce22 revert(help_tech): restore ae_loc preservation in Clear & Reload button
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>
2026-05-22 19:06:56 -04:00
Scott Idem
d1f5d0e2fd fix(launcher): clear ae_loc in cache cleanup; align tech help Clear & Reload
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>
2026-05-22 19:05:16 -04:00
Scott Idem
9c83567430 feat(launcher): add Clear Cache and Reload Launcher buttons to controls bar
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>
2026-05-22 19:02:26 -04:00
Scott Idem
b4d0d82141 fix(launcher): fix VLC stopping 10-15 seconds after open on macOS
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>
2026-05-22 18:10:37 -04:00
Scott Idem
15bfe6d5d6 feat(launcher): move Reset Apps to always-visible controls bar
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>
2026-05-22 18:02:22 -04:00
Scott Idem
dddf4b6170 feat(launcher): restore Kill Apps button in Native OS config
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>
2026-05-22 16:43:42 -04:00
Scott Idem
587b815446 docs(todo): mark composable flow + slide scripts done; add event_file cfg_json backend task
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:58:38 -04:00
Scott Idem
ca51a82dae feat(launcher): richer tooltip on file download button
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>
2026-05-22 15:58:35 -04:00
Scott Idem
a38320c7f5 fix(launcher): monospace font for session list date/time column
Datetime values align cleanly across rows when rendered in font-mono.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 15:58:31 -04:00
Scott Idem
c76fb8f2b5 fix(launcher): open_in_os win routing, display override, and onsite ext fix
- 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>
2026-05-22 15:58:27 -04:00
Scott Idem
a26ea8b49c fix(launcher): optimistic update for display override button
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>
2026-05-22 14:18:58 -04:00
Scott Idem
21fad1a698 fix(launcher): restore open_in_os win routing, fix cfg_json in IDB, fix display state
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>
2026-05-22 14:12:12 -04:00
Scott Idem
33e9eeef78 fix(launcher): retry activate in post-script loop to beat macOS focus-stealing
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>
2026-05-22 13:51:53 -04:00
Scott Idem
172ea994c7 refactor(launcher): consolidate menu controls and anchor to bottom
- 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>
2026-05-22 13:48:53 -04:00
Scott Idem
17b549a75c docs/refactor: finalize V3 cleanup and archive badge config project
- 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>
2026-05-21 23:04:07 -04:00
Scott Idem
3de01af1a1 docs: cleanup and archive agent TODO list
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>
2026-05-21 23:02:25 -04:00
Scott Idem
518a450b91 docs(events): reorganize badges and leads documentation
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>
2026-05-21 22:40:06 -04:00
Scott Idem
cb767ed115 docs(events): reorganize presentation and launcher documentation
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>
2026-05-21 22:36:00 -04:00
Scott Idem
86201f0fc1 feat(launcher): implement force-sync and chronological download priority
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>
2026-05-21 22:29:13 -04:00
Scott Idem
60e3fc539e docs(devops): add Nginx caching investigation task for app version pickup issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:31:51 -04:00
Scott Idem
b3029a4d27 docs: update TODO and add BOOTSTRAP mistake #13 for API retry regression
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>
2026-05-21 18:21:01 -04:00
Scott Idem
ea765d8ad2 fix(api): lower patch/delete timeout to 20s and add delete auth error banner
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>
2026-05-21 18:11:32 -04:00
Scott Idem
db5acdd30a docs: align API retry hardening status with implemented helpers 2026-05-21 18:04:06 -04:00
Scott Idem
a000e07647 api: harden delete retry classification and backoff 2026-05-21 17:58:59 -04:00
Scott Idem
7f9368589a api: harden patch retry classification and backoff 2026-05-21 17:53:30 -04:00
Scott Idem
55d3d49595 test: add v3 latency probe and modernize api coverage 2026-05-21 17:48:00 -04:00
Scott Idem
f5cf1ef398 api: separate timeout abort retries from intentional aborts 2026-05-21 15:46:30 -04:00
Scott Idem
d5d552a029 Badge layout fix for Axonius 2026-05-21 15:19:48 -04:00
Scott Idem
689bb326cb fix(api): restore network-error retry and add backoff in get/post_object
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>
2026-05-21 13:44:12 -04:00
Scott Idem
e6db2b4d6a fix(idaa): add Clear Cache & Reload escape hatch to recovery meetings server error state
"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>
2026-05-21 12:30:53 -04:00
199 changed files with 8613 additions and 6504 deletions

View File

@@ -1,22 +1,15 @@
# Aether Project Brief: aether_app_sveltekit # Aether Project Brief: aether_app_sveltekit
**Last Updated:** 2026-02-09 22:03:56 **Last Updated:** 2026-05-21 22:25:05
**Current Agent:** mcp_agent **Current Agent:** mcp_agent
## 🛠️ What I Just Did ## 🛠️ What I Just Did
Addressed multiple Svelte compiler warnings: Implemented "Force Sync Location" feature. Optimized file download order with a 4-tier chronological sort (Global > Session > Presentation > Creation Date). Added UI button for onsite operators. Updated project documentation. Verified with npm run check.
1. Converted ~30 decorative labels to spans (a11y).
2. Applied Svelte 5 untrack() pattern to initial state from props.
3. Fixed CSS scoping for TipTap editor.
4. Added rel="noopener noreferrer" to external links.
5. Commited changes in two atomic batches.
## 🚧 Current Blockers ## 🚧 Current Blockers
None. Remaining svelte-check warnings (219) require more granular ID/for linking in complex forms. None.
## ➡️ Exact Next Steps ## ➡️ Exact Next Steps
1. Granular fix for remaining 68 label/ID associations in address/person forms. User to review changes. Ready for onsite testing/deployment.
2. Systematic application of untrack() for remaining state-from-props warnings.
3. Clean up unused TipTap CSS selectors identified by svelte-check.
--- ---
*Generated by ae_brief* *Generated by ae_brief*

View File

@@ -0,0 +1,33 @@
# Plan: Fix IDAA Jitsi Breakout Links
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.
## 2. Implementation Steps
### Step 1: Update `src/routes/idaa/(idaa)/video_conferences/+page.svelte`
- Create a reactive `breakout_url` derived from `$page.url.href`.
- In the derivation logic:
- Instantiate a `new URL`.
- Check if `key` is present in `searchParams`. If missing, pull from `$ae_loc.allow_access` or `$ae_loc.site_access_key`.
- Check if `uuid` is present in `searchParams`. If missing, pull from `$idaa_loc.novi_uuid`.
- Return the resulting `href`.
- Update the following UI elements to use `breakout_url`:
- `copy_meeting_link` function (uses `navigator.clipboard.writeText`).
- "Open in New Tab" anchor tag (`href`).
- "Copy Link" fallback textarea (`value`).
- "Copy Break-out Link" in the Jitsi Tools panel (`value`).
### Step 2: Update `documentation/CLIENT__IDAA_and_customized_mods.md`
- Add a note in the "Authentication: Novi UUID System" or "Iframe Integration" section about the requirement for `key` and `uuid` in breakout links.
- Document that the frontend now automatically re-injects these for external links to ensure persistent session access outside the iframe.
## 3. Verification
- Manually verify the logic:
- If `key` and `uuid` are already in the URL (e.g., initial load), the derived URL should remain unchanged (or correctly deduplicated).
- If they are missing (e.g., after navigating from another IDAA page), they should be added to the generated link.
- Run `npx svelte-check` to ensure no syntax regressions.

View File

@@ -0,0 +1,53 @@
# Plan: Launcher Config UX Refinement (Cohesion & Stability)
The goal of this plan is to address the visual "bouncing", layering overload, and the misplaced close button in the new Launcher configuration modal.
## 1. Dimensional Stability
- **Problem:** Switching tabs causes the modal to resize vertically and horizontally, leading to a "bouncing" feel.
- **Solution:**
- Set a fixed height for the `Launcher_cfg` container (e.g., `h-[750px]`).
- Use `overflow-y-auto` only for the right-hand content pane.
- Ensure the sidebar has a stable width.
## 2. Visual Hierarchy & Layering
- **Problem:** Too many nested backgrounds (Page > Launcher > Modal > Inner Pane > Section Pane > Section Content).
- **Solution:**
- Flatten the background of the main content pane.
- Simplify `Launcher_Cfg_Section.svelte`:
- Remove `shadow-xl` from individual sections.
- Use subtler borders instead of strong "preset-outlined" colors.
- Remove the secondary background (`bg-white/5`) from the section content area.
- Standardize on a single, clean surface color for the right-hand pane.
## 3. The "Centered Close Button" Bug
- **Problem:** A close button is appearing in the middle of the screen.
- **Investigation:**
- Check for absolute-positioned elements in `Launcher_cfg.svelte` or `+layout.svelte`.
- Verify if Flowbite's `Modal` default close button is clashing with internal buttons.
- **Solution:**
- Consolidate all "Close" actions.
- Use the Modal's built-in top-right close button (if available) or a single, well-positioned button in the sidebar.
## 4. Implementation Steps
### Step 1: Update `Launcher_cfg.svelte`
- Set stable dimensions: `h-[750px] max-h-[90vh] w-[1000px] max-w-[95vw]`.
- Remove internal shadows and borders that conflict with the Modal container.
- Clean up the sidebar "Close" button.
### Step 2: Update `Launcher_cfg_section.svelte`
- Simplify the styling to reduce visual clutter.
- Remove `shadow-xl`.
- Use consistent padding and margins.
### Step 3: Update `+layout.svelte`
- Ensure the `Modal` is configured for a stable, large view without default padding issues.
- Verify the `modal_cfg_open` logic.
### Step 4: Add `Launcher_cfg_field.svelte` (Helper)
- Implement a unified field helper to standardize Label/Description/Input layouts across all tabs.
## 5. Verification
- Toggle between all 7 tabs. Verify zero layout shift (height/width remains constant).
- Check appearance in Light and Dark modes.
- Verify "Technical Mode" transitions.

View File

@@ -1,5 +1,7 @@
# Aether Project Architecture # Aether Project Architecture
**Last Updated:** 2026-06-12
This document outlines the overall architecture and key technologies used in the Aether SvelteKit frontend project. This document outlines the overall architecture and key technologies used in the Aether SvelteKit frontend project.
## 1. Project Overview ## 1. Project Overview
@@ -20,7 +22,7 @@ The Aether project is a Svelte and SvelteKit based application, utilizing Tailwi
- TipTap (`element_editor_tiptap.svelte`) — WYSIWYG rich-text for content fields (IDAA, Journals, Leads notes) - TipTap (`element_editor_tiptap.svelte`) — WYSIWYG rich-text for content fields (IDAA, Journals, Leads notes)
- **Icons:** Lucide Icons (SVG Icons) - **Icons:** Lucide Icons (SVG Icons)
- **Markdown Parsing:** `marked` library - **Markdown Parsing:** `marked` library
- **State Management:** Svelte stores, potentially with `liveQuery` from Dexie for reactive IndexedDB interactions. - **State Management:** Svelte 5 runes plus Dexie `liveQuery`. Events persisted state uses `runed` `PersistedState`; core and IDAA still contain legacy coarse-grained persisted stores pending migration.
### 2.1. Journals as the Canonical Frontend Pattern ### 2.1. Journals as the Canonical Frontend Pattern
@@ -78,8 +80,8 @@ Used for client-side persistence of various application states and configuration
Used for more structured client-side data storage, often for caching and offline capabilities. Used for more structured client-side data storage, often for caching and offline capabilities.
- `ae_core_db`: Core database instance. - `db_core`: Core database instance.
- `<module>`: Module-specific database instances. - `db_<module>`: Module-specific database instances (for example, `db_events` and `db_journals`).
- `<custom>`: Custom module-specific database instances (none currently defined). - `<custom>`: Custom module-specific database instances (none currently defined).
## 5. Data Sorting ## 5. Data Sorting
@@ -97,9 +99,9 @@ A set of standardized field names and types are used across Aether objects.
These fields are expected to be present in most Aether objects. These fields are expected to be present in most Aether objects.
- `id`: Primary key for an object (internal use, often a UUID). - `<object_type>_id`: Canonical randomized string ID returned by V3 (for example, `person_id`). Use this for URLs, relationships, and Dexie indexed lookups.
- `id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters). - `id`: Generic randomized string alias when returned by V3; do not assume it is a DB autonumber.
- `<object_type>_id_random`: Specific random ID for an object (e.g., `person_id_random`). - `id_random` / `<object_type>_id_random`: Legacy aliases. Do not introduce new usage.
- `code`: Short, unique identifier. - `code`: Short, unique identifier.
- `name`: Display name. - `name`: Display name.
- `enable`: Boolean for active/inactive status. - `enable`: Boolean for active/inactive status.

View File

@@ -1,5 +1,7 @@
# Aether Project Naming Conventions # Aether Project Naming Conventions
**Last Updated:** 2026-06-12
## 1. General Principles ## 1. General Principles
- **Clarity:** Names should clearly convey their purpose and meaning. - **Clarity:** Names should clearly convey their purpose and meaning.
@@ -11,9 +13,10 @@
- **Logic/Service Files:** `ae_<module>__<concept>.ts` (e.g., `ae_core__account.ts`, `ae_events__event.ts`) - **Logic/Service Files:** `ae_<module>__<concept>.ts` (e.g., `ae_core__account.ts`, `ae_events__event.ts`)
- **Database Definition Files:** `db_<module>.ts` (e.g., `db_core.ts`, `db_journals.ts`) - **Database Definition Files:** `db_<module>.ts` (e.g., `db_core.ts`, `db_journals.ts`)
- **Svelte Store Files:** `ae_<module>_stores.ts` (e.g., `ae_core_stores.ts`, `ae_journals_stores.ts`) - **Svelte Store Files:** Follow existing module names. Svelte 5 `PersistedState` files use a `.svelte.ts` suffix and are imported via the `.svelte` module path (for example, `ae_events_stores__badges.svelte.ts`).
- **Svelte Components:** - **Svelte Components:**
- **Module-specific components:** `ae_comp__<module>__<component_name>.svelte` (e.g., `ae_comp__events__event_card.svelte`) - **Route-level components:** `ae_comp__<component_name>.svelte`.
- **Module-specific components:** `ae_<module>_comp__<component_name>.svelte` (for example, `ae_events_comp__session_list.svelte`).
- **Generic/reusable components:** `element_<component_name>.svelte` (e.g., `element_input_file.svelte`, `element_qr_scanner_v2.svelte`) - **Generic/reusable components:** `element_<component_name>.svelte` (e.g., `element_input_file.svelte`, `element_qr_scanner_v2.svelte`)
- **SvelteKit Routes:** Follow SvelteKit's standard routing conventions (e.g., `+page.svelte`, `+layout.svelte`, `[id]/+page.svelte`). - **SvelteKit Routes:** Follow SvelteKit's standard routing conventions (e.g., `+page.svelte`, `+layout.svelte`, `[id]/+page.svelte`).
- **CSS Files:** `ae-<module>-<purpose>.css` (e.g., `ae-c-idaa-light.css`, `ae-osit-default.css`) - **CSS Files:** `ae-<module>-<purpose>.css` (e.g., `ae-c-idaa-light.css`, `ae-osit-default.css`)
@@ -37,9 +40,9 @@
- **Singularity:** Use singular nouns for objects and properties (e.g., `example.id`, not `examples.id`). - **Singularity:** Use singular nouns for objects and properties (e.g., `example.id`, not `examples.id`).
- **IDs:** - **IDs:**
- `id`: Primary key for an object (internal use, often a UUID). - `<object_type>_id`: Canonical randomized string ID returned by V3 (for example, `person_id`).
- `<object_type>_id`: Specific ID for an object (e.g., `person_id`). - `id`: Generic randomized string alias when V3 returns one; never assume it is an integer autonumber.
- `<object_type>_id_random`: Randomly generated ID for an object (often used for external exposure or URL parameters). - `<object_type>_id_random`: Legacy alias; do not introduce new usage.
- `account_id`, `site_id`, `user_id`, etc.: Foreign keys. - `account_id`, `site_id`, `user_id`, etc.: Foreign keys.
- **Common Properties:** - **Common Properties:**
- `code`: Short, unique identifier. - `code`: Short, unique identifier.
@@ -88,6 +91,6 @@
- `<module>` (extended modules) - `<module>` (extended modules)
- `<custom>` (custom modules) - `<custom>` (custom modules)
- **IndexedDB:** - **IndexedDB:**
- `ae_core_db` - `db_core`
- `<module>` - `db_<module>` (for example, `db_events`, `db_journals`)
- `<custom>` - `<custom>`

View File

@@ -1,6 +1,6 @@
# Aether — Permissions and Security # Aether — Permissions and Security
**Last updated:** 2026-02-27 **Last Updated:** 2026-06-12
**Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts` **Source of truth:** `src/lib/ae_utils/ae_utils__perm_checks.ts`, `src/lib/stores/ae_stores.ts`
--- ---
@@ -76,15 +76,18 @@ $ae_loc.adv_mode // boolean — advanced mode toggle
| AE Username + Password | `trusted` and above | Staff with AE accounts | | AE Username + Password | `trusted` and above | Staff with AE accounts |
| Novi UUID | `authenticated` | IDAA members (Novi membership system) | | Novi UUID | `authenticated` | IDAA members (Novi membership system) |
Passcodes are stored per-level in `$ae_loc.site_access_code_kv`: ### Site Passcode Security Warning
```typescript
site_access_code_kv: { The current frontend receives every site passcode in `access_code_kv_json`, copies the map into
administrator: null, // highest passcode tier persisted `$ae_loc.site_access_code_kv`, and compares entered passcodes locally. Verbose logging
trusted: null, // onsite staff passcode can also expose the complete map. This is a known active security gap, not the target design.
public: 'public1980', // example
authenticated: 'auth1980' Do not add new consumers of `site_access_code_kv`, log passcodes, or treat persisted
} `access_type` as durable proof of authentication. The target flow verifies passcodes through
``` `/authenticate_passcode`, stores a signed JWT with a role-specific TTL, and removes passcodes from
the public bootstrap response and client state.
See `documentation/PROJECT__AE_Site_Passcode_Security.md` for the active migration plan.
### `x-no-account-id` — Narrow Transport Exception ### `x-no-account-id` — Narrow Transport Exception

View File

@@ -1,4 +1,6 @@
# Aether SvelteKit — AI Agent Bootstrap / Quickstart # Aether SvelteKit — AI Agent Bootstrap / Quickstart
> **Doc Owner:** Frontend platform maintainers (OSIT) + active coding agents
> **Review Trigger:** Update when module architecture, critical rules, or primary doc paths change.
> **Read this first.** This doc is the fast path to being productive on this project. > **Read this first.** This doc is the fast path to being productive on this project.
> It covers the rules, patterns, and gotchas that matter most. > It covers the rules, patterns, and gotchas that matter most.
> Deep dives are in the linked docs at the bottom. > Deep dives are in the linked docs at the bottom.
@@ -29,7 +31,7 @@ running in Docker. The frontend talks to it exclusively via the V3 REST API.
| Editors | CodeMirror 6 (primary), Edra/TipTap (secondary) | | Editors | CodeMirror 6 (primary), Edra/TipTap (secondary) |
| Native | Electron app for onsite launcher (`src/lib/electron/electron_relay.ts`) | | Native | Electron app for onsite launcher (`src/lib/electron/electron_relay.ts`) |
| Backend | FastAPI + MariaDB, V3 API (`/v3/crud/`, `/v3/lookup/`) | | Backend | FastAPI + MariaDB, V3 API (`/v3/crud/`, `/v3/lookup/`) |
| Auth | Custom headers: `x-aether-api-key` + `x-account-id`; JWT Bearer is auto-injected when a session exists | | Auth | Custom headers only: `x-aether-api-key` + `x-account-id`**NOT** Bearer tokens |
--- ---
@@ -142,19 +144,31 @@ onDestroy(() => cleanup());
- Use `$state()` for local component state with no external binding. - Use `$state()` for local component state with no external binding.
### Store reactivity trap (important for `$effect`) ### Store reactivity trap (important for `$effect`)
The app uses `svelte-persisted-store` (Svelte 4 contract) for `$ae_loc`, `$ae_api`, The app has two kinds of persisted stores — know which you're reading:
`$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: **Svelte 4 `svelte-persisted-store` (coarse reactivity) — still used for:**
- `$ae_loc`, `$ae_sess`, `$ae_api` (global app state)
- `$idaa_loc`, `$idaa_sess` (IDAA module)
In Svelte 5 `$effect`, reading **any field** of these stores subscribes to the **entire store**.
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.
**Svelte 5 `PersistedState` (fine-grained reactivity) — Events module stores:**
- `badges_loc`, `leads_loc`, `pres_mgmt_loc`, `launcher_loc`, `events_auth_loc`
These use `runed`'s `PersistedState`. Access via `.current` (no `$` sigil):
`badges_loc.current.field`. Writing one field only re-triggers effects that read that field.
Import from the `.svelte` extension: `import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte'`.
For search pages using the coarse stores, this usually means:
- keep true user preferences in persisted local state - keep true user preferences in persisted local state
- keep transient triggers, loading flags, and last-executed search keys in session state when possible - 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 - 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 - 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
See `PROJECT__Stores_Svelte5_Migration.md` for migration status and the pattern to follow when migrating remaining stores.
### `{#await}` blocks ### `{#await}` blocks
```svelte ```svelte
{#await somePromise} {#await somePromise}
@@ -271,108 +285,29 @@ When building anything new, model it after Journals.
--- ---
## 7. Mistakes Agents Have Made on This Project ## 7. Common Mistakes (Reference)
These are real incidents — know them before you start. The full, curated mistake catalog now lives in
`documentation/REFERENCE__Common_Agent_Mistakes.md`.
1. **IDAA BB exposed publicly** — an agent removed an auth guard from the bulletin board Read this section first, then open the reference doc when your task touches one of these areas:
route. All IDAA content must be behind authentication. Always check route guards when
touching `/idaa/` routes.
2. **`event_file_id` in PATCH body (400 error)** — including the object ID in `data_kv` 1. **Security/Auth**private route guards, account scoping, and pre-gate data load risks.
when calling `update_ae_obj__*`. The V3 API tries to `SET event_file_id = ...` which 2. **V3 API payloads** — object ID in URL, `data_kv` field-only PATCH payloads.
fails because it's a view alias, not a DB column. See Section 2 above. 3. **Dexie/IDB behavior**`.get()` primary key trap, stale cache/version mismatches, broad result clipping.
4. **Svelte 5 reactivity** — coarse-store `$effect` loops and `$`-sigil misuse on plain props.
5. **Sorting and search correctness**`tmp_sort_*` comparator direction and Dexie sorting caveats.
6. **Network reliability** — retry classification in `api_*_object.ts` and timeout behavior.
7. **JSON field safety**`*_json` null reads/writes and wrapper serialization behavior.
8. **Service worker rollout behavior** — stale-tab symptoms, activation expectations, and trade-offs.
3. **Bad `.d.ts` declaration silently hid 1368 errors** — a `declare module` in `app.d.ts` The reference doc also includes a short archive list for older, less-relevant historical incidents.
(a script-context file) replaced the entire `@lucide/svelte` type exports instead of
merging. `svelte-check` showed 0 errors, masking real problems. If `svelte-check`
suddenly drops to 0 errors, verify it's not because a bad declaration wiped a module.
4. **Coarse store reactivity loop** — an `$effect` that read `$ae_loc.some_field` was
re-triggering repeatedly because unrelated writes to `$ae_loc` (e.g. SWR config reload)
fired the effect. In Svelte 5, any read of a Svelte 4 store inside `$effect` subscribes
to the whole store. Scope what you read carefully.
5. **`file_purpose == 'admin'` not hidden in Launcher** — the `hide_draft` prop hid
`outline` and `draft` files but not `admin` files. Gaps like this happen when a new
enum value is added to a field without auditing all the places that filter on it.
6. **Deleting files with `rm`** — always move to `~/tmp/agents_trash`. A deleted file may
contain context that's not recoverable from git if it was gitignored.
8. **Dexie `.get()` with a string object ID returns `undefined`** — Dexie `.get(value)`
looks up by the table's **primary key**, which is `id` (the first schema field). The V3
API never returns `id`, so it is always `undefined` in stored records. Passing a string
object ID (e.g. `person_id`) to `.get()` will silently return nothing. Always use
`.where('person_id').equals(person_id).first()` instead. This has caused liveQuery
blocks to always produce `undefined` even when the record exists in Dexie.
9. **Treating `$effect` blocks as auth bypass risks** — a `$effect` inside a child
component cannot bypass a parent `+layout.svelte` auth gate. Children only mount if
the parent calls `{@render children?.()}`. Adding redundant auth guards to `$effect`
blocks that can only run after the parent gate already passed is unnecessary — and
misleads future readers into thinking the parent gate is not sufficient on its own.
The **real** pre-gate risk is `+page.ts` / `+layout.ts`: universal load functions run
before any layout mounts and also fire during SvelteKit link prefetch. Keep those files
clean of data loads in private modules. See `GUIDE__SvelteKit2_Svelte5_DexieJS.md`
"SvelteKit Layout Hierarchy: Security and Execution Order" for the full explanation.
10. **Using query `key` as a proxy for bypass stripped `x-account-id`** — this caused
valid account-scoped requests to lose account context and 403. `key` can be a valid
endpoint/business param, but it is not equivalent to `x-no-account-id: bypass`. Keep
`x-no-account-id` usage narrow and temporary; do not expand it without a documented
allowlist case.
11. **Pre-stringifying `*_json` fields before passing to API wrappers** — the API wrappers
(`api_post__crud_obj.ts` for V3, `api.ts` for legacy CRUD) automatically serialize any
field ending in `_json` (e.g. `cfg_json`, `data_json`). Pass these as plain JS objects.
Pre-stringifying with `JSON.stringify()` before calling the wrapper will double-encode
the value in the legacy path (stringify sees a string and escapes it), and is at best
redundant on the V3 path. Both paths now pretty-print with 2-space indent.
See `GUIDE__AE_API_V3_for_Frontend.md` → section 3C for the full explanation.
12. **Broad Dexie result windows get silently clipped** — if a broad "All" view shows fewer
rows than a narrower filter, check for a page-level limit or an API revalidation step
replacing the local IDB result set. For empty text searches, the full local result set
should drive the display; server refreshes should update cache, not shrink visibility.
13. **Not bumping `IDB_CONTENT_VERSIONS` when changing `properties_to_save`** — this caused
the IDAA Recovery Meetings "no meetings found" bug for approximately one year (20252026).
**What happened:** A deploy changed `properties_to_save` in `ae_events__event.ts`, but no
one bumped `IDB_CONTENT_VERSIONS.events.event` in `store_versions.ts`. Existing users kept
the old stale event records in IndexedDB indefinitely. On the Recovery Meetings page, the
fast path (IDB search) returned those stale records, which all failed the `account_id`
filter and returned 0 results. The API call then either errored silently or was filtered
to 0 by the secondary client-side filter. Critically, the error state and the genuinely
empty state showed the **same** "No meetings found" message — users and staff had no
indication a failure had occurred. The manual Full Reset (via the `?` help panel) always
fixed it, but no one knew why it worked, making the root cause impossible to track down.
**The fix (2026-05-16):** `check_and_clear_idb_table()` in `store_versions.ts` is now
wired in `src/routes/idaa/(idaa)/+layout.svelte` for `db_events.event`. On a version
match it costs one localStorage read. On a mismatch it silently clears the table; the
SWR pattern then repopulates from the API on next load.
**The rule going forward:**
- When you change `properties_to_save` in any `ae_events__*.ts` file (or any other
object file) in a way that makes existing cached records stale — fields added, removed,
renamed, or where a computed field's behavior changes — **bump the matching entry in
`IDB_CONTENT_VERSIONS` in `src/lib/stores/store_versions.ts`**.
- If the table is not yet wired, wire it first (see the wiring instructions in the
`IDB_CONTENT_VERSIONS` comment block in `store_versions.ts`).
- Currently wired: `events.event`. All other tables are not yet wired.
**Also:** Never show the same UI message for both a failed API call and a genuinely empty
result. Always distinguish `qry__status === 'error'` from `qry__status === 'done'` with
0 results in your templates. Silent failures look like data problems and are extremely
difficult to diagnose.
--- ---
## 8. Source Layout (Quick Reference) ## 8. Source Layout (Quick Reference)
``` ```text
src/lib/ src/lib/
ae_api/ — API helpers (V3 preferred) ae_api/ — API helpers (V3 preferred)
ae_core/ — Account, User, Person, Site, hosted files ae_core/ — Account, User, Person, Site, hosted files
@@ -404,12 +339,23 @@ Start here, then go deeper as needed:
| What you need | Read | | What you need | Read |
|---|---| |---|---|
| Active tasks + known bugs | `documentation/TODO__Agents.md` ← always first | | Active tasks + known bugs | `documentation/TODO__Agents.md` ← always first |
| Documentation index | `documentation/README__Docs_Index.md` |
| Dev workflow + commit rules | `documentation/GUIDE__Development.md` | | Dev workflow + commit rules | `documentation/GUIDE__Development.md` |
| V3 API reference | `documentation/GUIDE__AE_API_V3_for_Frontend.md` | | V3 API reference | `documentation/GUIDE__AE_API_V3_for_Frontend.md` |
| WebSockets / real-time updates | `documentation/GUIDE__AE_API_V3_for_Frontend_websockets.md` |
| Dexie / liveQuery patterns | `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` | | Dexie / liveQuery patterns | `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md` |
| Common mistakes reference | `documentation/REFERENCE__Common_Agent_Mistakes.md` |
| Svelte 5 patterns + pitfalls | `documentation/GEMINI__Svelte_and_Me.md` | | Svelte 5 patterns + pitfalls | `documentation/GEMINI__Svelte_and_Me.md` |
| Permissions + auth levels | `documentation/AE__Permissions_and_Security.md` | | Permissions + auth levels | `documentation/AE__Permissions_and_Security.md` |
| Electron / native launcher | `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` | | Electron / native launcher | `documentation/MODULE__AE_Events_Launcher_Native.md` |
| Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.md` | | Store migration plan | `documentation/PROJECT__Stores_Svelte5_Migration.md` |
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Exhibitor_Leads.md` | | Journals module overview | `documentation/MODULE__AE_Journals.md` |
| Journals settings map | `documentation/MODULE__AE_Journals_Config_Map.md` |
| Exhibitor Leads module | `documentation/MODULE__AE_Events_Leads.md` |
| Presentation Management module | `documentation/MODULE__AE_Events_Presentation_Management.md` |
| IDAA client architecture | `documentation/CLIENT__IDAA_and_customized_mods.md` |
| IDAA Archives module | `documentation/MODULE__AE_IDAA_Archives.md` |
| IDAA Bulletin Board module | `documentation/MODULE__AE_IDAA_Bulletin_Board.md` |
| IDAA Recovery Meetings module | `documentation/MODULE__AE_IDAA_Recovery_Meetings.md` |
| IDAA Video Conferences module | `documentation/MODULE__AE_IDAA_Video_Conferences.md` |
| Naming conventions | `documentation/AE__Naming_Conventions.md` | | Naming conventions | `documentation/AE__Naming_Conventions.md` |

View File

@@ -31,6 +31,17 @@ IDAA is a private membership organization for physicians in recovery. They use t
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. 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.
**Example Breakout URL:**
`https://client.oneskyit.com/idaa/video_conferences?uuid=...&key=...&room=...`
--- ---
## Architecture: Composite Module ## Architecture: Composite Module
@@ -446,6 +457,32 @@ filtered) replaces it after the background refresh completes. The IDB path does
- Add `contact_li_json_ext` to the IDB fast-path filter in `search__event()` and the recovery - Add `contact_li_json_ext` to the IDB fast-path filter in `search__event()` and the recovery
meetings page so contact matches appear instantly from cache, not only after API refresh. meetings page so contact matches appear instantly from cache, not only after API refresh.
### Sort Encoding — Events Use Legacy (Not `build_tmp_sort`)
`ae_events__event.ts` builds `tmp_sort_1` with the **legacy encoding**: `priority ? 1 : 0`
(priority=true → `'1'`). This is the **opposite** of `build_tmp_sort` (priority=true → `'0'`).
| Module | Encoding | Correct comparator |
| --- | --- | --- |
| `ae_events__event.ts` (Recovery Meetings) | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` |
| `ae_events__event_session.ts` | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` |
| `ae_events__event_presentation.ts` | `build_tmp_sort` (overrides legacy in `specific_processor`) | **Ascending** `a.localeCompare(b)` |
| Journals, Posts, Archives | `build_tmp_sort` | **Ascending** `a.localeCompare(b)` |
**Do not apply the `build_tmp_sort` ascending rule to raw event or session sorts** until
`ae_events__event.ts` is migrated (tracked in TODO__Agents.md under IDB Sort rollout).
### Search Trigger — Use `$slct.account_id`, Not `$ae_loc.account_id`
The recovery meetings search `$effect` gates on `$slct.account_id` (set only by the bootstrap
Sync Effect, non-persisted). Do NOT change this back to `$ae_loc.account_id`.
**Why:** `$ae_loc` is a persisted store that hydrates from localStorage on page load. Its
`account_id` may be stale from a previous session (e.g., a dev/demo account_id left behind).
Using it as the gate fires the API call with the wrong account before bootstrap has run,
producing either a 403 or wrong-account data. `$slct.account_id` is null until bootstrap
sets it — a reliable gate. See mistake #14 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
### My Meetings (Favorites) ### My Meetings (Favorites)
Members can star meetings to build a personal "My Meetings" list. The star toggle appears: Members can star meetings to build a personal "My Meetings" list. The star toggle appears:
@@ -862,4 +899,4 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
--- ---
**Document Status:** ✅ Current **Document Status:** ✅ Current
**Last Verified:** 2026-05-19 — Access Gate: documented new `verify_error_type` error-handling states and retry/reset UI; Search Architecture: corrected contact-search status (now works via `default_qry_str` in API path — two root causes fixed 2026-05-18/19); noted IDB fast-path gap as remaining enhancement **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)

View File

@@ -1,5 +1,7 @@
# Aether API V3 Frontend Integration Guide (Svelte/TypeScript) # Aether API V3 Frontend Integration Guide (Svelte/TypeScript)
**Last Updated:** 2026-06-12
This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints. This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints.
--- ---
@@ -96,6 +98,26 @@ The primary way to retrieve data.
* **Endpoint:** `POST /v3/crud/{obj_type}/search` * **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. * **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.
#### Sorting with `order_by_li`
Pass a JSON object as the `order_by_li` query parameter to sort results:
```ts
// ?order_by_li={"filename":"ASC","created_on":"DESC"}
const params = new URLSearchParams({
order_by_li: JSON.stringify({ filename: 'ASC', created_on: 'DESC' })
});
```
> [!IMPORTANT]
> **`order_by_li` only accepts columns from the raw base table** — not view-only join columns.
>
> Some object types (e.g. `event_file`) have enriched views that JOIN other tables to expose convenience fields like `event_presenter_family_name`. These are available in search results when using `?view=alt`, but they **cannot** be used in `order_by_li`. Attempting to sort by them silently drops those sort keys (the query proceeds without them).
>
> If you need to sort by a joined field, sort client-side on the returned list.
>
> **Columns safe to sort on for `event_file`:** any field in the `event_file` table itself — `filename`, `title`, `extension`, `created_on`, `updated_on`, `sort`, `enable`, etc.
### C. POST Create / PATCH Update ### C. POST Create / PATCH Update
Modify data in the system. Modify data in the system.
* **Endpoints:** * **Endpoints:**
@@ -266,7 +288,7 @@ When seeding new lookup data (e.g., adding timezones in bulk):
## 5. Event File Data Retrieval (Hosted Files) ## 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: Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file`). The Hosted File is a metadata record for binary content, accessed via dedicated Action endpoints. To download an event file use `/v3/action/event_file/{event_file_id}/download` — not the hosted_file endpoint directly (each endpoint only accepts its own ID type). To retrieve hosted file metadata alongside an event file record:
* **Endpoint:** `GET /v3/crud/event_file/{event_file_id}` * **Endpoint:** `GET /v3/crud/event_file/{event_file_id}`
* **Query Parameter:** Add `inc_hosted_file=true` * **Query Parameter:** Add `inc_hosted_file=true`
@@ -281,6 +303,48 @@ Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file
* `hosted_file_size` (string - in bytes) * `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. 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.
### Direct Download Links (Shareable / External)
Event files can be downloaded without standard auth headers using one of two bypass mechanisms. This is useful for generating shareable links for staff or external recipients.
- **Method:** `GET`
- **Path:** `/v3/action/event_file/{event_file_id}/download`
> [!WARNING]
> **Breaking change (2026-06-10):** This endpoint now requires an `event_file_id`. Previously it accepted `hosted_file_id` or `archive_content_id` and resolved the chain automatically — that cross-resolution has been removed. Pass the correct ID type for the endpoint you are calling. If you were routing downloads through `/v3/action/hosted_file/{hosted_file_id}/download` as a workaround, switch to this endpoint using `event_file_id`. *(Remove this note after ~2026-06-24.)*
#### Auth bypass options
| Query param | Value | When to use |
|---|---|---|
| `?key=<account_id_random>` | Any valid account random ID | Staff sharing within a known account context |
| `?site_key=<site_access_key>` | The site's `access_key` value | Public or semi-public distribution tied to a specific site |
Either param replaces the need for `x-aether-api-key` / `x-account-id` headers, so the URL is self-contained and works in a plain browser tab or `<a href>` link.
#### Optional params
| Query param | Description |
|---|---|
| `filename` | Override the download filename (min 4 chars). Useful for giving files clean display names. |
#### Building a shareable link
```ts
// Build a self-contained download URL for staff/external use
function makeDownloadUrl(eventFileId: string, accountId: string, displayName?: string): string {
const base = `https://dev-api.oneskyit.com/v3/action/event_file/${eventFileId}/download`;
const params = new URLSearchParams({ key: accountId });
if (displayName) params.set('filename', displayName);
return `${base}?${params}`;
}
```
The endpoint supports byte-range requests (`Range` header), so it works correctly for in-browser media streaming as well as direct file downloads.
> [!NOTE]
> The `?key=` bypass verifies only that the account ID exists — it does not confirm the file belongs to that account. It is appropriate for internal staff tools. For publicly distributed links, prefer `?site_key=` which ties access to a specific site's configured key.
--- ---
## 6. Hosted File Actions: Convert & Clip (Frontend Notes) ## 6. Hosted File Actions: Convert & Clip (Frontend Notes)
@@ -301,18 +365,16 @@ These helper endpoints let the frontend request small server-side transformation
- Required query params: `link_to_type`, `link_to_id`, `start_time`, `end_time` (format `HH:MM:SS`) - Required query params: `link_to_type`, `link_to_id`, `start_time`, `end_time` (format `HH:MM:SS`)
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool) - Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool)
- Auth: standard V3 headers - Auth: standard V3 headers
- 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`.
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`. - Defaults to stream-copying (fast); set `reencode=true` to force H.264 or `scale_down=true` to resize.
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize. - Add `?background=true` to schedule the clip asynchronously — returns `202 Accepted` immediately; poll the `hosted_file` record for completion.
- 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; 202 when scheduled successfully.
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
Frontend guidance: 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. - 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. - 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. - Prefer `?background=true` for large inputs to avoid request timeouts. 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.
--- ---
@@ -673,19 +735,19 @@ Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether
|---|---|---| |---|---|---|
| `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 | | `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 | | `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'` | | `503` | Novi unreachable or Novi 5xx error | Surface as `'api_error'`; advise retry |
### Migration from direct Novi call — ✅ Complete (2026-05-19) ### Migration from direct Novi call
`+layout.svelte:verify_novi_uuid()` now calls this endpoint instead of Novi directly. Response code mapping (for reference): The frontend's `+layout.svelte:verify_novi_uuid()` currently calls Novi directly from the browser. Replace that `fetch()` with this endpoint. Response code mapping:
| Direct Novi result | This endpoint returns | Frontend behavior | | Direct Novi result | This endpoint returns | Frontend state |
|---|---|---| |---|---|---|
| `200` with identity data | `200` | `verified` | | `200` with identity data | `200` | `verified` |
| `200` with no identity data | `404` | `denied` | | `200` with no identity data | `404` | `denied` |
| `404` | `404` | `denied` | | `404` | `404` | `denied` |
| `429` | `429` | Auto-retry after 10s; `'rate_limited'` if retry fails | | `429` | `429` | `'rate_limited'` |
| Network error / Novi 5xx | `503` | Auto-retry after 3s; `'api_error'` if retry fails | | Network error / Novi 5xx | `503` | `'api_error'` |
### Caching ### Caching

View File

@@ -1,5 +1,7 @@
# Aether API V3 WebSocket Integration Guide # Aether API V3 WebSocket Integration Guide
**Last Updated:** 2026-06-12
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. 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.
--- ---

View File

@@ -1,7 +1,12 @@
# Aether Events — Onsite Badge Printing # Aether Events — Onsite Badge Printing
**Last Updated:** 2026-06-12
Notes on setup, process, hardware, and browser behavior for onsite badge printing at events. Notes on setup, process, hardware, and browser behavior for onsite badge printing at events.
For cross-module onsite operations (SRR, launcher monitoring, exhibitor leads support), see:
`documentation/GUIDE__AE_Events_Onsite_Runbook.md`.
--- ---
## Overview ## Overview
@@ -157,26 +162,58 @@ This layout hides `.badge_back` in `@media print` — only the front face prints
--- ---
### Epson — Fan-Fold / Label Printer ### Epson ColorWorks C3500 — Fan-Fold Label Printer
**Status:** Not yet tested. Section to be filled in after testing. **Card stock:** 4" × 6" fan-fold paper label stock
**Layout code:** `badge_4x6_fanfold`
**Status:** Configured. First live use: Axonius Adapt DC — June 9, 2026.
Common Epson models used for fan-fold name badge stock: TM-T88 series, C3500, LX series. The C3500 is a color inkjet label printer — it prints continuous fan-fold paper stock,
Fan-fold stock is typically 4" × 3" or 4" × 6" paper labels. not individual cards. Badges are separated along the perforation after printing.
#### Physical Setup
- Connect via USB or Ethernet
- Load 4" × 6" fan-fold stock per Epson instructions
- The C3500 is single-sided — only the front face prints. Back section is suppressed in CSS.
- The badge has a lanyard hole punch: 5/8" × 1/8", centered, 1/4" from the top.
Most fan-fold stock for badge use includes a pre-punched lanyard slot — verify stock matches.
#### Driver
- Epson ColorWorks C3500 CUPS driver available from epson.com (ColorWorks section)
- On Linux/CUPS: install the provided PPD and add the printer at `http://localhost:631`
- Set default paper size to **4" × 6"** in CUPS
- Print a test page from CUPS before going live
#### Chrome Print Settings (C3500)
| Setting | Value |
|---|---|
| Destination | Epson C3500 (CUPS name) |
| Paper size | 4 × 6 in (set in CUPS driver) |
| Margins | **None** |
| Background graphics | On |
| Pages | 1 (single-sided) |
#### CSS Layout #### CSS Layout
Fan-fold badges would use a layout sized to the specific label stock. The C3500 uses the `badge_4x6_fanfold` layout. CSS file:
A new CSS layout file will need to be created per stock size if not already present. `src/lib/ae_events/badges/css/badge_layout_epson_4x6_fanfold.css`
Naming convention: `badge_layout_epson_[model]_[size].css`
#### Setup Notes Created 2026-05-15 for Axonius Adapt DC. Key specs:
- `badge_front` 4" × 6", portrait orientation
*(To be filled in after testing — cover: driver source, CUPS setup, paper size, Chrome settings)* - `badge_header` max-height 1.5in
- Lanyard hole: 5/8" × 1/8", centered, 1/4" from top
- `@page { size: 4in 6in; margin: 0; }` set in the print page dynamically
- `.badge_back` suppressed in `@media print` (single-sided)
#### Known Behaviors #### Known Behaviors
*(To be filled in after testing)* - Same Chrome margin rules apply: **Margins → None** prevents URL/date header clipping
- Firefox honors `@page { size: 4in 6in }` for PDF proofing — use it to verify layout
- Fan-fold stock separates along the perforation — no cutting needed, but verify the
perforation lands outside the badge content area
--- ---

View File

@@ -0,0 +1,138 @@
# Guide — Aether Events: Onsite Runbook
**Last Updated:** 2026-06-12
This guide covers the human-centric logistics and "In the Heat of the Moment" support for onsite event operations.
---
## Badge Printing
Aether badge printing uses the browser's native `window.print()` — no special software or print
server needed.
This runbook keeps badge guidance concise for onsite flow. For detailed printer/browser setup,
driver notes, and troubleshooting matrix, use:
`documentation/GUIDE__AE_Events_Badges_Onsite.md`.
### Kiosk Station Setup
- **Browser:** Use **Chrome (Chromium)** for all kiosk stations.
- **Settings:** Set Margins to **None**. Enable **Background Graphics**.
- **Mode:** Use normal browser sessions (not Incognito) to allow PWA caching.
### Printer Reference: Zebra ZC10L (PVC)
- **Stock:** 3.5" × 5.5" PVC cards.
- **Orientation:** Cards face-up, landscape in the hopper.
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
- **Layout code:** `badge_3.5x5.5_pvc`
### Printer Reference: Epson ColorWorks C3500 (Fan-Fold)
- **Stock:** 4" × 6" fan-fold paper label stock.
- **Single-Sided:** Only the front face prints; the back section is hidden via CSS.
- **Layout code:** `badge_4x6_fanfold`
- **Lanyard hole:** Pre-punched 5/8" × 1/8" slot at top center — verify stock matches.
- **First live use:** Axonius Adapt DC, June 9, 2026.
### Printing Workflow
1. **Search:** Find the attendee by name or QR scan in the Badges module.
2. **Review:** Open the print page and confirm the layout looks correct.
3. **Print:** Click **Print Badge**. `print_count` increments automatically.
4. **Handoff:** Verify the card print quality before handing it to the attendee.
---
## Exhibitor Leads (Lead Retrieval)
Exhibitors use a PWA (Progressive Web App) to scan badges and capture leads.
### Exhibitor Support Workflow
1. **Booth Lookup:** Help the exhibitor find their booth in the Leads landing page.
2. **Sign-In:** Assist with the **Shared Passcode** or individual **Licensed User** login.
3. **App Install:** Encourage them to "Add to Home Screen" (iOS) or click the Install button (Android/Chrome) for offline stability.
4. **Scanning Demo:** Show them the **Rapid Scan** mode. Remind them that attendees must have `allow_tracking = true` on their record to be scanned.
### Managing Licenses
- License counts are managed in the **Manage** tab (Admin or Shared Passcode only).
- If an exhibitor needs more staff slots, update the `license_max` in the Exhibit record.
---
## Speaker Ready Room (SRR)
... (rest of the file) ...
The SRR is the central hub for content management and presenter support.
### SRR Practice Stations
Stations mirror the session room setup exactly:
- Same Mac laptop model and adapter/dongle configuration as the podiums.
- Projector and screen (where possible).
- Launcher running in **Native** mode — ensures verification matches the podium experience.
### Staffing Roles
| Role | Access Level | Typical Tasks |
|---|---|---|
| **OSIT Staff** | `trusted_access` | Manage devices, monitor via VNC, deep troubleshooting. |
| **Client Staff** | `authenticated_access` | Upload files, view session lists, assist presenters. |
| **Presenter** | `authenticated_access` | Self-upload via QR link (if enabled). |
### SRR Workflow — Day-of-Show
1. **Check-in:** Staff looks up the presenter's session in Presentation Management.
2. **Upload:** File is uploaded to the presenter/session record.
3. **Verification:** Staff opens the file on a practice station to confirm rendering.
4. **Launcher Sync:** File propagates to the podium. Use **Force Sync Location** in the Launcher config if immediate full-room caching is needed.
5. **Proceed:** Presenter walks to the room; the podium kiosk already has the file cached.
---
## Onsite Operation (Managing Parallel Rooms)
### SRR Overview Page
The Pres Mgmt overview (`/events/[id]/pres_mgmt`) is the "Command Center":
- Monitor file status per session.
- Filter by location and time block to stay ahead of active sessions.
### Per-Room Monitoring
- Use **VNC or RustDesk** to monitor all podium screens in real time from the SRR.
- Confirm "Native Sync" status chip in the bottom-left of the Launcher is green/idle before sessions start.
### Session Transitions
- **Timing:** Ideally, sessions show/hide based on `datetime_start`.
- **Manual Control:** In looser schedules, use Launcher controls to manually select the current session.
---
## Pre-Show Checklist
### 12 Weeks Before
- [ ] Event created with correct dates and timezone.
- [ ] `mod_pres_mgmt_json` configured for client needs.
- [ ] Locations (rooms) created and named.
- [ ] Sessions created, assigned to locations, and timed.
- [ ] Launcher devices (`event_device`) registered with correct codes.
- [ ] Device-to-location assignments confirmed.
### Day Before (SRR Setup)
- [ ] Mac laptops at podiums booted; Electron app running.
- [ ] Each podium confirms it loaded the correct room's Launcher.
- [ ] SRR practice stations confirmed (matching hardware).
- [ ] Run **Force Sync Location** on all podiums to pre-cache all day-1 content.
- [ ] VNC/RustDesk connections established to all podiums.
### Day of Show
- [ ] Confirm all session times are accurate before the first block.
- [ ] Monitor SRR queue and verify every file on a practice station.
- [ ] Check VNC wall to ensure all podiums are online and synced.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Session not in Launcher | Datetime wrong or Location unassigned. | Verify session metadata in Pres Mgmt. |
| File uploaded but missing | Polling lag or attached at wrong level. | Wait 30s; check if file is at Session vs Presenter level. |
| File opens slowly | Not in native cache yet. | Check "Native Sync" chip; use Force Sync in config. |
| File won't open | Corrupt upload or missing Mac codec. | Test on SRR station; convert or re-upload. |
| Drifted schedule | Room timing shifted. | Use Launcher controls to manually select the active session. |
| `lock_config` resets changes | Remote config is forced. | Edit the master `mod_pres_mgmt_json` in Event Settings. |
| Move laptop to new room | Hardware reassignment. | Update `location_id` in `event_device` record; restart Electron. |

View File

@@ -1,8 +1,10 @@
# Aether UI — Design System Style Guidelines # Aether UI — Design System Style Guidelines
> **Version:** 1.2 (2026-03-20) > **Version:** 1.2 (2026-03-20)
> **Last Updated:** 2026-03-20
> **Author:** One Sky IT / Scott Idem > **Author:** One Sky IT / Scott Idem
> **Scope:** All Aether SvelteKit frontend components > **Scope:** All Aether SvelteKit frontend components
> **Related:** `AE__UI_Component_Patterns.md`, `ae-firefly.css`, `documentation/AE__Components.md` > **Related:** `ae-firefly.css`, `documentation/MODULE__AE_Journals.md`, `documentation/MODULE__AE_Events_Presentation_Management.md`
> **Historical implementation log:** `documentation/archive/PROJECT__AE_Style_Review_2026-03.md`
--- ---

View File

@@ -1,5 +1,6 @@
# Aether Development SOP (Frontend) # Aether Development SOP (Frontend)
> **Version:** 1.2 (2026-03-17) > **Version:** 1.2 (2026-03-17)
> **Last Updated:** 2026-03-17
> **Location:** documentation/GUIDE__Development.md > **Location:** documentation/GUIDE__Development.md
## 1. Verification (The "Test-First" Mandate) ## 1. Verification (The "Test-First" Mandate)
@@ -50,7 +51,7 @@ You are not working in a vacuum. Coordinate with the Backend Agent via MCP tools
| `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns | | `documentation/GEMINI__Svelte_and_Me.md` | Svelte 5 runes patterns |
| `documentation/AE__Architecture.md` | System architecture overview | | `documentation/AE__Architecture.md` | System architecture overview |
| `documentation/AE__Naming_Conventions.md` | Naming rules | | `documentation/AE__Naming_Conventions.md` | Naming rules |
| `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` | Electron/Launcher reference | | `documentation/MODULE__AE_Events_Launcher_Native.md` | Electron/Launcher reference |
| `tests/README.md` | Playwright test guide — shared helpers, hard-won lessons, demo IDs | | `tests/README.md` | Playwright test guide — shared helpers, hard-won lessons, demo IDs |
## 6. Inline Field Editing — `element_ae_obj_field_editor` ## 6. Inline Field Editing — `element_ae_obj_field_editor`

View File

@@ -1,4 +1,6 @@
# AE Docker CI Cache Policy (recommendation) # Aether Docker CI Cache Policy
**Last Updated:** 2026-06-12
Purpose Purpose
- Provide a straightforward policy to keep build caches useful but bounded. - Provide a straightforward policy to keep build caches useful but bounded.

View File

@@ -1,5 +1,7 @@
# Stability Patterns for liveQuery + Svelte 5 # Stability Patterns for liveQuery + Svelte 5
**Last Updated:** 2026-06-12
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). 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). - 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).
@@ -25,13 +27,54 @@ let lq__obj = $derived(
- 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. - 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(() => {
const account_id = $slct.account_id; // null until bootstrap Sync Effect runs
const api_ready = !!$ae_api?.base_url;
const entry = $lq__obj as SomeType | null | undefined;
if (!browser || !account_id || !api_ready) return;
// null account_id on a record = global/shared fallback — still a valid 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...';
}
});
```
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 ### 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. **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: **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**. 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. 2. **Missing microtask yields**: Even when caching was enabled, components would mount and subscribe to liveQuery _before_ IndexedDB writes completed, causing race conditions.
**Example of the Bug:** **Example of the Bug:**
```typescript ```typescript
@@ -89,6 +132,99 @@ $effect(() => {
- 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. - 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()`.
**Utility:** `src/lib/ae_core/core__idb_sort.ts``build_tmp_sort()`
```typescript
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
// Inside specific_processor callback:
const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
prefix: [obj.group ?? '0'], // always first
priority: obj.priority, // boolean; true→'0' so ASC sorts it first
sort: obj.sort, // zero-padded to 8 chars
fields_1: [...], // module-specific tier-1 fields
fields_2: [...], // tier-2 fields (tmp_sort_2 = base + tier-1 + tier-2)
fields_3: [...] // tier-3 fields
});
obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
obj.tmp_sort_3 = tmp_sort_3;
```
**Sort chain convention:** `group → priority DESC → sort ASC → [module-specific] → name`
**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.
```ts
// ✅ Correct — ascending; priority=true ('0') sorts before priority=false ('1')
list.sort((a, b) => (a.tmp_sort_1 ?? '').localeCompare(b.tmp_sort_1 ?? ''));
// ❌ Wrong — descending inverts the encoding; priority=false ('1') sorts first
list.sort((a, b) => (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? ''));
```
**Modules using `build_tmp_sort`:**
- `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
let lq__event_session_obj_li = $derived.by(() => {
const ids = event_session_id_li; // drives SCENARIO 1 vs 2
const event_id = $events_slct?.event_id;
const qry_hidden = pres_mgmt_loc.current.qry_hidden; // extra dependency
return liveQuery(async () => {
// SCENARIO 1 — specific IDs (fast path or API result)
if (Array.isArray(ids) && ids.length > 0) {
const results = await db.session.bulkGet(ids);
return results.filter(Boolean);
}
// SCENARIO 2 — broad fallback, uses captured qry_hidden
if (event_id && !someFilter) {
const all = await db.session.where('event_id').equals(event_id).sortBy('name');
return all.filter((s: any) => {
if (qry_hidden === 'not_hidden') return !s.hide;
if (qry_hidden === 'hidden') return !!s.hide;
return true; // 'all'
});
}
return [];
});
});
```
**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:
hidden: pres_mgmt_loc.current.qry_hidden ?? 'not_hidden'
// Good — uses snapshot captured when handle_search_refresh was called:
hidden: params.qry_hidden ?? 'not_hidden'
```
---
## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings) ## Practical Patterns from Aether (Journals & Events & IDAA Recovery Meetings)
- 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: 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. - 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.
@@ -244,7 +380,7 @@ The `createLiveQueryStore` function creates a readable store that automatically
## SvelteKit Layout Hierarchy: Security and Execution Order ## SvelteKit Layout Hierarchy: Security and Execution Order
Understanding *when* SvelteKit code runs is critical for private-data modules like IDAA. Understanding _when_ SvelteKit code runs is critical for private-data modules like IDAA.
### Execution order on any navigation ### Execution order on any navigation
@@ -280,7 +416,7 @@ future readers into thinking the parent gate alone is not sufficient.
### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts` ### Where the actual pre-gate risk lives: `+page.ts` / `+layout.ts`
Universal load functions run *before* components mount and *before* layout effects Universal load functions run _before_ components mount and _before_ layout effects
execute. They also fire during SvelteKit link prefetch — triggered by the user execute. They also fire during SvelteKit link prefetch — triggered by the user
hovering a link, even if they never navigate. This makes them unsafe for private data: hovering a link, even if they never navigate. This makes them unsafe for private data:
@@ -364,7 +500,7 @@ If you must use non-blocking loads, you must pass the initial data to the compon
## The `untrack()` Reactive-Tracking Trap ## 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. `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 ### Symptom
@@ -428,7 +564,7 @@ Before wrapping a store read in `untrack()`, ask: **"Do I need this effect to re
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**. Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
**❌ Invalid Pattern (Causes Compile Error):** **❌ Invalid Pattern (Causes Compile Error):**
Attempting to normalize a value *inside* the binding will fail. Attempting to normalize a value _inside_ the binding will fail.
```svelte ```svelte
<!-- Error: Can only bind to an Identifier or MemberExpression --> <!-- Error: Can only bind to an Identifier or MemberExpression -->
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} /> <Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />

View File

@@ -3,7 +3,7 @@
**Module Path:** `src/routes/events/[event_id]/(badges)/templates/` **Module Path:** `src/routes/events/[event_id]/(badges)/templates/`
**API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts` **API Module:** `src/lib/ae_events/ae_events__event_badge_template.ts`
**Database Table:** `event_badge_template` **Database Table:** `event_badge_template`
**Last Updated:** 2026-03-02 **Last Updated:** 2026-06-12
--- ---
@@ -114,17 +114,19 @@ corresponding `ticket_N_text` on the template provides the HTML rendered on the
| `priority`, `sort`, `group` | int/str | Standard AE sort fields | | `priority`, `sort`, `group` | int/str | Standard AE sort fields |
| `notes` | str | Internal notes | | `notes` | str | Internal notes |
### New Field (pending backend addition) ### Duplex / Single-Sided
| Field | Type | Notes | | Field | Type | Notes |
|---|---|---| |---|---|---|
| `duplex` | bool | **Planned** — when `false`, back section is hidden from print (`@media print`) | | `duplex` | bool | When `false`, back section is hidden from print (`@media print`) |
The `duplex` field controls whether the back-of-badge section renders during printing. The `duplex` field controls whether the back-of-badge section renders during printing.
When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front When `false` (single-sided), `badge_back` gets `print:hidden` applied so only the front
prints. The back section still displays on screen for configuration reference. prints. The back section still displays on screen for configuration reference.
The first event using this system (Axonius, NYC, mid-April 2026) uses single-sided PVC `duplex` is in `properties_to_save` and `show_badge_back` is derived from it in
cards on a Zebra ZC10L — `duplex` will be `false` for that event's templates. `ae_comp__badge_obj_view.svelte`. (Verified 2026-03-18)
Axonius events use `duplex = false` — single-sided printing only.
--- ---
@@ -155,7 +157,7 @@ The print page (`print/+page.svelte`) or the badge view should conditionally add
</svelte:head> </svelte:head>
``` ```
This is not yet implemented — tracked as a pending Phase 1 item. This is implemented — `style_href` loads via `<svelte:head>` in `print/+page.svelte` and is included in `properties_to_save`. (Verified 2026-03-18)
### CSS Scope ### CSS Scope
@@ -192,6 +194,128 @@ wrapper so multiple layouts can coexist in the bundle without conflict.
--- ---
## 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. |
| `hide_affiliations` | bool | `false` | Suppresses the affiliations field. |
| `hide_location` | bool | `false` | Suppresses the location field. |
### QR Codes
These keys override the top-level DB fields (`show_qr_front`, `show_qr_back`) when present. Prefer setting them here rather than the top-level fields.
| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `show_qr_front` | bool | `false` | Show attendee QR on badge front. |
| `show_qr_back` | bool | `true` | Show attendee QR (+ ID text) on badge back. |
### Text Alignment
Stored under a nested `align` object.
```json
"align": { "name": "left", "title": "left", "affiliations": "left", "location": "center" }
```
| Key | Values | Default |
| --- | --- | --- |
| `align.name` | `left` \| `center` \| `right` \| `justify` | `center` |
| `align.title` | `left` \| `center` \| `right` \| `justify` | `center` |
| `align.affiliations` | `left` \| `center` \| `right` \| `justify` | `center` |
| `align.location` | `left` \| `center` \| `right` \| `justify` | `center` |
QR alignment stored under `qr_alignment`:
| Key | Values | Default |
| --- | --- | --- |
| `qr_alignment.front` | `left` \| `center` \| `right` | `center` |
| `qr_alignment.back` | `left` \| `center` \| `right` \| `justify` | `center` |
### Header Image
| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `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.
```json
"fit_heights": { "grp_name_title": "1.8in", "name": "1.4in" }
```
| Key | Notes |
|---|---|
| `grp_name_title` | Height of the name+title container |
| `grp_name_title_flex` | Flex distribution: `around` \| `between` \| `even` \| `center` \| `start` \| `end` |
| `name` | Height of the name text zone |
| `title` | Height of the title text zone |
| `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.
```json
"punch_holes": { "left": true, "right": true, "center": false }
```
| Key | Default | Notes |
| --- | --- | --- |
| `punch_holes.left` | `false` | Left clip slot marker |
| `punch_holes.right` | `false` | Right clip slot marker |
| `punch_holes.center` | `false` | Center clip slot marker (less common) |
---
### Controls Panel (`controls_cfg`)
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.
```json
"controls_cfg": {
"shown": ["name", "title", "affiliations"],
"auth_editable": ["title", "affiliations", "location"]
}
```
| Key | Type | Default |
| --- | --- | --- |
| `controls_cfg.shown` | `string[]` | `["name", "title", "affiliations", "location"]` |
| `controls_cfg.auth_editable` | `string[]` | `["title", "affiliations", "location", "allow_tracking", "pronouns"]` |
Valid field keys: `name`, `title`, `affiliations`, `location`, `pronouns`, `allow_tracking`.
This config applies to the onsite print controls. Remote review currently uses
`event.mod_badges_json.edit_permissions` instead. Consolidating or defining precedence between
these two permission sources is tracked in `documentation/TODO__Agents.md`.
---
## Template-Derived Features (component behavior) ## Template-Derived Features (component behavior)
### badge_type_list → badge type select ### badge_type_list → badge type select
@@ -218,12 +342,12 @@ The `properties_to_save` array in `ae_events__event_badge_template.ts` controls
gets cached locally. Current state — fields **NOT** in properties_to_save that exist gets cached locally. Current state — fields **NOT** in properties_to_save that exist
in DB and may be needed: in DB and may be needed:
- `style_href` — needed once external CSS is wired via `<svelte:head>`
- `passcode` — not needed client-side - `passcode` — not needed client-side
- `footer_title`, `footer_left`, `footer_right` — not needed (legacy) - `footer_title`, `footer_left`, `footer_right` — not needed (legacy)
- `header_background`, `footer_background` — not needed (legacy) - `header_background`, `footer_background` — not needed (legacy)
- `script_src` — do not add; this field should not be used - `script_src` — do not add; this field should not be used
- `duplex`**add when backend adds the field**
`duplex` is already saved to IDB and drives single-sided rendering.
--- ---
@@ -360,6 +484,8 @@ Firefox users can use "Save to PDF" directly — it just works.
- [x] Add `duplex` to `properties_to_save` — done. (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] 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] `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. - [ ] `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) - [ ] 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)
- [ ] Improve `ae_comp__badge_template_form.svelte` to edit all relevant fields (currently minimal)

View File

@@ -1,843 +1,150 @@
# MODULE: Aether Events — Badges # Aether Events — Badges
**Module Path:** `src/routes/events/[event_id]/(badges)/badges/` **Last Updated:** 2026-06-12
**API Module:** `src/lib/ae_events/ae_events__event_badge.ts`
**Database:** `db_events.badge` (Dexie IndexedDB table) 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.
**Last Updated:** 2026-02-27 (rev 6)
**Related Docs:** `documentation/PROJECT__AE_Events_Badges_Review_Print.md` (implementation guide)
--- ---
## Overview ## Data Model & Hierarchy
The Badges module manages event attendee badges with support for: ### Core Objects
- **External system imports as needed** (CSV/Excel, iMIS, Zoom, Novi, Impexium, Confex, Cvent, and others) - **Event Badge** (`event_badge`): The attendee record containing name, title, affiliations, and tracking flags.
- **Field override protection** to prevent staff/attendee edits from being overwritten by automated syncs - **Badge Template** (`event_badge_template`): The visual and structural configuration for printing (branding, layout, QR placement).
- **Multi-tier access control** for field editing
- **QR code generation** for badge scanning ### Relationships
- **Print tracking** (count, first/last print datetime) - **Badge → Event:** Many-to-one.
- **Advanced search and filtering** - **Badge → Template:** Many-to-one (via `event_badge_template_id`).
- **HTML rendering** in display fields for rich text formatting - **Badge → Person:** Optional link to core Aether Person record for unified profiles.
- **Accessibility features** (text enlargement toggle)
--- ---
## Critical Design Pattern: Override Fields ## Critical Design Pattern: Override Fields
### Purpose ### Purpose
The `*_override` fields pattern protects data from being overwritten during scheduled cron syncs from external systems. This is essential because: 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.
1. Staff may need to correct imported data
2. Attendees may be allowed to self-update certain fields (e.g., preferred name, pronouns)
3. External systems often have outdated or incorrect data
4. Changes should persist across multiple sync cycles
### How It Works ### 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.
**Import Behavior:** ### Standard Override Pairs
```
External System → Aether API → Populates REGULAR fields only
(never touches *_override fields)
```
**Display Behavior:** | Regular Field | Override Field | Editable By | HTML? |
``` |---|---|---|---|
UI Display Logic: | `full_name` | `full_name_override` | Staff, Attendee | ✅ |
1. IF `*_override` field has value → USE IT (highest priority) | `professional_title` | `professional_title_override` | Staff, Attendee | ✅ |
2. ELSE IF regular field has value → USE IT (fallback) | `affiliations` | `affiliations_override` | Staff, Attendee | ✅ |
3. ELSE → Display placeholder/empty | `location` | `location_override` | Staff, Attendee | ✅ |
``` | `email` | `email_override` | Staff Only | No |
| `badge_type` | `badge_type_override` | Staff Only | No |
**HTML Rendering (implemented 2026-02-27):**
Certain fields support HTML markup for rich text formatting. When viewing (not editing), these fields use Svelte's `{@html}` directive to render the markup:
- `full_name` / `full_name_override`
- `professional_title` / `professional_title_override`
- `affiliations` / `affiliations_override`
- `location` / `location_override`
This allows for formatting like:
- Bold/italic: `<b>Dr.</b> Jane Smith` or `<i>Chief Medical Officer</i>`
- Line breaks: `Hospital Name<br>Department Name`
- Special characters and entities
**Example — Full Name:**
```typescript
// API imports from iMIS
badge.given_name = "Robert"
badge.family_name = "Smith"
badge.full_name = "Robert Smith" // Auto-computed
// Staff edits to preferred name with HTML
badge.full_name_override = "<b>Bob</b> Smith"
// Display in UI (review form)
{@html badge.full_name_override || badge.full_name || "— no name —"}
// Result: **Bob** Smith (bold rendered)
// Edit mode shows raw HTML
<input bind:value={editable_full_name_override} />
// Shows: <b>Bob</b> Smith (editable as text)
// Next cron sync from iMIS
// ✅ badge.full_name updated to "Robert J. Smith" (middle initial added)
// ✅ badge.full_name_override remains "<b>Bob</b> Smith" (PROTECTED)
// ✅ Display still shows **Bob** Smith (bold rendered)
```
### Override Fields
| Regular Field | Override Field | Purpose | Editable By | HTML Rendering |
|---|---|---|---|---|
| `pronouns` | `pronouns_override` | Preferred pronouns | Staff, Attendee | No |
| `professional_title` | `professional_title_override` | Job title display | Staff, Attendee | ✅ Yes |
| `full_name` | `full_name_override` | Preferred name display | Staff, Attendee | ✅ Yes |
| `affiliations` | `affiliations_override` | Organization display | Staff, Attendee | ✅ Yes |
| `phone` | `phone_override` | Phone number | Staff, Attendee | No |
| `email` | `email_override` | Contact email override | Staff only | No |
| `location` | `location_override` | City/State/Country display | Staff, Attendee | ✅ Yes |
| `badge_type` | `badge_type_override` | Badge category label text | Staff only | No |
| `badge_type_code` | `badge_type_code_override` | Badge access level code | Staff only | No |
| `registration_type` | `registration_type_override` | Registration category label text | Staff only | No |
| `registration_type_code` | `registration_type_code_override` | Registration category code | Staff only | No |
> **Note:** `phone`, `phone_override`, `pronouns_override`, `registration_type`, `registration_type_code`, `registration_type_override`, `registration_type_code_override` may need to be confirmed against the DB schema via `ae_describe event_badge` and added to `properties_to_save` in `ae_events__event_badge.ts` if not already present.
### Sync Safety Rules
**Automated Sync (Cron Jobs):**
- ✅ CAN update: All regular fields (`given_name`, `family_name`, `email`, `affiliations`, etc.)
- ❌ CANNOT update: Any `*_override` field
- ❌ CANNOT delete: Any `*_override` value
**Manual Staff Edit:**
- ✅ CAN update: Any field (including overrides)
- ✅ CAN clear: Override fields (reverts to regular field)
**Attendee Self-Service Edit:**
- ✅ CAN update: Only specific override fields (per event config)
- ✅ CAN clear: Their own override fields
- ❌ CANNOT edit: Regular fields, badge_type, email_override
--- ---
## External System Integration ## External System Integration
### Supported Import Sources 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."
- **iMIS** (Association Management)
- **Zoom** (Virtual event registration)
- **Novi AMS** (Association Management)
- **Impexium** (Association Management)
- **Confex** (Event abstract management)
- **Cvent** (Event registration)
- **Custom CSV/Excel** imports
### Data Flow Direction ### Supported Sources
``` - **iMIS**, **Novi AMS**, **Impexium** (Associations)
External Systems ─────────> Aether - **Zoom**, **Cvent** (Registrations)
(READ ONLY) (WRITE + DISPLAY) - **Confex** (Abstracts/Presenters)
``` - **Custom CSV/Excel**
**Important:** Aether is **pull-only** — does not push changes back to external systems. This prevents sync conflicts and maintains external systems as the source of truth for base data.
### Sync Behavior
- **Frequency:** Scheduled cron jobs (typically hourly, daily, or on-demand)
- **Method:** Full sync or incremental (depends on external system API)
- **Conflict Resolution:** Override fields always win
**Pseudocode:**
```python
def sync_badge_from_external(external_badge_data, existing_badge):
# Update regular fields from external source
existing_badge.given_name = external_badge_data.first_name
existing_badge.family_name = external_badge_data.last_name
existing_badge.email = external_badge_data.email
existing_badge.affiliations = external_badge_data.organization
existing_badge.badge_type_code = external_badge_data.registration_type
# NEVER TOUCH OVERRIDE FIELDS
# existing_badge.full_name_override ← PROTECTED
# existing_badge.affiliations_override ← PROTECTED
# existing_badge.email_override ← PROTECTED
return existing_badge
```
--- ---
## Access Control & Edit Permissions ## Access Control & Permissions
### Access Levels (Ascending) | Level | Access |
1. **Anonymous** — No access to badges |---|---|
2. **Public** View public event info only (no badge access) | **Public kiosk** | View badge and perform the first print; cannot edit fields without authenticated access. |
3. **Authenticated** — View own badge, limited self-edit | **Authenticated** | Edit fields allowed by the active permission config. |
4. **Trusted** Search all badges, view all, edit own | **Trusted** | Search all badges, view all, and correct names; reprint requires global Edit Mode. |
5. **Administrator** Full CRUD, bulk operations, override any field | **Administrator** | Full CRUD, bulk operations, and override access. |
6. **Manager** All administrator + event configuration | **Manager** | All Administrator capabilities plus Event/Template configuration. |
7. **Super** — All manager + cross-event operations
### Current Implementation (v3) — 2026-02-27 ### Attendee Self-Service (`/review`)
Attendees can access their own record via a passcode-gated link (typically `?passcode=...`).
Editable fields come from `event.mod_badges_json.edit_permissions`, with module defaults as fallback.
#### Badge Search Results Visibility ### Onsite Kiosk (`/print`)
The print controls update the badge preview live. Authenticated field editing is controlled by the
badge template's `cfg_json.controls_cfg` (`shown` and `auth_editable`). Trusted + global Edit Mode
overrides the template config and exposes all controls. This differs from the review page's
event-level permission source; consolidation is an active follow-up.
| Access Level | Sees | ### Review-Link Email
| --- | --- | Email Link actions are placeholders and do not currently send mail. When delivery is implemented,
| Below Trusted (incl. anonymous) | Only badges where `print_count < 1` and not hidden | it must use the imported `event_badge.email` address, never attendee-editable `email_override`.
| Trusted, not Edit Mode | Only badges where `print_count < 1` and not hidden |
| Trusted + Edit Mode | All badges where `hide === false` (including already-printed) |
#### Print Button Behavior (per result row)
| Access Level | Print Action |
| --- | --- |
| Below Trusted | No print action — name shown with User icon, non-interactive |
| Trusted, `print_count < 1` | Clickable link → `/print` page, Printer icon |
| Trusted, `print_count >= 1`, not Edit Mode | Disabled (already printed safety lock), shows `Nx` count |
| Trusted, `print_count >= 1`, Edit Mode | Clickable reprint — shows `Nx` count badge next to icon |
Print count displayed as `[Printer][2×] Name` when `print_count >= 1`.
#### Review Area Buttons (per result row, up to 3 buttons total)
| Button | Visible To | Behavior |
| --- | --- | --- |
| Email Review Link | All users | Placeholder `alert()` — will trigger email API |
| Review Link (clipboard) | Trusted + Edit Mode only | Copies `/review` URL to clipboard; shows `Copied!` feedback |
| *(direct Review link)* | *(future)* | *(not yet implemented as separate nav button)* |
#### Badge Edit Form (`ae_comp__badge_obj_view.svelte`)
**Currently editable fields (local `edit_mode_active`, not global `edit_mode`):**
```typescript
editable_full_name_override: string | null
editable_professional_title_override: string | null
editable_affiliations_override: string | null // textarea
editable_location_override: string | null
editable_allow_tracking: boolean | null
editable_email: string | null
editable_badge_type_code: string | null
```
- Save button → `handle_save_changes()` — only changed fields sent to API
- Cancel button → `handle_cancel_changes()` — reverts to IDB values
- **IMPORTANT:** This component must NEVER write to `$ae_loc.edit_mode` — it uses its own local `edit_mode_active` flag only. (Bug fixed 2026-02-27)
#### Badge Review Form (`ae_comp__badge_review_form.svelte`)
Form-based review (NOT a badge render). Used by the `/review` page.
**Props:**
- `can_edit_fields: string[]` prop controls which fields are editable per user level
- `['*']` = administrator (all fields)
- `is_staff: boolean` prop shows/hides the staff-only fields
- Fields show "(overridden)" label when an override value differs from the base field
**Features (implemented 2026-02-27):**
- **HTML Rendering**: `full_name_override`, `professional_title_override`, `affiliations_override`, and `location_override` fields render HTML markup using `{@html}` directive when viewing (not when editing)
- **Accessibility**: Text enlargement toggle button switches between text-2xl (normal) and text-4xl (enlarged) for improved readability
- **Help Modal**: Flowbite Modal component with 6 help sections (Reviewing Badge, Editing Info, Accessibility, QR Code, Lead Scanning, Assistance)
- **QR Code Display**: Generates QR code using `core_func.js_generate_qr_code()` with badge ID, supports hover zoom and click-to-expand
- **Print Status**: Shows print count, first print datetime, and last print datetime at top of form
- **Local Edit Mode**: Independent `local_edit_active` state (never writes to `$ae_loc.edit_mode`)
- **Save/Cancel**: Only changed fields sent to API; revert button for override fields
**Editable Fields:**
- Pronouns, Full Name, Professional Title, Affiliations, Phone, Location (all with override support)
- Allow Tracking checkbox (lead scanning permission)
- Staff-only: Email, Badge Type, Registration Type, Hide, Priority, Notes
- Staff-only: Options (`other_1_code` through `other_8_code`) and Tickets (`ticket_1_code` through `ticket_8_code`)
- Agree to Terms & Conditions checkbox (attendee-visible when in can_edit_fields)
#### Badge Review Page — Header Buttons (implemented 2026-02-27)
| Button | Visible To | Behavior |
| --- | --- | --- |
| Back → Search (ArrowLeft) | Staff (`has_staff_access`) only | `<a href="/events/{id}/badges">` |
| Print (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | `<a href="/print">`, shows `Nx` count if reprinting |
| Copy Link (clipboard) | Trusted + Edit Mode only | Copies review URL to clipboard; `Copied!` feedback for 2s |
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
#### Badge Print Page — Header Buttons (implemented 2026-02-27)
| Button | Visible To | Behavior |
| --- | --- | --- |
| Back → Search (ArrowLeft) | Always (when badge loaded) | `<a href="/events/{id}/badges">` |
| Print Now (Printer icon) | Trusted+, not printed OR Trusted+Edit if printed | Calls `window.print()` directly (convenience duplicate); print count tracked by component button |
| Review (Eye icon) | Trusted + Edit Mode only | `<a href="/review">` nav link |
| Email Link (Mail icon) | All if not printed; Trusted+Edit if printed | Placeholder `alert()` — email API pending |
#### Badge Review Page — Display Sections (implemented 2026-02-27)
The review form (`ae_comp__badge_review_form.svelte`) displays:
1. **Print status** ✅ — print count + first/last print timestamps (read-only, hidden if never printed)
2. **QR Code** ✅ — the attendee's badge QR code for scanning at the badge kiosk (for automatic badge search + print flow). Generated using `core_func.js_generate_qr_code()` with `obj_type: 'event_badge'` and badge ID. Supports hover zoom overlay and click-to-expand.
3. **Editable Fields** ✅ — all fields with access-level gating, override support, and HTML rendering for display fields
4. **Options** ✅ (`other_1_code` through `other_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Option X` checkmark display only when value exists
5. **Tickets** ✅ (`ticket_1_code` through `ticket_8_code`) — Staff: editable text inputs; Attendees: shown as `[✓] Ticket X` checkmark display only when value exists
6. **Accessibility Toggle** ✅ — Font size enlargement button in sticky header (text-2xl ↔ text-4xl)
7. **Help Modal** ✅ — Attendee guidance modal with 6 sections explaining the review process, editing, QR codes, and lead scanning
#### Default Field Permissions (hardcoded for now — Axonius first show, mid-April 2026)
These are hardcoded in `review/+page.svelte` pending connection to `mod_badges_json.edit_permissions`.
**Attendee (passcode-authenticated / anonymous with link):**
```typescript
[
'pronouns_override',
'full_name_override',
'professional_title_override',
'affiliations_override',
'phone_override',
'location_override',
'allow_tracking', // Exhibitor Leads opt-in
'agree_to_tc', // Terms & Conditions placeholder
]
```
**Trusted Staff and above:**
```typescript
[
'pronouns_override',
'full_name_override',
'professional_title_override',
'affiliations_override',
'email_override',
'phone_override',
'location_override',
'badge_type_code_override', // + badge_type_override (text label)
'registration_type_code_override', // + registration_type_override (text label)
'option_1' ... 'option_8', // i.e. other_1_code ... other_8_code
'ticket_1_code' ... 'ticket_8_code',
'allow_tracking',
'agree_to_tc',
'hide',
'priority',
'notes',
]
```
**Administrator**`can_edit_fields = ['*']` (all fields)
**Badge type options (hardcoded for now):** `member`, `non-member`, `guest`, `exhibitor`, `staff`, `test`
(In future: read from Event Badge Template's configured list)
**Registration type options:** Same list as badge type for now — identical select options.
#### Future: Per-Event Configuration
`event.mod_badges_json.edit_permissions` — placeholder settings UI exists in
`ae_comp__event_settings_badges_form.svelte`. Review page uses hardcoded defaults for now.
The settings form and review page are not yet connected.
```json
{
"authenticated": {
"can_edit": ["pronouns_override", "full_name_override", "professional_title_override", "affiliations_override", "phone_override", "location_override", "allow_tracking", "agree_to_tc"]
},
"trusted": {
"can_edit": ["*attendee_fields", "email_override", "badge_type_code_override", "registration_type_code_override", "option_x", "ticket_x_code", "allow_tracking", "agree_to_tc", "hide", "priority", "notes"]
}
}
```
--- ---
## Search & Filter Capabilities ## Search & Filter Capabilities
### Search Component - **Fulltext Search:** Matches against a consolidated `default_qry_str` (Name, email, IDs).
**File:** `ae_comp__badge_search.svelte` - **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.
- **Advanced Filters (Trusted + Edit Mode):** Badge Type, Printed Status, Affiliations, Sort Order.
### Multi-Word Search Fix (2026-02-26) ### Visibility Filter (Trusted + Edit Mode)
Fulltext search now correctly handles multi-word queries by splitting on whitespace and applying AND logic per word:
```typescript
// "scott idem" → LIKE '%scott%' AND LIKE '%idem%'
// Previously: LIKE '%scott idem%' (failed to match)
const words = qry.split(/\s+/).filter(w => w.length > 0);
for (const word of words) {
search_query.and.push({ field: 'default_qry_str', op: 'like', value: `%${word}%` });
}
```
**Committed:** dc0f3066
### Available Filters Three-option select controlling which records are shown:
**Fulltext Search** (All Users) | Option | Who can set it | Effect |
- Searches: `default_qry_str` database field | --- | --- | --- |
- Includes: Name, email, external IDs | **Default** | Any | Hides hidden and disabled badges |
- Type: `LIKE %query%` (case-insensitive) | **Show Hidden** | Trusted | Shows hidden badges alongside normal ones |
- Trigger: Enter key or 3+ characters typed | **Show Disabled + Hidden** | Manager only | Shows all records regardless of enable/hide flags |
**Advanced Filters** (Trusted Access & Above) ### Result Limit Stepper (Edit Mode)
```typescript
// Badge Type Filter
badge_type_code: 'current_member' | 'inactive_member' | 'ex_all' | 'staff' | etc.
// Note: Badge types are defined per Event and Event Badge Template in database table records.
// Common types include: member, nonmember, guest, exhibitor, staff
// This is a work in progress - types vary by event configuration.
// Print Status Filter Controls the maximum number of results returned. Only visible in edit mode.
qry_printed_status: 'all' | 'printed' | 'not_printed'
// Affiliations Search | Access Level | Range | Step |
qry_affiliations: string // Separate filter for organization search | --- | --- | --- |
| Below Trusted | Fixed 25 | — |
| Trusted | 25 250 | 25 |
| Manager+ | 25 2550 | 25 up to 250, then 100 |
// Sort Options ### Badge Type Filter — Known Limitation
qry_sort_order:
- 'name_asc' / 'name_desc'
- 'updated_desc' / 'updated_asc'
- 'print_count_desc'
- 'print_first_desc' / 'print_last_desc'
- 'badge_type_asc'
- 'affiliations_asc'
```
### QR Scan Search 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.
- Scans badge QR code
- Extracts badge ID
- Auto-fills search with ID
- Jumps to badge detail view
### Search Implementation Pattern
**File:** `badges/+page.svelte` (Lines 117-365)
**Strategy:** Standardized Reactive Search Pattern (Aether UI V3)
1. **Isolate dependencies** into stable `$derived` object
2. **Debounced effect** (300ms) triggers search
3. **Fast Path:** Search IDB first (if not `remote_first`)
4. **Revalidate:** API request updates IDB
5. **LiveQuery:** UI auto-updates from IDB changes
**Search API:** `events_func.search__event_badge()`
```typescript
await search__event_badge({
api_cfg: $ae_api,
event_id: event_id,
fulltext_search_qry_str: qry_str || null,
type_code: type_code || null,
printed_status: printed_status,
affiliations_qry_str: aff_str || null,
order_by_li: order_by_li,
limit: 150,
log_lvl: 0
})
```
--- ---
## Badge Display Logic ## Print Rendering and Tracking
### Name Display Priority - The canonical badge render uses binary-search text fitting for name, title, affiliations, and location.
```typescript - Template `show_qr_front`/`show_qr_back` settings control QR placement.
// Component: ae_comp__badge_obj_li.svelte (Lines 113-121) - Template `style_href` loads event-specific CSS on the print page.
if (event_badge_obj?.full_name_override) - Template `duplex = false` suppresses the badge back for single-sided stock.
display: full_name_override - Chromium PDF proofing requires margins set to None; physical printer paper size remains driver-controlled.
else if (event_badge_obj?.full_name)
display: full_name
else
display: given_name + ' ' + family_name
```
### Badge View Page Aether tracks the lifecycle of every physical badge to prevent unauthorized reprints and monitor kiosk activity.
**Route:** `/events/[event_id]/badges/[badge_id]`
**Components:** | Field | Purpose |
- `+page.svelte` — Container with LiveQuery for badge data |---|---|
- `ae_comp__badge_obj_view.svelte` — Full badge display + edit UI | `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. |
**LiveQueries:** > **Operational Note:** Reprints triggered via the Edit Mode shortcut do not increment the count; only the formal "Print Badge" workflow does.
```typescript
lq__event_badge_obj = liveQuery(() => db_events.badge.get(event_badge_id))
lq__event_badge_template_obj = liveQuery(() =>
db_events.badge_template.get(badge.event_badge_template_id)
)
```
**Loading States:**
- `is_loading_idb` — Waiting for initial IDB lookup
- If badge not found → "Badge Not Found" error with reload button
- Loader spinner while fetching
--- ---
## Badge Templates ## Route Map (Badges)
### Purpose | URL | Purpose |
Badge templates define the visual layout and content structure for printed badges: |---|---|
- Header images/logos | `/events/[id]/badges` | Main search and attendee list. |
- Field positions and font sizes | `/events/[id]/badges/templates` | Badge template management. |
- QR code placement | `/events/[id]/badges/[id]/print` | The actual print-ready render page. |
- Ticket/option indicator display | `/events/[id]/badges/[id]/review` | Attendee-facing self-service form. |
- WiFi credentials display
### Template Selection
Each badge references an `event_badge_template_id`. The template controls:
- Layout (front/back)
- Branding elements
- Which fields to show
- Field formatting rules
### Template Loading
Templates are loaded alongside badges via `inc_template` parameter:
```typescript
load_ae_obj_id__event_badge({
event_badge_id: badge_id,
inc_template: true // Also loads template
})
```
---
## Print Tracking
### Print Fields
```typescript
print_count: number // Increments each print
print_first_datetime: string // ISO datetime of first print
print_last_datetime: string // ISO datetime of most recent print
```
### Print Button (Implemented 2026-02-26)
The `handle_print_badge()` function in `ae_comp__badge_obj_view.svelte` increments the count and records timestamps:
```typescript
async function handle_print_badge() {
const now = new Date().toISOString();
const current_print_count = $lq__event_badge_obj.print_count ?? 0;
const data_to_update = {
print_count: current_print_count + 1,
print_last_datetime: now
};
if (current_print_count === 0) {
data_to_update.print_first_datetime = now; // Only set on first print
}
await events_func.update_ae_obj__event_badge({ ... });
}
```
Button has `data-testid="badge-print-btn"` and shows loading/done/error states with icon feedback.
### Print Workflow
1. **Pre-Print:** Badge print page (`/print`) shows "Already printed N times" warning in screen-only header if `print_count >= 1`
2. **Record:** `handle_print_badge()` updates `print_count`, `print_last_datetime`, and `print_first_datetime` (first print only) via API before printing
3. **Print:** `window.print()` — standard browser print dialog, wired and working (2026-02-27)
4. **Redirect:** After 1 second, `goto(/events/{id}/badges)` returns to search
5. **Audit:** `print_first_datetime` and `print_last_datetime` visible in Edit Mode debug row
**Browser vs Electron:** Badge printing does NOT require the Electron native app. The standard browser print dialog works well across Chrome, Chromium, and Firefox. The Electron native app is specialized for the **Events Pres Mgmt Launcher only** and should not be assumed available for badge stations.
---
## Database Schema
### IndexedDB Table: `badge`
**File:** `src/lib/ae_events/db_events.ts` (Lines 841-852)
**Indexed Fields:**
```typescript
badge: `
event_badge_id, id,
event_id,
full_name, full_name_override, email, email_override,
affiliations, affiliations_override,
badge_type, badge_type_code, badge_type_code_override, badge_type_override,
external_event_id, external_id, external_person_id,
default_qry_str,
alert,
tmp_sort_1, tmp_sort_2,
print_count, print_first_datetime, print_last_datetime,
enable, hide, priority, sort, group, notes, created_on, updated_on
`
```
### Saved Properties
**File:** `ae_events__event_badge.ts` (Lines 495-563)
**Complete field list** (67 fields total):
- Identity: `id`, `event_badge_id`, `event_id`, `event_badge_template_id`
- Name: `pronouns`, `informal_name`, `title_names`, `given_name`, `middle_name`, `family_name`, `designations`
- Professional: `professional_title`, `professional_title_override`
- Display: `full_name`, `full_name_override`
- Organization: `affiliations`, `affiliations_override`
- Contact: `email`, `email_override`
- Address: `address_line_1`, `address_line_2`, `address_line_3`, `city`, `country_subdivision_code`, `state_province`, `state_province_abb`, `postal_code`, `country_alpha_2_code`, `country`, `full_address`
- Location: `location`, `location_override`
- Classification: `badge_type`, `badge_type_code`, `badge_type_override`, `badge_type_code_override`
- External: `external_event_id`, `external_id`, `external_person_id`
- Search: `query_str`, `default_qry_str`
- System: `alert`, `enable`, `hide`, `priority`, `sort`, `group`, `notes`, `created_on`, `updated_on`
- Print: `print_count`, `print_first_datetime`, `print_last_datetime`
- Sorting: `tmp_sort_1`, `tmp_sort_2`
- Person Link: `person_external_id`, `person_external_sys_id`, `person_given_name`, `person_family_name`, `person_full_name`, `person_professional_title`, `person_affiliations`, `person_primary_email`, `person_passcode`
---
## API Functions
### CRUD Operations
**File:** `src/lib/ae_events/ae_events__event_badge.ts`
```typescript
// Load single badge
load_ae_obj_id__event_badge({ event_badge_id, event_id, inc_template })
// Load badge list
load_ae_obj_li__event_badge({ event_id, view, limit, order_by_li })
// Search badges (V3 API)
search__event_badge({
event_id,
fulltext_search_qry_str,
type_code,
printed_status,
affiliations_qry_str,
order_by_li
})
// Create badge
create_ae_obj__event_badge({ event_id, data_kv })
// Update badge
update_ae_obj__event_badge({ event_badge_id, event_id, data_kv })
// Delete badge
delete_ae_obj_id__event_badge({ event_badge_id, event_id, method })
```
### Field Processing
**Function:** `process_ae_obj__event_badge_props()`
**Processing Steps:**
1. Map `*_random` fields to clean names (`event_badge_id_random``event_badge_id`)
2. Set primary `id` field from `event_badge_id`
3. Ensure `event_id` is set (from function parameter if missing)
4. Calculate `tmp_sort_1` and `tmp_sort_2` for efficient sorting
5. Return processed objects
**Critical Fix (2026-02-26):** All CRUD functions now return **processed** data (matches IDB cache) instead of raw API responses. This ensures consistency between function return values and cached data.
---
## Component Architecture
### Route Structure
```
/events/[event_id]/(badges)/badges/
├── +layout.svelte # Layout wrapper (minimal)
├── +page.svelte # Badge list + search
├── ae_comp__badge_search.svelte # Search form + filters
├── ae_comp__badge_obj_li.svelte # Badge list display (results)
├── ae_comp__badge_create_form.svelte # (Not actively used)
├── ae_comp__badge_upload_form.svelte # Bulk CSV upload
└── [badge_id]/
├── ae_comp__badge_obj_view.svelte # Badge rendering + staff edit + print button
├── ae_comp__badge_review_form.svelte # Form-based field review/edit (attendee + staff)
├── print/
│ ├── +page.ts # Non-blocking badge loader (inc_template: true)
│ └── +page.svelte # Print-focused page — screen header + badge render
└── review/
├── +page.ts # Non-blocking badge loader (inc_template: false)
└── +page.svelte # Passcode-gated review page
```
> **Note:** The old `[badge_id]/+page.svelte` placeholder was removed (2026-02-27). The name link in the search results list now goes directly to `/print`.
#### Badge Print Page (`/print`)
- Screen-only header (`print:hidden`): "Back to Search" link + "Already printed N times" warning
- Badge rendered via `ae_comp__badge_obj_view` with `is_review_mode={false}`
- Print button inside `ae_comp__badge_obj_view` handles count update → `window.print()` → redirect to search
- Page `<title>` includes badge name + event name
#### Badge Review Page (`/review`)
- Passcode-gated for attendees — URL `?passcode=...` matched against `badge.person_passcode`
- **Note:** `person_passcode` field is not yet in the DB (as of 2026-02-27). Review page accessible to staff via `trusted_access` without a passcode.
- Access hierarchy (checked in order):
1. Administrator → full access (`can_edit_fields = ['*']`)
2. Trusted Staff → staff field set
3. Attendee with valid passcode → attendee field set
4. No access → passcode entry form shown
- Uses `ae_comp__badge_review_form.svelte` (NOT badge render)
- "Back to Search" link shown for staff only
### Key Components
**Badge List Page** (`+page.svelte`)
- **LiveQuery:** Reactive badge list from IDB
- **Search Pattern:** Debounced search with fast path + revalidation
- **ID List:** `event_badge_id_li` drives LiveQuery
- **Loading State:** Shows spinner when `search_status === 'loading'`
**Badge Search** (`ae_comp__badge_search.svelte`)
- **Form Mode:** Toggle between search form and QR scanner
- **Filters:** Badge type, print status, affiliations, sort order (trusted+ only)
- **Fulltext:** Name/email search (all users)
- **QR Scan:** Integrated QR scanner for badge ID lookup
**Badge List Display** (`ae_comp__badge_obj_li.svelte`)
- **Visibility Filter:** Respects `hide` flag (trusted+ sees all)
- **Display Logic:** Override → regular → fallback pattern
- **Print Indicator:** Green checkmark badge shows `print_count`
- **Metadata:** ID, created/updated timestamps (edit mode only)
**Badge Detail View** (`ae_comp__badge_obj_view.svelte`)
- **Edit Mode:** Activated by edit button (or `#review` URL hash for future self-service)
- **Condition:** Renders only when BOTH `$lq__event_badge_obj` AND `$lq__event_badge_template_obj` are non-null
- **Form Binding:** Direct `bind:value` on editable fields
- **Dynamic Sizing:** Font size adjusts based on text length
- **Print Preview:** Full badge layout with template
- **Save Handler:** Only sends changed fields to API
- **`data-testid` attributes:** `badge-edit-btn`, `badge-save-btn`, `badge-cancel-btn`, `badge-print-btn`, `badge-professional-title-input` — use these in tests
---
## Testing Status
### Current Test Coverage
- ✅ Badge list loads (all 6 data integrity tests passing)
- ✅ Badge template list loads and displays
- ✅ Badge template form renders and populates correctly
- ✅ Badge template values persist in edit form
- ✅ Electron bridge compatibility (graceful degradation in browser)
- ✅ Badge field processor handles missing optional fields
- ✅ Badge type filter tests
- ✅ Badge template relationship tests
-**Attendee workflow test** — navigate → edit professional title → print → return (d1ded2d4)
### Key Test Lessons Learned
**Search API path is FLAT, not nested.** `search_ae_obj` builds `/v3/crud/{obj_type}/search` — always flat regardless of the parent relationship. Mocks must match this:
```typescript
// CORRECT — flat path
url.includes('/v3/crud/event_badge/search') && method === 'POST'
// WRONG — nested path, mock will never fire
url.includes(`/v3/crud/event/${event_id}/event_badge/search`) && method === 'POST'
```
**List API (GET) is also FLAT with query params.** `get_ae_obj_li` builds `/v3/crud/{obj_type}/?for_obj_id=...` — always flat. Mocks must check `url.includes('/v3/crud/event_badge_template/') && url.includes('for_obj_id')`.
**CSS `input[value*=...]` selectors don't work with Svelte bind:value.** The CSS selector checks the HTML *attribute*; Svelte's `bind:value` sets the DOM *property* only. In Playwright tests, use `page.getByLabel()` or `locator.inputValue()` instead.
**Dexie requires `_random` ID fields.** Badge objects saved to IDB must include:
```typescript
event_badge_id_random: string // Must be present or Dexie skips the object
id_random: string // Also checked
// Error: "Object is missing a valid ID for table 'badge'"
```
All API mock responses in tests need these fields.
**Badge view requires both badge AND template.** `ae_comp__badge_obj_view.svelte` wraps everything in `{#if $lq__event_badge_obj && $lq__event_badge_template_obj}` — if the template isn't loaded, edit/print buttons and the badge itself don't render. Tests must mock the badge template endpoint.
**Badge GET endpoint (single object):** `/v3/crud/event_badge/{id}` (NOT nested under event). Matches `api.get_ae_obj()` which uses the flat path.
**Badge PATCH endpoint (update):** `/v3/crud/event/${event_id}/event_badge/${badge_id}` (nested under event). Matches `api.patch_ae_obj()` which uses the nested path.
**Use `data-testid` for test selectors.** Key buttons have targets: `badge-edit-btn`, `badge-save-btn`, `badge-cancel-btn`, `badge-print-btn`, `badge-professional-title-input`.
### Remaining Test Issues
None — all current badge tests passing as of 2026-02-26 (f5e98b8c).
---
## Known Issues & Future Enhancements
### Known Issues
1. **Session Cold-Start:** Potential race condition on first load (same as pres mgmt module)
2. **Type Definitions:** Some pre-existing TypeScript errors on external package types (not introduced by badge work)
3. **`person_passcode` not in DB:** Attendee-gated review URL (`?passcode=...`) cannot function until this field is added to the `event_badge` schema. The review page falls back to passcode entry form for non-staff.
4. **Print page CSS:** Badge print rendering and `@page` print styles not yet fine-tuned — expected to need work
5. **`mod_badges_json.edit_permissions` not connected:** Settings UI exists but review page uses hardcoded field defaults
### Implemented (2026-02-27)
-`window.print()` wired to print button (records count first, then prints, then redirects)
- ✅ Dedicated `/print` page — replaces old `[badge_id]/+page.svelte` placeholder
- ✅ Dedicated `/review` page — passcode-gated, access-tiered
-`ae_comp__badge_review_form.svelte` — stub created, full form fields pending
- ✅ Badge search results visibility rules (unprinted-only for non-edit, all for trusted+edit)
- ✅ Badge list: 4 action buttons per row (Print, Review nav, Copy Link, Email Link) — all Lucide icons
- ✅ Print page: 3 action buttons in header (Print Now, Review nav, Email Link) — all Lucide icons
- ✅ Review page: 3 action buttons in header (Print nav, Copy Link, Email Link) — all Lucide icons
- ✅ Print button: not shown when already printed (unless Edit Mode)
- ✅ Print count shown as `Nx` badge next to printer icon
- ✅ Email obscuring for non-trusted users
- ✅ Email Review Link button (placeholder alert — email API pending)
- ✅ Direct Review Link clipboard copy (trusted + Edit Mode only)
- ✅ Fixed: components no longer write to `$ae_loc.edit_mode`
- ✅ Settings UI for `edit_permissions` per event (`ae_comp__event_settings_badges_form.svelte`)
- ✅ All badge module icons converted to Lucide (Font Awesome removed from badge routes)
### Recently Completed (2026-02-27)
-**Badge Review Form**`ae_comp__badge_review_form.svelte` fully implemented (fields, QR, save/cancel, options/tickets, accessibility, help modal)
-**Print font size controls (v1)** — Screen-only `[]/[+]/[↺]` panel on print page; 4 px props added to `ae_comp__badge_obj_view.svelte`; auto-sizing unchanged when props absent
-**Bug fix**`default_authenticated_fields` / `default_trusted_fields` in `review/+page.svelte` corrected (wrong field names caused silent save drops)
### Still Needed — HIGH PRIORITY (first show: April 2026)
### Still Needed — MEDIUM PRIORITY
1. **Email API for review links:** `send_review_email()` is a placeholder `alert()`. Needs actual email send endpoint.
2. **`person_passcode` DB field:** Add to `event_badge` schema to enable attendee-gated review URLs.
3. **Connect `edit_permissions` config:** Read `mod_badges_json.edit_permissions` in review page instead of hardcoded defaults.
4. **Print page CSS / `@page` styles:** Badge rendering, sizing, and print-specific stylesheet.
### Still Needed — FUTURE / LOW PRIORITY
1. **Batch Operations:** Bulk update, bulk print, bulk export
2. **Audit Log:** Track who edited which fields and when
3. **Photo Badges:** Support badge photo upload and display
4. **Real-Time Sync:** WebSocket updates for multi-device badge printing stations
---
## Development Guidelines
### Adding New Override Fields
1. Add `{field}_override` to database schema
2. Add to `properties_to_save` array in `ae_events__event_badge.ts`
3. Update display logic to check override first
4. Add to editable fields in `ae_comp__badge_obj_view.svelte`
5. Update access control config
6. Document in this file
### Testing Override Fields
```typescript
// Simulate external sync
badge.given_name = "External Value"
// User edits
badge.given_name_override = "User Value"
// Next sync (should NOT change override)
badge.given_name = "Updated External Value"
// Display should still show "User Value"
assert(display === badge.given_name_override)
```
### Debugging Search Issues
```typescript
// Enable search logging
log_lvl: 2
// Check search params object
console.log('Search params:', search_params)
// Verify API request
console.log('API request:', { event_id, fulltext_search_qry_str, type_code })
// Check returned IDs
console.log('Badge IDs:', event_badge_id_li)
// Verify IDB contents
db_events.badge.toArray().then(console.log)
```
--- ---
## Related Documentation ## Related Documentation
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md) 👉 **[MODULE__AE_Events_Badge_Templates.md](./MODULE__AE_Events_Badge_Templates.md)** (Technical reference for layouts)
- [Development Guide](./GUIDE__Development.md) 👉 **[GUIDE__AE_Events_Badges_Onsite.md](./GUIDE__AE_Events_Badges_Onsite.md)** (Hardware & station setup)
- [Events Launcher Native Integration](./PROJECT__AE_Events_Launcher_Native_integration.md) 👉 **[GUIDE__AE_Events_Onsite_Runbook.md](./GUIDE__AE_Events_Onsite_Runbook.md)** (Onsite operational checklists)
- [Naming Conventions](./AE__Naming_Conventions.md)
---
**Document Status:** 🔄 In Progress
**Last Verified:** 2026-02-27 (rev 5 — field permissions spec added, header buttons implemented, review form fields pending)
**Verified Against:** Code as of 2026-02-27 (branch ae_app_3x_llm)

View File

@@ -0,0 +1,81 @@
# Aether Events — Launcher (Podium Display)
**Last Updated:** 2026-06-12
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. |
| **Onsite** | Browser on event network | Faster polling; browser-managed files. |
| **Native** | Electron app on podium Mac | Background pre-cache; atomic file handover. |
For production onsite use, **Native mode on Mac laptops** is the target. The Electron
app pre-caches all session files in the background so presentations open instantly without
a network round-trip at the moment of launch.
---
## Launcher Display Views
| View | Shown When |
|---|---|
| **Session view** | Active session with session-level files. |
| **Presentation view** | Active session with named presentations. |
| **Presenter view** | Presentation selected; shows presenter bio/photo. |
| **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:
👉 **[MODULE__AE_Events_Launcher_Native.md](./MODULE__AE_Events_Launcher_Native.md)**
---
## Route Map (Display)
| URL | Purpose |
|---|---|
| `/events/[id]/launcher` | Launcher home — select location |
| `/events/[id]/launcher/[location_id]` | Launcher display for a specific room |
---
## Access Levels
| Feature | Minimum Access |
|---|---|
| View Launcher display | `authenticated_access` |
| Manual session selection | `trusted_access` |
| Advanced Config / Sync Control | `trusted_access` (via Configuration Drawer) |

View File

@@ -0,0 +1,95 @@
# Aether Events — Launcher Configuration Menu (Inventory)
> **Status:** Current Reference (v3.0)
> **Last Updated:** 2026-06-12
> **Location:** `src/routes/events/[event_id]/(launcher)/launcher_cfg.svelte`
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.
---
## 2. Menu Inventory (Tabbed View)
### Tab 1: Setup (Onsite Operator Focus)
| Section | Feature | Technical Mode Only |
| :--- | :--- | :--- |
| **Display & App Modes** | Session Mode Preset (Oral vs Poster Kiosk) | |
| | Operational Env (Web / App / Onsite) | |
| | Interface Visibility (Hide Header/Menu/Footer/Times) | |
| | Clock Format (12/24 hour) | |
| | WebSocket Debugger Toggle | Yes |
| | Poster Modal Title Toggle | Yes |
| | Native Test Mode (Simulation) | Yes |
| **Remote Controller** | WS Connection Status Badge | |
| | Controller Strategy (Local / Remote / Local Push) | |
| | Connect / Disconnect Action | |
| | Group Reload (WS trigger) | |
| | Channel Group Code (Locked/Unlockable) | Yes |
| **Poster Screen Saver** | Idle Timeout Summary | |
| | Timer Overrides (Idle / Cycle / Loop) | Yes |
### Tab 2: Device (Technical & Native Focus)
| Section | Feature | Technical Mode Only |
| :--- | :--- | :--- |
| **Sync Engine & Timers** | Pause / Resume Sync | |
| | Force Sync Location (Recursive fetch) | |
| | Polling Periods (Event/Device/Loc/Sess/Pres/Presenter) | Yes |
| | Cache Hash Prefix Length (1-3 chars) | Yes |
| **System & Sync Health** | CPU & RAM Usage Gauges | |
| | Heartbeat Status & Timestamp | |
| | Sync Progress (Cached vs Total) | |
| | Active Sync Filename (Animated) | |
| | Hostname & IP List | Yes |
| | Raw Device JSON Inspector | Yes |
| **Native OS Management** | Open Cache / Temp Folders | |
| | Window Control (Maximize / Kiosk) | |
| | Display Mode (Extend / Mirror) | |
| | Presentation Remote (Prev/Start/Stop/Next) | |
| | Reset Wallpaper (Site Header) | Yes |
| | Kill Presentation Apps (PowerPoint/Keynote/etc) | Yes |
| | Power Actions (Reboot / Shutdown) | Yes |
| | Manual Terminal Command Entry | Yes |
| **Wallpaper** | Primary Display URL Preset/Input | |
| | External Display URL Preset/Input | |
| | Save & Apply Wallpaper | |
| | Restore macOS Default | |
| **Launch Timing** | Per-Profile Post-Open Delay (ms) Overrides | Yes |
| **Application Updates** | Update Source (File / URL) | Yes |
| | Check for Updates | |
| | Install & Relaunch | |
### Tab 3: Dev (Technical/Developer Focus)
| Section | Feature | Technical Mode Only |
| :--- | :--- | :--- |
| **Local Reset & Actions** | Maintenance Select (Wipe IDB / LocalStorage) | Yes |
| | Global Sys Menu Toggle | Yes |
| | Global Debug Menu Toggle | Yes |
| | Cache .tmp Cleanup (Native Only) | Yes |
| | API Endpoint & Account ID Summary | Yes |
---
## 3. Global Actions (Footer)
- **Close:** Dismisses the configuration menu.
- **Reload:** Performs a full browser `location.reload()`.
- **Debug Panel:** Opens the raw state inspector (Technical Mode Only).

View File

@@ -1,7 +1,7 @@
# Aether Events Launcher: Native Electron Integration # Aether Events Launcher: Native Integration
> **Status:** Operational / Phase 5 Implementation > **Status:** Operational / Permanent Reference
> **Last Updated:** 2026-05-11 > **Last Updated:** 2026-05-21 (Reorganized)
> **Primary Platform:** macOS (Darwin) > **Primary Platform:** macOS (Darwin)
> **Fallback Platform:** Linux / Windows > **Fallback Platform:** Linux / Windows
@@ -72,10 +72,15 @@ Files are stored persistently using their SHA-256 hash to prevent filename colli
- **Filename:** `{hash}.file` - **Filename:** `{hash}.file`
### 4.2 Background Sync (File Warming) ### 4.2 Background Sync (File Warming)
When a user navigates to a session in the Launcher UI, the `LauncherBackgroundSync` component: 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. Extracts all `event_file_id` values for that session.
2. Checks the native cache via `aetherNative.check_cache`. 1. **Metadata Fetch:** The system fetches all sessions, presentations, and presenters for the current location into the local database (Dexie).
3. Triggers background downloads for missing files via `aetherNative.download_to_cache`. 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) ### 4.3 Safe Handover (Launch Sequence)
When a user clicks "Open", the system follows a non-destructive sequence: When a user clicks "Open", the system follows a non-destructive sequence:

View File

@@ -1,6 +1,7 @@
# Aether Events — Exhibitor Leads Module (v3) # Aether Events — Exhibitor Leads Module (v3)
**Status:** Implemented and ready for demo. Core lead capture flow works end-to-end. **Status:** Implemented and ready for demo. Core lead capture flow works end-to-end.
**Last Updated:** 2026-06-12
**Platform:** PWA only — mobile-first, offline-capable. **Platform:** PWA only — mobile-first, offline-capable.
**Target users:** Conference exhibitors scanning attendee badges at their booths. **Target users:** Conference exhibitors scanning attendee badges at their booths.

View File

@@ -0,0 +1,141 @@
# Aether Events — Presentation Management
**Last Updated:** 2026-06-12
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 |
| `hide__session_code` | `false` | Hide session code column/field |
| `hide__session_description` | `false` | Hide session description field |
| `hide__session_location` | `false` | Hide location field on session view |
| `hide__session_datetime` | `false` | Hide datetime fields |
| `hide__presentation_code` | `false` | Hide presentation code |
| `hide__presenter_code` | `false` | Hide presenter code |
| `hide__location_code` | `false` | Hide location code |
| `show__launcher_link` | `false` | Show direct Launcher link in session view |
| `show__session_qr` | `false` | Show QR code for session (SRR lookup) |
| `show__presenter_qr` | `false` | Show QR code for presenter (SRR lookup) |
| `label__person_external_id` | `null` | Override label for external ID field (e.g., `"Member ID"`) |
| `label__session_poc_name` | `null` | Override label for session POC (e.g., `"Champion"`) |
| `file_purpose_option_kv` | `{}` | Key-value map of file purpose options (e.g., `{"ppt": "PowerPoint", "pdf": "PDF"}`) |
---
## Route Map (Administration)
| URL | Purpose |
|---|---|
| `/events/[id]/pres_mgmt` | Overview — sessions list, search, filter by location |
| `/events/[id]/pres_mgmt/config` | Config editor (admin only) |
| `/events/[id]/session/[session_id]` | Session detail — files, presentations, timing, alert |
| `/events/[id]/presenter/[presenter_id]` | Presenter detail — bio, files, agreement, alert |
| `/events/[id]/location/[location_id]` | Location detail — session schedule for this room, alert |
| `/events/[id]/locations` | All locations list |
| `/events/[id]/reports` | Reports — sessions, presenters, files |
---
## Access Levels
| Feature | Minimum Access |
|---|---|
| View pres_mgmt overview | `authenticated_access` |
| Upload files | `authenticated_access` |
| Edit sessions / presentations | `trusted_access` |
| Edit config | `administrator_access` + `edit_mode` |
| Device management | `administrator_access` |

View File

@@ -1,496 +0,0 @@
# Aether Events — Presentation Management & Launcher
Notes on setup, workflow, configuration, and onsite operation for the Events Presentation
Management module and the companion Launcher (podium display) system.
---
## Overview
The Presentation Management (Pres Mgmt) module handles the full lifecycle of conference
content: sessions, presentations, presenters, presentation files, and room/location
assignments. The Launcher module provides the podium display interface that runs on each
session room's kiosk machine.
These two modules are deployed together — Pres Mgmt is the back office, Launcher is the
front-of-house display. Every client show is at least slightly customized. Some clients
have extensive presenter/presentation data; others just have sessions and files. The
platform is flexible enough to handle the full range.
**Reference clients (current/repeat):**
- **BGH** (Business Group on Health) — most basic setup; session-only, no named Presenters
- **LCI** (Lean Construction Institute) — most complex current setup
- **AAPOR**, **ASCM**, **CMSC** — other active/repeat clients
**Module paths:**
- Pres Mgmt: `/events/[event_id]/pres_mgmt`
- Launcher: `/events/[event_id]/launcher`
**Key source directories:**
- `src/routes/events/[event_id]/(pres_mgmt)/`
- `src/routes/events/[event_id]/(launcher)/`
- `src/lib/ae_events/` — data types and API functions for all event objects
---
## 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 — see note below)
└── 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; the Launcher's primary unit of display |
| Presentation | `event_presentation` | A talk within a session; belongs to exactly one Session |
| Presenter | `event_presenter` | Person linked to exactly one Presentation; optionally linked to `event_person` |
| 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 |
| Event Device | `event_device` | Registered Launcher kiosk (Electron native instance) |
| Event Track | `event_track` | Optional content grouping (see note below) |
### Event Tracks
The API supports Event Tracks — an optional grouping layer above Sessions. Used twice
historically; could have been omitted both times. Tracks may become genuinely useful for
larger events running many parallel Locations where thematic grouping helps navigation.
Not in active use currently and not wired into the standard Pres Mgmt UI workflow.
### Session → Location
The Launcher's primary display unit is the Location. It shows the active Session for that
Location based on `datetime_start` / `datetime_end` or manual selection. A Location hosts
many Sessions over the event's run; typically only one is active at a time.
---
## 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.
Launcher linked from Pres Mgmt views.
The config that drives this is `event.mod_pres_mgmt_json` — see the Configuration section.
---
## Speaker Ready Room (SRR)
The Speaker Ready Room is a dedicated space where presenters check in and staff manage
content before it goes live in the session rooms. Setup varies by client:
- **Small/private:** Only a few client staff and OSIT. Not open to presenters at large.
- **Open SRR:** Open to all presenters as long as sessions are running. People come and go
all day — reviewing silently, editing with a group, practicing at a station.
### SRR Practice Stations
Stations mirror the session room setup exactly:
- Same Mac laptop model and adapter/dongle configuration as the podiums
- Projector and screen (same as session rooms where possible)
- Launcher running in Native (Electron) mode — cached files open immediately
- Full dry-run capability: load their file, start the deck, confirm everything works
### Remote Monitoring
SRR staff typically monitor the session room Launchers in real time via **VNC or RustDesk**.
This lets one person watch multiple podium displays simultaneously without being in each room.
### QR Codes (Session and Presenter)
QR codes are available for Sessions and Presenters and have been useful onsite for quick
lookups — scanning a code takes staff directly to the session or presenter record.
Whether to enable this depends on the SRR flow for each show. It gets toggled on or off
per event via config.
### SRR Staffing Roles
| Role | Access Level | Typical Tasks |
|---|---|---|
| OSIT Staff | `trusted_access` or higher | Upload files, edit sessions/presentations, manage devices, monitor via VNC |
| Client Staff | `authenticated_access` | Upload files, view session list |
| Presenter (self-service) | `authenticated_access` (if enabled) | Upload their own files via QR link |
### SRR Workflow — Day-of-Show
1. **Presenter checks in** — staff looks up their session(s) in Pres Mgmt
2. **File upload** — staff or presenter uploads file to the correct presenter/session record
3. **File verification** — staff opens the file on a practice station to confirm it renders
4. **Launcher sync** — file appears in the Launcher within the next polling cycle
5. **Presenter proceeds to room** — podium kiosk already has the file cached
---
## File Upload Workflows
### Pre-Show (Remote / Staff Ahead of Time)
Files can be uploaded anytime before the event via the Pres Mgmt web UI:
1. Navigate to the presenter, session, or appropriate level
2. Use the file upload panel (drag & drop or browse)
3. File is stored server-side and immediately available to the Launcher
Some clients enable presenter self-upload via a direct link (requires `authenticated_access`).
Controlled per-event via config.
### Day Before — SRR Setup
For higher-volume shows, the SRR opens the day before the event:
- Pre-uploaded files are already loaded and can be verified
- Early-arriving presenters check in; staff upload their files
- Electron Launcher instances on podium Macs begin pre-caching files overnight
- Problems (corrupt files, wrong format, wrong codec) surface with time to fix them
### Live Onsite Upload
For late arrivals and last-minute changes:
1. Presenter arrives at SRR (or sends file via USB/email to staff)
2. Staff uploads via Pres Mgmt web UI
3. File propagates to Launcher within one polling cycle (~30 seconds on fast networks)
4. VNC or RustDesk confirms the podium received the file before the presenter walks in
---
## Onsite Operation — Managing 412 Parallel Rooms
### Overview Page
The Pres Mgmt overview (`/events/[event_id]/pres_mgmt`) shows:
- All sessions, filterable by location and time
- File status per session
- Quick links to each session's file management
For events with multiple parallel rooms, filtering by location and time block is essential
for SRR staff staying on top of what's active right now.
### Per-Room Workflow
Each room/location has its own Launcher display:
- `/events/[event_id]/launcher` → select location → Launcher for that room
- The Launcher shows the active session based on the current time or manual selection
- VNC/RustDesk gives SRR staff a real-time view of all podiums simultaneously
### Session Display Timing
Ideally, sessions would automatically show and hide based on `datetime_start` /
`datetime_end` — appearing a configurable number of minutes before the session starts
and disappearing after it ends. This is a planned/desired behavior. In practice:
- Some clients run tight schedules and could rely on time-based transitions
- Others drift significantly from the published schedule; time-based auto-advance
would cause more problems than it solves
- Currently, session transitions can be managed manually via Launcher controls
> **TODO (future):** Configurable `show_before_minutes` / `hide_after_minutes` per event
> so well-run shows can automate transitions while looser shows stay manual.
### Device (Laptop) Assignment
Each Launcher kiosk Mac is registered as an `event_device` and typically assigned to one
Location for the duration of the event. However, laptops do get moved:
- Venues add or lose rooms as spaces are reconfigured
- A session room may open for one day only
- Devices can be reassigned to a different Location in the `event_device` record as needed
The Electron app reads its location assignment from the API at startup, so reassigning a
device takes effect on the next launch (or app restart).
---
## Alert Fields
Sessions, Presenters, and Locations each have alert fields that can display a visible
notice in the Pres Mgmt UI and/or the Launcher.
Useful for:
- "Presenter requested no recording"
- "Room change — moved to Hall B"
- "File not received — follow up"
- "AV note: needs confidence monitor"
> **Status:** Alert fields exist but the implementation and display behavior needs review
> and cleanup. Not a blocking issue for BGH next week — revisit for a future show.
---
## Launcher Module
### Operational Modes
| Mode | Use Case | File Handling |
|---|---|---|
| **Default** | Browser on any machine | Files downloaded on demand |
| **Onsite** | Browser on event network | Faster polling; browser-managed files |
| **Native** | Electron app on dedicated podium Mac | Background pre-cache; atomic file handover |
For production onsite use, **Native mode on Mac laptops** is the target. The Electron
app pre-caches all session files in the background so presentations open instantly without
a network round-trip at the moment of launch.
### Native Mode — Electron App
- **Repo:** `~/OSIT_dev/aether_app_native_electron/`
- **Platform:** macOS (primary); Linux/Windows as fallback
- **Seed config:** `seed.json` (Device ID + API key) — loaded at startup
- **File cache:** `~/Library/Caches/OSIT/file_cache/` (hashed by SHA-256)
- **Doc:** `documentation/PROJECT__AE_Events_Launcher_Native_integration.md`
The Electron app zero-configs itself:
1. Reads `seed.json` → gets device code
2. Calls Aether API → pulls device config and location assignment
3. Navigates directly to the Launcher for that location
4. Begins pre-caching session files in the background
### Launcher Display Views
| View | Shown When |
|---|---|
| Session view | Active session with session-level files |
| Presentation view | Active session with named presentations |
| Presenter view | Presentation selected; shows presenter bio/photo |
| Poster/group view | Special layout for poster sessions |
| Screensaver | No active session; idle state |
### File Opening (Native Mode) — 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 (Keynote, PowerPoint, Preview, etc.)
**Configurable launch behavior:** The file-open behavior is driven by a Launch Profile, not
just a command string. Profiles are stored per file extension in
`event_device.data_json.launch_profiles` (device-level config) or
`event.launcher.launch_profiles` (event-level fallback). The built-in Svelte defaults are the
final fallback and are documented below. A profile can choose the app, display mode, open
command, and post-open automation. The resolved native template uses `{{path}}` as the file
path placeholder; AppleScript or `shell:` prefixed commands are both supported. No Electron
rebuild is required to change how files open — edit config in Aether and it applies
immediately. See `PROJECT__AE_Events_Launcher_Native_integration.md` Section 8.
### Built-In Default Launch Profiles
These are the initial built-in defaults shipped with the Launcher. They are the Svelte-side
fallbacks used when neither device config nor event config defines a profile for the file
extension. Each canonical profile can have multiple extension aliases. `post_delay_ms` is part
of the profile object, so a device-specific `launch_profiles[profile].post_delay_ms` override can
tune it later without changing the profile table.
| Profile name | Extension aliases | Default app | Display mode | Post delay | Notes |
|---|---|---|---|---|---|
| `powerpoint_mac_extend` | `pptx`, `ppt` | Microsoft PowerPoint for macOS | `extend` | `1000ms` | Open in the presentation app and extend to an external display if one is present. |
| `keynote_mac_extend` | `key` | Keynote | `extend` | `1000ms` | Keynote slideshow on the external display if available. Post-script polls `count of documents > 0` before starting — handles slow file load on cold start. |
| `libreoffice_mac_extend` | `odp` | LibreOffice for macOS | `extend` | `1000ms` | LibreOffice Impress for OpenDocument presentations. |
| `acrobat_mac_mirror` | `pdf` | Adobe Acrobat for macOS | `mirror` | `1000ms` | PDF handout / deck view uses mirrored display. |
| `vlc_mirror` | `mp4`, `mkv`, `mp3`, `m4v`, `m4a`, `webm`, `wav`, `aac`, `flac`, `mov`, `mpeg`, `avi`, `flv`, `ogg`, `ogv`, `wmv` | VLC for macOS | `mirror` | `1000ms` | Media playback is mirrored so the room sees the same output as the operator. Uses `--no-play-and-exit` so playback ends on the last frame instead of closing video output. |
| `powerpoint_win_extend` | `pptxwin`, `pptwin` | PowerPoint for Windows (Parallels) | `extend` | `1500ms` | Windows PowerPoint profile for Parallels-based rooms. Higher base delay to account for Parallels VM startup latency. |
| `libreoffice_win_extend` | `odpwin` | LibreOffice for Windows | `extend` | `1500ms` | Windows LibreOffice profile for Parallels-based rooms. |
| `acrobat_win_mirror` | `pdfwin` | Adobe Acrobat for Windows (Parallels) | `mirror` | `1500ms` | Windows PDF profile for mirrored display rooms. |
| `url_web` | `url` | Browser / Event File web presentation | `extend` | `n/a` | Web-based presentations are handled as Event File URLs rather than cached local files. |
> **Post-open automation timing:** All profiles with a `post_script` use a polling loop rather than a fixed sleep — the AppleScript waits up to ~7.5 s for the target process to become frontmost before firing the automation keystroke. The `post_delay_ms` value is a minimum baseline before polling begins. Overridable per profile via `launch_profiles[profile].post_delay_ms` in `event_device.data_json`.
Versioning is handled automatically: when a presenter uploads an updated file, the new
hash is cached separately and the old one remains intact.
---
## Configuration — `mod_pres_mgmt_json`
The event's Pres Mgmt behavior is controlled by `event.mod_pres_mgmt_json`.
> **Note:** The config schema is being cleaned up — see
> `documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md` for the canonical
> `PressMgmtRemoteCfg` interface and naming conventions.
### 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 |
| `hide__session_code` | `false` | Hide session code column/field |
| `hide__session_description` | `false` | Hide session description field |
| `hide__session_location` | `false` | Hide location field on session view |
| `hide__session_datetime` | `false` | Hide datetime fields |
| `hide__presentation_code` | `false` | Hide presentation code |
| `hide__presenter_code` | `false` | Hide presenter code |
| `hide__location_code` | `false` | Hide location code |
| `show__launcher_link` | `false` | Show direct Launcher link in session view |
| `show__session_qr` | `false` | Show QR code for session (SRR lookup) |
| `show__presenter_qr` | `false` | Show QR code for presenter (SRR lookup) |
| `label__person_external_id` | `null` | Override label for external ID field (e.g., `"Member ID"`) |
| `label__session_poc_name` | `null` | Override label for session POC (e.g., `"Champion"`) |
| `file_purpose_option_kv` | `{}` | Key-value map of file purpose options (e.g., `{"ppt": "PowerPoint", "pdf": "PDF"}`) |
### Per-Show Config Examples
**BGH (session-only, minimal; no named Presentations or Presenters):**
```json
{
"lock_config": false,
"hide__presentation_code": true,
"hide__presenter_code": true
}
```
**LCI (full setup, member ID label, Launcher link enabled):**
```json
{
"lock_config": true,
"label__person_external_id": "LCI Member ID",
"show__launcher_link": true
}
```
> Admin must currently edit `mod_pres_mgmt_json` directly in the DB or via the event
> settings page. A proper Config UI is planned — see `PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`.
---
## Route Map
| URL | Purpose |
|---|---|
| `/events/[id]/pres_mgmt` | Overview — sessions list, search, filter by location |
| `/events/[id]/pres_mgmt/config` | Config editor (admin only) |
| `/events/[id]/session/[session_id]` | Session detail — files, presentations, timing, alert |
| `/events/[id]/presenter/[presenter_id]` | Presenter detail — bio, files, agreement, alert |
| `/events/[id]/location/[location_id]` | Location detail — session schedule for this room, alert |
| `/events/[id]/locations` | All locations list |
| `/events/[id]/reports` | Reports — sessions, presenters, files |
| `/events/[id]/launcher` | Launcher home — select location |
| `/events/[id]/launcher/[location_id]` | Launcher display for a specific room |
---
## Device Management
Each Electron kiosk is registered as an `event_device` record:
- `code` — matches the device's `seed.json` code
- `name` — human-readable (e.g., "Ballroom A Podium")
- `data_json.location_id` — the `event_location_id` this device is assigned to
Devices can be managed in Pres Mgmt (`/events/[id]/device/device`). Location reassignment
takes effect on the next Electron app launch.
---
## Access Levels
| Feature | Minimum Access |
|---|---|
| View pres_mgmt overview | `authenticated_access` |
| Upload files | `authenticated_access` |
| Edit sessions / presentations | `trusted_access` |
| Edit config | `administrator_access` + `edit_mode` |
| View Launcher display | `authenticated_access` |
| Manual session selection in Launcher | `trusted_access` |
| Device management | `administrator_access` |
---
## Pre-Show Checklist
### 12 Weeks Before
- [ ] Event created in Aether with correct dates
- [ ] `mod_pres_mgmt_json` configured for this client's needs
- [ ] Locations (rooms) created and named
- [ ] Sessions created, assigned to locations, datetime ranges set
- [ ] If using Presentations/Presenters: records imported or entered
- [ ] File purpose options configured in `file_purpose_option_kv`
- [ ] Launcher devices registered (`event_device` records with correct codes)
- [ ] Device-to-location assignments confirmed
- [ ] Decide: QR codes for Sessions / Presenters needed? Enable/disable in config
### Day Before (SRR Setup)
- [ ] Mac laptops at podiums booted and Electron app running
- [ ] Each podium confirms it loaded the correct location's Launcher
- [ ] SRR practice stations confirmed — projector, same Mac/dongle setup as session rooms
- [ ] Pre-loaded files verified in Launcher (open at least one per room to test Safe Handover)
- [ ] SRR staff briefed on upload workflow and VNC/RustDesk monitoring setup
- [ ] VNC/RustDesk connections established to all podium displays
### Day of Show
- [ ] Confirm all session times in Aether are accurate before first session
- [ ] Monitor SRR queue — upload files as presenters check in
- [ ] Verify each file opens on a practice station before the presenter walks to their room
- [ ] Monitor podium displays via VNC/RustDesk — flag any stuck or offline devices
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Session not showing in Launcher | Session datetime wrong or location not assigned | Verify session location and datetime range |
| File uploaded but not in Launcher | Polling cycle lag; or file attached at wrong level | Wait one cycle; check that file is attached to session/location (not just a presenter record) if using session-only setup |
| Electron app shows wrong location | Device code mismatch or stale device config | Re-check `event_device` record; restart Electron app |
| File opens slowly at podium | Not in native cache yet | Check background sync in Launcher; pre-cache may not have completed |
| File won't open | Corrupt upload, wrong format, or missing codec on Mac | Test on SRR practice station; re-upload or convert |
| Session out of sync with schedule | Timing drifted; manual advance needed | Use Launcher controls to manually select the current session |
| Alert field not showing | Alert fields need implementation review | Known — lower priority than active operations |
| `lock_config: true` resets local changes | Expected behavior — remote config wins | Change the remote config in `mod_pres_mgmt_json` |
| Device needs to move to different room | Location reassigned mid-event | Update `data_json.location_id` on `event_device` record; restart Electron app on that machine |

View File

@@ -0,0 +1,39 @@
# Aether IDAA — Archives Module
**Status:** Active (private)
**Last Updated:** 2026-06-12
**Routes:** `src/routes/idaa/(idaa)/archives/`
**Underlying library:** `src/lib/ae_archives/`
IDAA Archives provides authenticated access to archival documents and media for the IDAA community.
---
## Core Responsibilities
- List, view, and edit archive records (permission-gated).
- Upload and manage archive content files.
- Render media/content viewers for archived assets.
---
## Security Requirements
- All IDAA archive content is private.
- Auth guard must remain enforced for all archive routes and child views.
- Do not add pre-gate data loading in universal `+page.ts`/`+layout.ts` paths.
---
## Route Map
- `/idaa/archives`
- `/idaa/archives/[archive_id]`
---
## Related Docs
- `documentation/CLIENT__IDAA_and_customized_mods.md`
- `documentation/AE__Permissions_and_Security.md`
- `documentation/REFERENCE__Common_Agent_Mistakes.md`

View File

@@ -0,0 +1,39 @@
# Aether IDAA — Bulletin Board Module
**Status:** Active (private)
**Last Updated:** 2026-06-12
**Routes:** `src/routes/idaa/(idaa)/bb/`
**Underlying library:** `src/lib/ae_posts/`
The IDAA Bulletin Board (BB) is a private community posting and comment system for authenticated IDAA users.
---
## Core Responsibilities
- Post list and post detail flows.
- Comment create/edit workflows.
- Priority/visibility and sort behavior aligned with IDAA privacy and moderation rules.
---
## Security Requirements
- All BB routes are private/authenticated.
- Do not weaken layout-level auth gating.
- Treat any public exposure of BB data as Sev-1.
---
## Route Map
- `/idaa/bb`
- `/idaa/bb/[post_id]`
---
## Related Docs
- `documentation/CLIENT__IDAA_and_customized_mods.md`
- `documentation/AE__Permissions_and_Security.md`
- `documentation/REFERENCE__Common_Agent_Mistakes.md`

View File

@@ -0,0 +1,41 @@
# Aether IDAA — Recovery Meetings Module
**Status:** Active (private)
**Last Updated:** 2026-06-12
**Routes:** `src/routes/idaa/(idaa)/recovery_meetings/`
**Underlying library:** `src/lib/ae_events/` (repurposed)
Recovery Meetings adapts Events module primitives for IDAA meeting discovery, filtering, viewing, and trusted editing.
---
## Core Responsibilities
- Meeting search/list/detail flows (IDB-first with API revalidation).
- Filter and sort controls for member workflows.
- Trusted/staff edit flows for meeting records.
- Modal and direct-page detail/edit entry paths.
---
## Security and Data Integrity
- Module is private/authenticated.
- Keep error state distinct from empty-result state.
- When persisted object shape changes, update `IDB_CONTENT_VERSIONS` wiring/version.
---
## Route Map
- `/idaa/recovery_meetings`
- `/idaa/recovery_meetings/[event_id]`
---
## Related Docs
- `documentation/CLIENT__IDAA_and_customized_mods.md`
- `documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md`
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
- `documentation/REFERENCE__Common_Agent_Mistakes.md`

View File

@@ -0,0 +1,35 @@
# Aether IDAA — Video Conferences Module
**Status:** Active (private)
**Last Updated:** 2026-06-12
**Routes:** `src/routes/idaa/(idaa)/video_conferences/`
Video Conferences provides IDAAs Jitsi meeting access experience within the IDAA private module.
---
## Core Responsibilities
- Render conference links and meeting access UX.
- Support breakout links from Novi iframe context.
- Preserve required URL bootstrap parameters when generating breakout URLs.
---
## Breakout Link Requirement
When opening outside the iframe, ensure required keys (for example site key and Novi identity UUID) remain in the URL so users do not hit access denial flows.
---
## Security Requirements
- Module is private/authenticated.
- Avoid exposing conference route details publicly.
---
## Related Docs
- `documentation/CLIENT__IDAA_and_customized_mods.md`
- `documentation/AE__Permissions_and_Security.md`

View File

@@ -0,0 +1,81 @@
# Aether Journals — Module Overview
**Status:** Active module
**Last Updated:** 2026-06-12
**Library:** `src/lib/ae_journals/`
**Routes:** `src/routes/journals/`
This module manages private personal journals and journal entries with offline-first behavior and Svelte 5 runes patterns.
---
## Core Responsibilities
- Journal and journal-entry CRUD via V3 API wrappers.
- Dexie-backed local cache with liveQuery-driven UI updates.
- Private/passcode-aware access behavior and client-side content encryption.
- Quick Add, Append/Prepend, import/export, and entry auto-save workflows.
- Tabbed module, journal, and entry configuration modals.
---
## Key Data Objects
- `journal`
- `journal_entry`
Common fields and behavior follow Aether object conventions (`code`, `name`, `enable`, `hide`, `priority`, `sort`, `cfg_json`, `data_json`).
---
## Storage and Reactivity
- Local cache: Dexie tables in journals DB layer.
- UI reactivity: Svelte 5 runes (`$state`, `$derived`, `$effect`) plus liveQuery wrappers (`lq__*`, `lqw__*`).
- Persisted module settings: see config map.
Related config map:
- `documentation/MODULE__AE_Journals_Config_Map.md`
---
## Implemented Entry Workflows
- Quick Add creates a plaintext note in a selected journal without opening the full editor.
- Append/Prepend injects timestamped content into an existing entry.
- Bulk import creates entries from parsed files; export supports centralized templates.
- Entry edits support debounced auto-save when `journals_loc.entry.auto_save` is enabled.
- Full entry saves encrypt `content` into `content_encrypted` when the entry's `private`
flag is enabled; disabling `private` clears encrypted content/history fields.
- The non-reactive `decrypt_journal_entry()` helper isolates decryption from Svelte effects.
- Entry configuration exposes Actions, Metadata, Security, and JSON views. Trusted users
can Remove (disable); managers and administrators can hard Delete.
## Current Security Limitations
- `passcode_hash` is editable but is not compared as secondary authentication before
decryption. This remains an active task.
- Quick Add explicitly creates entries with `private: false`; import creates plaintext
content without setting encryption fields. These paths do not currently offer E2EE.
- Successful decryption currently logs a short plaintext preview to the browser console.
Removal is tracked as an active privacy fix.
- Outbound email sharing is not implemented and requires a product/security decision
because journal content is private.
---
## Access and Privacy
Journals contain private personal data. The Journals layout renders module content only when
the user has `user_id`, `person_id`, and `trusted_access`. Treat all journal and journal-entry
routes, API responses, decrypted state, logs, exports, and future sharing features as private.
---
## Related Docs
- `documentation/archive/PROJECT__AE_UI_Journals_Module_Update_2026.md`
- `documentation/TODO__Agents.md`
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`

View File

@@ -1,5 +1,7 @@
# Aether Journals: Configuration & Settings Map # Aether Journals: Configuration & Settings Map
**Last Updated:** 2026-06-12
This document tracks all available settings across the three levels of the Journals module. This document tracks all available settings across the three levels of the Journals module.
## 1. Module Level (Global) ## 1. Module Level (Global)
@@ -51,9 +53,23 @@ This document tracks all available settings across the three levels of the Journ
| `sort` | integer | Manual sort order weight. | Manual (Done) | | `sort` | integer | Manual sort order weight. | Manual (Done) |
| `archive_on` | datetime | Scheduled date for automatic archiving. | Manual (Done) | | `archive_on` | datetime | Scheduled date for automatic archiving. | Manual (Done) |
| `private` | boolean | Trigger for E2EE (Encryption). | Manual (Done) | | `private` | boolean | Trigger for E2EE (Encryption). | Manual (Done) |
| `content_encrypted` | encrypted string | Encrypted entry content written during a full save when `private` is enabled. | Generated on save |
| `history_encrypted` | encrypted string | Encrypted entry history when history encryption is available. | Generated on save |
| `passcode_hash` | string | Entry-level secondary-auth field; comparison logic is not yet implemented. | Manual (Done) |
| `alert` | boolean | Trigger for visual "Alert" state. | Manual (Done) | | `alert` | boolean | Trigger for visual "Alert" state. | Manual (Done) |
| `group` | string | Grouping key for the list view. | Manual (JSON only) | | `group` | string | Grouping key for the list view. | Manual (JSON only) |
## Encryption Behavior and Gaps
1. Full entry saves combine the journal `passcode` and `private_passcode` to encrypt
plaintext content when the entry's `private` flag is enabled.
2. Decryption prefers a passcode typed in the current session, then falls back to the
journal `private_passcode`; the journal `passcode` is combined with that private key.
3. `passcode_hash` secondary-auth comparison is pending and must not be described as enforced.
4. Quick Add currently forces `private: false`, and bulk import creates plaintext entries
without encryption fields. Use the full editor to enable encryption until those workflows
are updated.
## 📐 Data Normalization Rules ## 📐 Data Normalization Rules
To prevent infinite reactivity loops and trivial save cycles, the following normalizations are applied before comparison: To prevent infinite reactivity loops and trivial save cycles, the following normalizations are applied before comparison:
1. **Strings:** Trimmed and `null` treated as `""`. 1. **Strings:** Trimmed and `null` treated as `""`.

View File

@@ -1,8 +1,9 @@
# Project: Pres Mgmt Config Cleanup & Config UI # Project: Pres Mgmt Config Cleanup & Config UI
**Status:** Planning / Ready to Execute **Status:** 🟡 ~70% Complete — Core Working, Cleanup Pass Needed
**Priority:** High (BGH conference in ~2 weeks; only one active event using pres_mgmt) **Priority:** Medium (core features functional; remaining work is deprecation + consistency)
**Created:** 2026-04-02 **Created:** 2026-04-02
**Last Updated:** 2026-06-12 (regression audit)
**Related:** `TODO__Agents.md`, `PROJECT__Stores_Svelte5_Migration.md` **Related:** `TODO__Agents.md`, `PROJECT__Stores_Svelte5_Migration.md`
--- ---
@@ -17,7 +18,7 @@ The `event.mod_pres_mgmt_json` config grew organically across several conference
- Duplicate keys with different names (`file_purpose_option_kv` = `file_purpose_option_li`) - Duplicate keys with different names (`file_purpose_option_kv` = `file_purpose_option_li`)
- Dead config (`HOLD__*` prefix) - Dead config (`HOLD__*` prefix)
- Type inconsistency (`label__person_external_id: false` vs `"LCI member ID"` string) - Type inconsistency (`label__person_external_id: false` vs `"LCI member ID"` string)
- Keys in the DB not consumed by `sync_config__event_pres_mgmt()` - Keys in the DB not consumed by `sync_config__event_pres_mgmt()`
- Bug: `label__session_poc_name_short` is read then immediately overwritten (line 970-972 in ae_events__event.ts) - Bug: `label__session_poc_name_short` is read then immediately overwritten (line 970-972 in ae_events__event.ts)
- `hide_launcher_link` / `hide_launcher_link_legacy` missing the `__` separator (inconsistent) - `hide_launcher_link` / `hide_launcher_link_legacy` missing the `__` separator (inconsistent)
- `show_content__presentation_description` uses a third naming convention - `show_content__presentation_description` uses a third naming convention
@@ -135,7 +136,7 @@ interface PressMgmtRemoteCfg {
## New Svelte 5 Local Store ## New Svelte 5 Local Store
**Do NOT touch `events_loc` or the paused Svelte 5 migration.** **Do NOT touch `events_loc` or the paused Svelte 5 migration.**
Instead, create a standalone store for pres_mgmt local config. Instead, create a standalone store for pres_mgmt local config.
**File:** `src/lib/stores/ae_events_stores__pres_mgmt.svelte.ts` **File:** `src/lib/stores/ae_events_stores__pres_mgmt.svelte.ts`
@@ -162,9 +163,9 @@ AFTER: pres_mgmt_loc.current.hide__session_code
## Config UI Page ## Config UI Page
**Route:** `/events/[event_id]/(pres_mgmt)/pres_mgmt/config/` **Route:** `/events/[event_id]/(pres_mgmt)/pres_mgmt/config/`
**Access:** `$ae_loc.manager_access` only **Access:** `$ae_loc.manager_access` only
**Button visibility:** Edit mode only (`$ae_loc.edit_mode`) **Button visibility:** Edit mode only (`$ae_loc.edit_mode`)
### Page behavior ### Page behavior
- Loads `event.mod_pres_mgmt_json` fresh from API on page open - Loads `event.mod_pres_mgmt_json` fresh from API on page open
@@ -201,15 +202,27 @@ Safe and backward compatible — old DB records fall through to `?? false` defau
## Implementation Steps ## Implementation Steps
- [ ] **Step 1** — Define `PressMgmtRemoteCfg` TypeScript interface (new file or in `ae_events__event.ts`) - [x] **Step 1** — Define `PressMgmtRemoteCfg` TypeScript interface (new file or in `ae_events__event.ts`)
- [ ] **Step 2** — New `ae_events_stores__pres_mgmt.svelte.ts` with `PersistedState`; add version gate to `store_versions.ts` - [x] **Step 2** — New `ae_events_stores__pres_mgmt.svelte.ts` with `PersistedState` ⚠️ **Missing:** version gate in `store_versions.ts`
- [ ] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys and write to the new store - [x] **Step 3** — Rewrite `sync_config__event_pres_mgmt()` in `ae_events__event.ts` to use canonical keys ⚠️ **Issue:** `show__launcher_link_legacy` hard-coded instead of synced from remote
- [ ] **Step 4** — Build config UI page at `(pres_mgmt)/pres_mgmt/config/+page.svelte` (manager_access + edit_mode gated) - [x] **Step 4** — Build config UI page at `(pres_mgmt)/pres_mgmt/config/+page.svelte` (manager_access + edit_mode gated)
- [ ] **Step 5** — Strip `ae_comp__event_settings_pres_mgmt_form.svelte` from settings page (or replace with a link to new page) - [ ] **Step 5** — Strip `ae_comp__event_settings_pres_mgmt_form.svelte` from settings page (or replace with a link to new page) **BLOCKING**
- [ ] **Step 6** — Migrate all `$events_loc.pres_mgmt.*` references in pres_mgmt templates to `pres_mgmt_loc.current.*` - [x] **Step 6** — Migrate all `$events_loc.pres_mgmt.*` references in pres_mgmt templates to `pres_mgmt_loc.current.*`
- [ ] **Step 7** — Update BGH (and any other active events) via new UI - [ ] **Step 7** — Update BGH (and any other active events) via new UI (blocked on Step 5)
- [ ] **Step 8**`npx svelte-check` clean; commit - [ ] **Step 8**`npx svelte-check` clean; commit
### Regression Fixes Needed (2026-06-12 Audit)
- [ ] **Add `show__launcher_link_legacy` to `PressMgmtRemoteCfg`** or remove entirely if deprecated
- Currently hard-coded to `true` in sync function (line 1054 `ae_events__event.ts`)
- Can't be controlled via config UI
- [ ] **Resolve `hide__launcher_link*` local/remote conflict**
- Menu toggles ([ae_comp__events_menu_opts.svelte](../src/routes/events/ae_comp__events_menu_opts.svelte) lines 462-494) use `hide__launcher_link` for LOCAL UI state
- Remote schema uses `show__launcher_link` (inverted)
- Decision: Keep separate? Document clearly? Unify?
- [ ] **Add `AE_PRES_MGMT_LOC_VERSION` to `store_versions.ts`** (Step 2 requirement)
- [ ] **Clean `hide__launcher_link*` from defaults** if truly deprecated (lines 154-155, 333-334 in `pres_mgmt_defaults.ts`)
### Step 6 scope (mechanical find-replace) ### Step 6 scope (mechanical find-replace)
The `$events_loc.pres_mgmt` pattern appears across: The `$events_loc.pres_mgmt` pattern appears across:

View File

@@ -1,8 +1,14 @@
# PROJECT: Site Passcode Security — API-Verified Auth # PROJECT: Site Passcode Security — API-Verified Auth
**Last updated:** 2026-04-10 **Last Updated:** 2026-06-12
**Status:** Backend work in progress — frontend pending backend completion **Last Verified Against Frontend Source:** 2026-06-12
**Priority:** High — passcodes for trusted/administrator access currently in localStorage plaintext **Status:** Active security gap — frontend migration not started
**Priority:** High — passcodes for trusted/administrator access currently remain in localStorage plaintext
The frontend still caches `access_code_kv_json`, compares passcodes locally, and can log the
full passcode map when verbose logging is enabled. No frontend call to `/authenticate_passcode`
or passcode-JWT expiry restoration exists. Backend implementation is documented as completed,
but deployment must be confirmed in the backend repository/environment before frontend cutover.
--- ---
@@ -81,7 +87,11 @@ This gives session expiry without a network call on every page load.
## Backend Changes Required ## 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 status note:** The fixes below were reported implemented and tested in the
`aether_api_fastapi` repository. This frontend-only audit did not verify the backend source or
deployment. Confirm that the deployed `/authenticate_passcode` uses explicit role priority,
returns a complete passcode JWT with `auth_type: 'passcode'`, applies per-role TTLs, and validates
passcode length before starting frontend cutover.
### Backend Agent Follow-Up ### Backend Agent Follow-Up
@@ -316,6 +326,19 @@ async def authenticate_passcode(
--- ---
## Frontend Implementation Status
Verified 2026-06-12:
- [ ] Confirm the corrected backend endpoint is deployed and reachable.
- [ ] Replace local passcode comparison with API verification and JWT storage.
- [ ] Add pending/error UI for passcode authentication.
- [ ] Stop copying `access_code_kv_json` into frontend auth state.
- [ ] Validate passcode JWT expiry during session restoration.
- [ ] Remove `site_access_code_kv` from auth store defaults and types.
- [ ] Remove any logging of passcode maps or entered passcodes.
- [ ] Backend Phase 2: remove `access_code_kv_json` from the public bootstrap model.
## Frontend Changes Required ## Frontend Changes Required
**These depend on the backend fixes above being deployed first.** **These depend on the backend fixes above being deployed first.**

View File

@@ -0,0 +1,88 @@
# Project: Documentation Refresh and Archive Plan (2026)
**Last Updated:** 2026-06-12
**Goal:** Keep onboarding docs fast and current while preserving historical context in archive.
## 1) Naming Standard
Use one of these prefixes consistently:
- `BOOTSTRAP__` for first-read onboarding.
- `GUIDE__` for cross-module technical guides.
- `MODULE__` for module-specific docs.
- `PROJECT__` for active, time-bounded workstreams.
- `PROPOSAL__` for design proposals not yet adopted.
- `REFERENCE__` for evergreen troubleshooting/reference catalogs.
## 2) Module Coverage Baseline (Active)
Minimum one module doc per active module family:
- Events Presentation Management -> `MODULE__AE_Events_Presentation_Management.md`
- Events Launcher -> `MODULE__AE_Events_Launcher.md`
- Events Launcher Native -> `MODULE__AE_Events_Launcher_Native.md`
- Events Badges -> `MODULE__AE_Events_Badges.md`
- Events Leads -> `MODULE__AE_Events_Leads.md`
- Journals -> `MODULE__AE_Journals.md`
- IDAA Archives -> `MODULE__AE_IDAA_Archives.md`
- IDAA Bulletin Board -> `MODULE__AE_IDAA_Bulletin_Board.md`
- IDAA Recovery Meetings -> `MODULE__AE_IDAA_Recovery_Meetings.md`
- IDAA Video Conferences -> `MODULE__AE_IDAA_Video_Conferences.md`
## 3) Archive Strategy
Archive docs that are superseded, duplicate, or no longer operationally used.
Do not delete historical context; move to `documentation/archive/` with clear names.
### Completed in this pass
- Moved `MODULE__AE_Events_Launcher_Config_Menu_new.md` -> `archive/PROPOSAL__AE_Events_Launcher_Config_Menu_Unified_Vision_v3_1.md`.
- Moved `PROPOSAL__IDAA_UI_UX_Roadmap_2026.md` -> `archive/PROPOSAL__IDAA_Recovery_Meetings_UI_UX_Roadmap_2026.md`.
- Renamed `MODULE__AE Journals_config_map.md` -> `MODULE__AE_Journals_Config_Map.md`.
- Renamed `PROJECT__AE_UI_Journals_module_update_2026.md` -> `PROJECT__AE_UI_Journals_Module_Update_2026.md`.
- Renamed `AE_Docker_CI_cache_policy.md` -> `AE__Docker_CI_Cache_Policy.md`.
- Archived `PROJECT__AE_Style_Review.md` -> `archive/PROJECT__AE_Style_Review_2026-03.md` (active style source is `GUIDE__AE_UI_Style_Guidelines.md`).
- Clarified scope split: `GUIDE__AE_Events_Badges_Onsite.md` = deep badge-print reference, `GUIDE__AE_Events_Onsite_Runbook.md` = cross-module onsite operations runbook.
- Validated all `documentation/*.md` references in active docs; no missing targets remain.
- Added ownership and review-trigger metadata to the bootstrap, task list, and docs index.
- Reviewed active project docs for archive eligibility. Object Field Editor and Site Passcode Security remain active and were added to the docs index.
- Archived legacy API-object, component-inventory, data-structure, performance, and UI-pattern references that contradicted V3 IDs, Svelte 5, or current private-route execution rules.
- Refreshed `AE__Architecture.md` and `AE__Naming_Conventions.md` as the active replacements.
- Added `documentation/archive/README.md` to explain archive categories and restoration policy.
- Renamed `AE__Docker_CI_Cache_Policy.md` -> `GUIDE__Docker_CI_Cache_Policy.md`.
- Renamed `AE__UI_UX_future_ideas.md` -> `PROPOSAL__AE_UI_UX_Future_Ideas.md`.
- Audited the Journals UI update against current source and archived
`PROJECT__AE_UI_Journals_Module_Update_2026.md`; remaining security work was moved to
the active task list and module documentation.
- Audited the Badges review/print project against current source and archived
`PROJECT__AE_Events_Badges_Review_Print.md`; email delivery and permission-source
unification remain active follow-ups.
- Audited Site Passcode Security against current source. It remains an active high-priority
project because plaintext client storage and local passcode comparison are still present.
- Audited V3 CRUD upgrade project against source (2026-06-12). All production code migrated
to V3; legacy wrappers remain exported but unused. Archived `PROJECT__Use_AE_API_V3_CRUD_upgrade.md`.
Optional cleanup task added to TODO for removing dead wrapper code.
- Audited Field Editor V3 project against source (2026-06-12). Component complete with 50+ active
usages, GUIDE documentation, and all core field types. Searchable dropdowns deferred as optional
enhancement. Archived `PROJECT__AE_Object_Field_Editor_V3_upgrade.md`.
- Audited Pres Mgmt Config Cleanup project (2026-06-12). Core infrastructure working (~70% complete)
but regressions identified: `show__launcher_link_legacy` missing from schema, old settings form
not removed, local/remote key conflicts. Updated project doc with regression fixes; keeping active.
### Next archive candidates (review + approve)
- Older style-review snapshots once current style guide references are centralized.
- Closed project docs where TODO/archive already capture final status.
- Duplicate docs where one `MODULE__` file and one `PROJECT__` file now overlap heavily.
## 4) Review Cadence
Monthly lightweight review:
1. Verify `BOOTSTRAP__AI_Agent_Quickstart.md` links still resolve and reflect active work.
2. Verify each active module has one current `MODULE__` anchor doc.
3. Move stale proposals and completed projects into archive.
4. Update `REFERENCE__Common_Agent_Mistakes.md` keep/archive sections.
## 5) Immediate Follow-Up Tasks
1. Continue quarterly archive reviews for remaining stale `PROJECT__` docs; the Journals and Badges projects were archived on 2026-06-12, while Site Passcode Security remains active.
2. Continue the broader permission-helper and IDAA authentication review; the Site Passcode section was source-verified on 2026-06-12.
3. Review module docs against current routes and store names rather than relying only on filename/header freshness.
4. Add a lightweight reusable link-check script if manual path validation becomes frequent.

View File

@@ -0,0 +1,267 @@
# PROJECT: IDAA `idaa_loc` Migration to Svelte 5 `PersistedState`
**Last Updated:** 2026-06-12
## Objective
Migrate IDAA persisted local state from legacy `svelte-persisted-store` (`$idaa_loc`) to Svelte 5 `PersistedState` (`idaa_loc.current`) without behavior regressions, auth leaks, or broken page flows.
Primary target store:
- `src/lib/stores/ae_idaa_stores__idaa_loc.svelte.ts` ← new store (created, not yet wired in)
Legacy source currently used by routes:
- `src/lib/stores/ae_idaa_stores.ts` ← remove `idaa_loc` export after migration
## Why This Matters
- Removes coarse-grained reactivity side effects from legacy persisted store access.
- Aligns IDAA with the completed Events store migration pattern.
- Reduces risk of auth-state corruption from broad re-renders triggered by unrelated
writes to the same store key.
## Scope
In scope:
- Replace all `idaa_loc` imports from `ae_idaa_stores.ts` with imports from
`ae_idaa_stores__idaa_loc.svelte.ts`.
- Replace all `$idaa_loc.*` reads/writes with `idaa_loc.current.*`.
- Remove `idaa_loc` export and its `persisted()` definition from `ae_idaa_stores.ts`
after all consumers are migrated.
- Keep `store_versions.ts` wipe call for `ae_idaa_loc` — this cleans old data from
browsers of returning users. Keep it for at least one year post-migration (same rule
as `ae_events_loc`).
Out of scope:
- Migrating `idaa_sess`, `idaa_slct`, `idaa_trig`, `idaa_prom` — in-memory writables,
no coarse-reactivity problem.
- Backend/API changes.
- `ae_loc` migration (separate project).
## Consumer File Inventory
### Files requiring import + `$idaa_loc` → `idaa_loc.current` changes (29 files)
#### IDAA module root layouts (auth-critical — do these first and last)
| File | `$idaa_loc` hits | Notes |
| --- | --- | --- |
| `src/routes/idaa/+layout.svelte` | 3 | Top-level IDAA layout; `/idaa/clear-caches` lives outside this |
| `src/routes/idaa/(idaa)/+layout.svelte` | 40 | Most complex. Novi verification loop, auth escalation, admin/trusted list writes |
#### Bulletin Board (BB)
| File | `$idaa_loc` hits |
| --- | --- |
| `src/routes/idaa/(idaa)/bb/+layout.svelte` | 3 |
| `src/routes/idaa/(idaa)/bb/+page.svelte` | 11 |
| `src/routes/idaa/(idaa)/bb/[post_id]/+page.svelte` | 6 |
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_li.svelte` | 4 |
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte` | 11 |
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_view.svelte` | low |
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte` | 7 |
| `src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte` | 18 |
#### Archives
| File | `$idaa_loc` hits |
| --- | --- |
| `src/routes/idaa/(idaa)/archives/+layout.svelte` | low |
| `src/routes/idaa/(idaa)/archives/+page.svelte` | 6 |
| `src/routes/idaa/(idaa)/archives/ae_idaa_comp__media_player.svelte` | low |
| `src/routes/idaa/(idaa)/archives/[archive_id]/+page.svelte` | 11 |
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_obj_id_edit.svelte` | 4 |
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_obj_id_view.svelte` | 16 |
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_content_obj_id_edit.svelte` | 4 |
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__archive_content_obj_li.svelte` | low |
| `src/routes/idaa/(idaa)/archives/[archive_id]/ae_idaa_comp__modal_media_player.svelte` | low |
#### Recovery Meetings
| File | `$idaa_loc` hits |
| --- | --- |
| `src/routes/idaa/(idaa)/recovery_meetings/+layout.svelte` | low |
| `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` | 37 |
| `src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte` | 10 |
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte` | 41 |
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte` | 8 |
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte` | 12 |
| `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_view.svelte` | low |
#### Other IDAA
| File | `$idaa_loc` hits |
| --- | --- |
| `src/routes/idaa/(idaa)/jitsi_reports/+page.svelte` | low |
| `src/routes/idaa/(idaa)/video_conferences/+page.svelte` | 12 |
### Files that reference `ae_idaa_loc` as a raw string only — NO changes needed
- `src/routes/events/+layout.svelte``localStorage.removeItem('ae_idaa_loc')` (sign-out)
- `src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_local_actions.svelte` — same
- `src/lib/stores/store_versions.ts``_check_and_wipe('ae_idaa_loc', ...)` — keep as-is
## Safety Rules
1. Do this as one atomic migration — no split old/new `idaa_loc` consumers in the same session.
2. Do not loosen any IDAA auth gating — IDAA is private data, always authenticated.
3. Do not move IDAA private data loads into `+page.ts` / `+layout.ts` where prefetch can run.
4. Keep route behavior and permissions exactly as current production behavior.
5. The `(idaa)/+layout.svelte` auth gate is the most sensitive file. Review it by hand after
the mechanical pass — do not rely solely on `svelte-check` to catch auth logic regressions.
## Key Syntax Changes
| Before | After |
| --- | --- |
| `import { idaa_loc } from '$lib/stores/ae_idaa_stores'` | `import { idaa_loc } from '$lib/stores/ae_idaa_stores__idaa_loc.svelte'` |
| `$idaa_loc.novi_uuid` | `idaa_loc.current.novi_uuid` |
| `$idaa_loc.bb.qry__hidden = 'not_hidden'` | `idaa_loc.current.bb.qry__hidden = 'not_hidden'` |
| `$idaa_loc.recovery_meetings.qry__favorites_only` | `idaa_loc.current.recovery_meetings.qry__favorites_only` |
Notes:
- Keep other imports from `ae_idaa_stores` (`idaa_sess`, `idaa_slct`, `idaa_trig`, `idaa_prom`)
unchanged — they stay in that file.
- No `$` sigil — access via `.current` property, not Svelte store subscription.
## Execution Plan
### Phase 0: Baseline Checkpoint
- Ensure clean compile baseline: `npx svelte-check` → 0/0.
- Confirm new store file committed: `ae_idaa_stores__idaa_loc.svelte.ts` ✅ (done 2026-06-11)
Exit criteria:
- `svelte-check` 0 errors / 0 warnings.
### Phase 1: Store Consumer Conversion (Mechanical)
Recommended order — least to most auth-critical, so layouts are done fresh at the end:
#### 1a. Sub-module components (BB, Archives, Recovery Meetings)
All `ae_idaa_comp__*` component files — pure consumers, no auth logic.
#### 1b. Sub-module pages and layouts (BB, Archives, Recovery Meetings)
Page and layout files for the three sub-modules. Sub-layouts (`bb/+layout.svelte`,
`archives/+layout.svelte`, `recovery_meetings/+layout.svelte`) check auth indirectly
but don't own the Novi verification loop.
#### 1c. Jitsi reports + video conferences
Low-hit, isolated pages.
#### 1d. `src/routes/idaa/+layout.svelte` (top-level)
Reads `$idaa_loc` for display only; writes happen in the (idaa) inner layout.
#### 1e. `src/routes/idaa/(idaa)/+layout.svelte` (innermost — do last)
Most complex file (40 hits). Owns the Novi verification loop and all auth escalation writes.
The `$idaa_loc.bb.qry__hidden`, `$idaa_loc.novi_admin_li`, etc. writes all live here.
Review by hand after mechanical pass.
For each file:
1. Update import — change `idaa_loc` source, keep `idaa_sess` / `idaa_slct` / etc. from `ae_idaa_stores`.
2. Replace `$idaa_loc.``idaa_loc.current.` (global find-replace within file).
3. No other logic changes — mechanical pass only.
Exit criteria:
- No remaining `$idaa_loc.` usages in `src/routes/idaa/`.
- No `idaa_loc` imports pointing to `ae_idaa_stores` in route files.
### Phase 2: Store File Cleanup
After all consumers are migrated:
- Remove `idaa_loc` export and `persisted('ae_idaa_loc', idaa_local_data_struct)` from
`ae_idaa_stores.ts`.
- Remove `AE_IDAA_LOC_VERSION` import from `ae_idaa_stores.ts` (no longer needed there).
- Keep `store_versions.ts` unchanged — the `_check_and_wipe` call stays to clean old data.
### Phase 3: Critical Auth Layout Validation
Manually review after conversion:
- `src/routes/idaa/(idaa)/+layout.svelte` — Novi verification `$effect`, auth escalation, sign-out
- `src/routes/idaa/+layout.svelte` — top-level auth gate template
- `src/routes/idaa/(idaa)/bb/+layout.svelte`
- `src/routes/idaa/(idaa)/archives/+layout.svelte`
- `src/routes/idaa/(idaa)/recovery_meetings/+layout.svelte`
Checks:
- Auth gate still blocks unauthenticated users on all sub-routes.
- `novi_verified`, `novi_uuid`, trusted/admin flag writes work correctly.
- No duplicate or skipped verification loops.
- `bb.qry__hidden`, `bb.qry__enabled` reset after Novi verification still fires.
Exit criteria:
- Auth flow matches current production behavior exactly.
### Phase 4: Compile + Search Guards
Run:
```bash
npx svelte-check
grep -rn '\$idaa_loc\.' src/
grep -rn "from '\$lib/stores/ae_idaa_stores'" src/routes/idaa/
```
Exit criteria:
- `svelte-check`: 0 errors / 0 warnings.
- No `$idaa_loc.` references remaining in source.
- No `idaa_loc` imports from `ae_idaa_stores` in route files.
### Phase 5: Test File Updates
The IDAA Novi auth test (`tests/idaa_novi_auth.test.ts`) seeds `ae_idaa_loc` via
`addInitScript`. After migration:
- The seeded structure remains valid (same key, same shape).
- Remove any `ver:` field from the seed if present — `PersistedState` stores don't use it.
- Verify the full nested structure is still seeded (the `bb`, `archives`, `recovery_meetings`
objects must be present — see the "Seed the Full ae_idaa_loc Structure" lesson in `tests/README.md`).
### Phase 6: Runtime Smoke Test
Test flows:
1. Direct navigation to `/idaa/` — auth gate behavior correct.
2. Bulletin Board: list, post view, post edit, comment.
3. Archives: list, archive detail, media player.
4. Recovery Meetings: list, search/filter, favorites toggle, edit form.
5. Video Conferences page loads.
6. Jitsi Reports page loads.
7. Cache clear page (`/idaa/clear-caches`) still clears state and posts message to parent.
8. Sign-out clears `ae_idaa_loc` (the localStorage key name is unchanged, so this works
automatically for any caller using `localStorage.removeItem('ae_idaa_loc')`).
Exit criteria:
- No regressions in primary user paths.
## Risk Register
### R1: Split-brain state
**Risk:** Mixed old/new `idaa_loc` consumers in the same session can lead to inconsistent state.
**Mitigation:** Convert all consumers in one agent pass before committing. Do not commit partial migrations.
### R2: Auth regression in `(idaa)/+layout.svelte`
**Risk:** The Novi verification loop is complex (40 `$idaa_loc` hits). A subtle change to
`$effect` dependency tracking between old and new store access could break or skip verification.
**Mitigation:** Review this file by hand after the mechanical pass. Test auth flow explicitly.
### R3: Nested object merge gap
**Risk:** The `deserialize` function does a shallow spread at the top level only:
`{ ...idaa_loc_defaults, ...JSON.parse(raw) }`. If a new field is added inside `bb`,
`archives`, or `recovery_meetings` after a user has stored data, that field will get
`undefined` rather than its default.
**Mitigation:** This is the same accepted trade-off as the events sub-stores. If a new
nested field is added in the future, add a migration step or accept that the old stored
value takes over wholesale.
### R4: Mechanical typo / missed reference
**Risk:** High replacement count (29 files, ~300+ hits) introduces missed `$idaa_loc.` references.
**Mitigation:** Run the grep guard in Phase 4 before declaring done.
## Rollback Plan
If issues are found after migration:
1. `git revert` the migration commit(s).
2. Re-run `npx svelte-check`.
3. Re-attempt using smaller batches per sub-module (BB only, then Archives, then Recovery Meetings).
## Deliverables
- All 29 consumer files converted from `$idaa_loc.*``idaa_loc.current.*`.
- `idaa_loc` export removed from `ae_idaa_stores.ts`.
- Passing compile check (0/0).
- Smoke-tested all IDAA sub-modules.
## Definition of Done
- Full `idaa_loc` migration complete.
- No auth/privacy regressions.
- `svelte-check` 0/0.
- IDAA smoke-tested (auth gate, BB, Archives, Recovery Meetings, cache clear).

View File

@@ -1,97 +1,122 @@
# Project: Svelte 4 Store → Svelte 5 State Migration # Project: Svelte 4 Store → Svelte 5 State Migration
**Status:** Execution / Phase B (In Progress) **Status:** Events module — COMPLETE. Core / IDAA — In Progress (field cleanup done, PersistedState pending).
**Priority:** High (post-April 2026 conference) **Priority:** High
**Created:** 2026-03-30 **Created:** 2026-03-30
**Related:** `TODO__Agents.md` — [Stores] Svelte 5 State Migration entry **Last Updated:** 2026-06-11
--- ---
## Background ## Background
All core Aether stores (`ae_loc`, `idaa_loc`, `ae_events_loc`, etc.) are being migrated from All core Aether stores were built with `svelte-persisted-store` (Svelte 4 contract). This provides
Svelte 4 stores to Svelte 5 `$state` using the `runed` library's `PersistedState`. This provides coarse reactivity: any write to any field notifies *all* subscribers and re-serializes the entire
fine-grained reactivity, ensuring that effects only re-run when specific fields they access are object. For large stores like `ae_loc` and `ae_events_loc`, this caused unnecessary re-renders and
updated, rather than on every write to the store object. was the root cause of the IDAA "Access Denied" corruption bug (a bootstrap write to `ae_loc` would
overwrite `authenticated_access` if a persisted value was slightly different, corrupting IDAA
member state stored in the same key).
### Phase B Progress & Learnings (Updated 2026-03-30) The migration target: replace all `persisted()` stores with `runed`'s `PersistedState`, which uses
Svelte 5 fine-grained reactivity — a write to one field only triggers effects that read that field.
1. **Dependency Installed**: `runed` is now a project dependency.
2. **Module Resolution Strategy**:
- Core store files renamed: `ae_stores.ts``ae_stores.svelte.ts`, etc.
- **Critical Discovery**: SvelteKit and CLI tools (`svelte-check`) struggled to resolve
extension-less imports (like `$lib/stores/ae_stores`) when only `.svelte.ts` existed.
- **Solution**: Created `.ts` wrapper files (e.g., `src/lib/stores/ae_stores.ts`) that
simply re-export everything via `export * from './ae_stores.svelte'`. This maintains
backward compatibility for all existing import paths without manual updates.
3. **API Confirmation**: Confirmed that `runed`'s `PersistedState` uses `.current` to access
the state object, matching the intended migration syntax.
4. **Mass Replacement**:
- `$ae_loc``ae_loc_v5.current`
- `$idaa_loc``idaa_loc_v5.current`
- `$events_loc``events_loc_v5.current`
- These replacements have been applied across the entire `src/` directory (~2000+ sites).
5. **Import Updates**: A robust Python script was used to surgically add `ae_loc_v5`,
`idaa_loc_v5`, and `events_loc_v5` to existing import blocks, avoiding `import type`
lines and duplicates.
--- ---
## Syntax Changes: Before / After ## Completed: Events Module (2026-06-11)
### Store declaration All `ae_events_stores` sub-modules have been promoted to their own `PersistedState` stores and
`events_loc` (the old `persisted()` store) has been **fully retired**.
```typescript | Store | File | localStorage key | Status |
// BEFORE (ae_stores.svelte.ts) |---|---|---|---|
export const ae_loc: Writable<key_val> = persisted('ae_loc', defaults); | `badges_loc` | `ae_events_stores__badges.svelte.ts` | `ae_badges_loc` | ✅ Done (2026-04-02) |
| `leads_loc` | `ae_events_stores__leads.svelte.ts` | `ae_leads_loc` | ✅ Done (2026-04-03) |
| `pres_mgmt_loc` | `ae_events_stores__pres_mgmt.svelte.ts` | `ae_pres_mgmt_loc` | ✅ Done (2026-04-03) |
| `launcher_loc` | `ae_events_stores__launcher.svelte.ts` | `ae_launcher_loc` | ✅ Done (2026-06-11) |
| `events_auth_loc` | `ae_events_stores__auth.svelte.ts` | `ae_events_auth_loc` | ✅ Done (2026-06-11) |
| `events_loc` | *(retired)* | `ae_events_loc` | ✅ Store removed (2026-06-11) |
// AFTER (ae_stores.svelte.ts) `ae_events_stores.ts` now only exports `events_sess` (in-memory writable, no migration needed),
export const ae_loc_v5 = new PersistedState('ae_loc', defaults); `events_slct`, `events_trig`, `events_trig_kv`, `events_trigger`, and the `EVENTS_MODULE_TITLE`
``` constant. The file no longer imports `svelte-persisted-store`.
### Reading/Writing (Consumers) `store_versions.ts` still calls `_check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION)` to clean
old data out of users' browsers — this is intentional and should be kept for at least one year.
```svelte
<!-- BEFORE -->
{$ae_loc.theme_mode}
{#if $ae_loc.trusted_access}
<!-- AFTER -->
{ae_loc_v5.current.theme_mode}
{#if ae_loc_v5.current.trusted_access}
```
### Batch Update Pattern
```typescript
// Use Object.assign for multi-field updates to maintain fine-grained reactivity
Object.assign(ae_loc_v5.current, { theme_mode: 'dark', edit_mode: true });
```
--- ---
## Implementation Notes ## In Progress / Remaining: `ae_stores.ts` and `ae_idaa_stores.ts`
### Prettier & Formatting Both stores had their unused default properties pruned (2026-06-11), reducing migration scope:
Because the mass migration touches ~250 files, formatting may be inconsistent (especially in
import blocks). It is recommended to run `npm run format` (if available) or rely on standard
linting after the imports are stabilized.
### Batching vs. "Big Sweep" **`ae_loc`** (in `ae_stores.ts`) — still `persisted('ae_loc', ...)`:
While the original plan suggested smaller batches, the global nature of `ae_loc` and the - Remaining fields: auth/identity, theme, permissions, ui config, file upload tracking, query prefs
hundreds of interdependent files made a "Big Sweep" more efficient to ensure the app remains - This is the highest-impact remaining migration — used in nearly every route
in a consistent state. However, verification should still be done module-by-module. - Root cause of IDAA "Access Denied" bug (coarse write during bootstrap stomps on permission fields)
**`idaa_loc`** (in `ae_idaa_stores.ts`) — still `persisted('ae_idaa_loc', ...)`:
- Remaining fields: `novi_uuid/verified/ts`, `novi_admin_li/trusted_li`, `archives/bb/recovery_meetings` sub-objects
- Fields pruned (2026-06-11): `ds`, `idaa_cfg_json`, top-level `qry__*`, `novi_*_base_url`, `novi_rate_limited_until`
--- ---
## Current Status (Pause Point) ## Migration Pattern (Established)
- [x] Phase A: Plan written. Each sub-store follows this pattern, using the `badges` store as the canonical reference:
- [x] Phase B: Core infrastructure setup (runed, renames, wrappers).
- [x] Phase B: Mass variable replacement ($ae_loc -> ae_loc_v5.current).
- [/] Phase B: Mass import update (In Progress/Verifying).
- [ ] Phase B: Final validation (`svelte-check` clean).
**Do NOT stage or commit until `npx svelte-check` is fully verified.** ### 1. Defaults file (`*_defaults.ts`)
The app currently has a high error count due to the transition period where imports are being Define the shape and defaults as a plain TypeScript object (and interface if complex):
re-aligned. Final verification is the next step after the pause.
```ts
export interface BadgesLocState { ... }
export const badges_loc_defaults: BadgesLocState = { ... };
```
### 2. Store file (`*.svelte.ts`)
```ts
import { PersistedState } from 'runed';
import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults, {
serializer: {
serialize: JSON.stringify,
// Merge with defaults so new fields added after first session get their defaults.
deserialize: (raw: string) => ({ ...badges_loc_defaults, ...JSON.parse(raw) })
}
});
```
### 3. Consumer syntax
```ts
// Import (note .svelte extension, not .svelte.ts):
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
// Read:
badges_loc.current.fulltext_search_qry_str
// Write (fine-grained — only triggers effects that read this field):
badges_loc.current.fulltext_search_qry_str = 'hello';
// Bulk reset:
badges_loc.current = { ...badges_loc_defaults };
```
### Key differences from old `svelte-persisted-store` pattern:
| | Old (`persisted()`) | New (`PersistedState`) |
|---|---|---|
| Import | `import { events_loc } from '...ae_events_stores'` | `import { badges_loc } from '...ae_events_stores__badges.svelte'` |
| Read | `$events_loc.badges.field` | `badges_loc.current.field` |
| Write | `$events_loc.badges.field = x` | `badges_loc.current.field = x` |
| Reactivity | Coarse — entire store notified | Fine-grained — only affected fields |
| In `$effect` | Subscribes to entire store | Only subscribes to fields you read |
---
## Next Steps
1. **`idaa_loc` → PersistedState** — Highest priority for IDAA stability. Promotes `novi_uuid/verified`,
`archives`, `bb`, `recovery_meetings` sub-objects to their own stores following the same pattern.
Primary benefit: eliminates the IDAA "Access Denied" corruption from `ae_loc` bootstrap writes.
2. **`ae_loc` → PersistedState** — Largest scope (~every route in the app). Defer until after
`idaa_loc` is done. Consider extracting `auth_loc` (the identity/permission fields) as the
first sub-store since those are the fields implicated in the IDAA corruption bug.

View File

@@ -2,7 +2,8 @@
> Collection of concrete UX improvements for the Aether frontend. Each entry includes > Collection of concrete UX improvements for the Aether frontend. Each entry includes
> the rationale, current behavior, proposed change, and implementation notes. > the rationale, current behavior, proposed change, and implementation notes.
> **Date:** 2026-05-17 > **Created:** 2026-05-17
> **Last Updated:** 2026-05-18
--- ---

View File

@@ -0,0 +1,68 @@
# Aether SvelteKit — Documentation Index
**Doc Owner:** Frontend platform maintainers (OSIT)
**Review Trigger:** Update whenever a documentation file is added, renamed, archived, or restored.
Use this file as the routing map for project documentation.
## 1) First Read
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
- `documentation/TODO__Agents.md`
## 2) Core Guides
- `documentation/GUIDE__Development.md`
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
- `documentation/GUIDE__AE_API_V3_for_Frontend_websockets.md`
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
- `documentation/GUIDE__AE_UI_Style_Guidelines.md`
- `documentation/GUIDE__Docker_CI_Cache_Policy.md`
## 3) Safety and Reference
- `documentation/AE__Architecture.md`
- `documentation/AE__Permissions_and_Security.md`
- `documentation/REFERENCE__Common_Agent_Mistakes.md`
- `documentation/AE__Naming_Conventions.md`
## 4) Module Docs (Active)
### Events
- `documentation/MODULE__AE_Events_Presentation_Management.md`
- `documentation/MODULE__AE_Events_Launcher.md`
- `documentation/MODULE__AE_Events_Launcher_Native.md`
- `documentation/MODULE__AE_Events_Launcher_Config_Menu.md`
- `documentation/MODULE__AE_Events_Badges.md`
- `documentation/MODULE__AE_Events_Badge_Templates.md`
- `documentation/MODULE__AE_Events_Leads.md`
### Journals
- `documentation/MODULE__AE_Journals.md`
- `documentation/MODULE__AE_Journals_Config_Map.md`
### IDAA
- `documentation/CLIENT__IDAA_and_customized_mods.md`
- `documentation/MODULE__AE_IDAA_Archives.md`
- `documentation/MODULE__AE_IDAA_Bulletin_Board.md`
- `documentation/MODULE__AE_IDAA_Recovery_Meetings.md`
- `documentation/MODULE__AE_IDAA_Video_Conferences.md`
## 5) Active Projects
- `documentation/PROJECT__Documentation_Refresh_and_Archive_Plan_2026.md`
- `documentation/PROJECT__Stores_Svelte5_Migration.md`
- `documentation/PROJECT__IDAA_Stores_Svelte5_Migration_2026.md`
- `documentation/PROJECT__AE_Events_PressMgmt_Config_Cleanup.md`
- `documentation/PROJECT__AE_Site_Passcode_Security.md`
## 6) Active Proposals
- `documentation/PROPOSAL__AE_UI_UX_Future_Ideas.md`
## 7) Archive
- `documentation/archive/README.md`
Archive contains completed historical project notes and superseded proposals.
Legacy API-object, component-inventory, data-structure, performance, and UI-pattern snapshots are archived because they describe pre-V3 or pre-runes behavior. Use the active V3, Dexie, style, architecture, and module docs instead.

View File

@@ -0,0 +1,171 @@
# Aether SvelteKit — Common Agent Mistakes (Reference)
This is the detailed mistake catalog referenced by `BOOTSTRAP__AI_Agent_Quickstart.md`.
Use it as a troubleshooting and prevention guide, not as first-pass onboarding.
Review policy: balanced curation.
- Keep: high-impact or recurring mistakes.
- Archive: one-off or lower-signal historical incidents.
---
## Active Mistakes (Curated)
### 1) IDAA content exposed publicly
**Impact:** Sev-1 privacy breach.
**What happened:** A route guard was removed on an IDAA BB route.
**Rule:** All `/idaa/` content is private. Never remove auth guards on IDAA routes.
**Verify:** Confirm route/layout auth checks still gate all IDAA pages before data load.
### 2) Object ID included in PATCH body (`data_kv`)
**Impact:** 400 errors (`Unknown column in SET`).
**What happened:** Object ID (for URL path) was also sent in `data_kv`.
**Rule:** Keep object ID in the URL only; `data_kv` contains changed fields only.
**Verify:** In `update_ae_obj__*` calls, confirm `data_kv` excludes `*_id` fields.
### 3) Coarse-store `$effect` reactivity loops
**Impact:** repeated fetches, duplicate side effects, noisy state churn.
**What happened:** `$effect` read fields from `svelte-persisted-store` stores (`$ae_loc`, `$idaa_loc`), subscribing to entire store.
**Rule:** Be minimal about coarse-store reads in `$effect`; move transient triggers to session state and keep duplicate guards in executor paths.
**Verify:** Check effect dependencies and ensure unrelated writes do not retrigger critical effects.
### 4) Dexie `.get()` used with string object ID
**Impact:** false misses (`undefined`) despite cached data.
**What happened:** `.get()` queried primary key `id`, but V3 records rely on object string IDs (e.g. `person_id`).
**Rule:** Use `.where('<obj_id>').equals(value).first()` for object lookups.
**Verify:** Search for `.get(` against object IDs and replace with indexed `where` query.
### 5) Misunderstanding `$effect` and auth gates
**Impact:** security confusion and wrong mitigations.
**What happened:** Child-component `$effect` blocks were treated as possible bypass vectors.
**Rule:** Child effects cannot bypass parent layout auth gates; pre-gate risk is in universal `+page.ts` / `+layout.ts` loads and prefetch.
**Verify:** Keep private-module data loads out of universal load files unless explicitly authenticated.
### 6) Using query `key` like `x-no-account-id: bypass`
**Impact:** dropped `x-account-id`, unexpected 403 on scoped requests.
**What happened:** `key` was treated as bypass intent and account context was stripped.
**Rule:** `key` is business data, not bypass intent. Only explicit `x-no-account-id: bypass` drops account context.
**Verify:** Check request headers for account-scoped calls and preserve `x-account-id` unless bypass is explicitly required.
### 7) Pre-stringifying `*_json` before API wrappers
**Impact:** double encoding risk and inconsistent payloads.
**What happened:** Callers stringified JSON fields manually before wrapper serialization.
**Rule:** Pass plain objects for `*_json` fields; wrappers handle serialization.
**Verify:** Remove `JSON.stringify(...)` around `cfg_json`, `data_json`, and related fields before wrapper calls.
### 8) Broad Dexie result windows silently clipped
**Impact:** “All” views show fewer rows than narrower filters.
**What happened:** page limits or API revalidation replaced local broad result sets.
**Rule:** For empty text search, local IDB result set should drive visible results; server refresh updates cache without shrinking display.
**Verify:** Compare empty-search “All” view vs narrower filter counts and inspect revalidation merge behavior.
### 9) Missing `IDB_CONTENT_VERSIONS` bump after `properties_to_save` changes
**Impact:** long-lived stale cache bugs (e.g. IDAA “no meetings found”).
**What happened:** shape persisted in IDB changed, but table content version was not bumped.
**Rule:** When persisted object shape/behavior changes, bump matching `IDB_CONTENT_VERSIONS` entry in `src/lib/stores/store_versions.ts`.
**Verify:** For relevant object changes, confirm both version increment and table-wiring path are in the same change set.
### 10) API retry loop broken by returning transient errors
**Impact:** no retries on common WiFi/network blips.
**What happened:** transient error paths returned `false` instead of throwing, bypassing retry loop.
**Rule:** Keep retry classification strict:
- transient `TypeError` and timeout aborts -> `throw`
- navigation abort -> `return false`
- deterministic 4xx -> `return false`
- 5xx -> `throw`
**Verify:** Simulate transient network failure and confirm retry/backoff attempts occur.
### 11) Account-scoped trigger fired before bootstrap account is ready
**Impact:** wrong-account API fetch and cached cross-account data.
**What happened:** trigger effects ran before account bootstrap settled, reading stale context.
**Rule:** Gate account-scoped load triggers on `$slct.account_id` readiness, not persisted `$ae_loc.account_id`.
**Verify:** Ensure trigger effects require browser + account_id + api readiness before fetch trigger assignment.
### 12) `tmp_sort_*` comparator direction inverted
**Impact:** priority ordering reversed.
**What happened:** descending comparator used with `build_tmp_sort` encoding (which expects ascending).
**Rule:** Use ascending compare for `build_tmp_sort` outputs, with documented exceptions for legacy encodings.
**Verify:** Confirm comparator direction per module encoding and avoid `collection.reverse().sortBy(...)` assumptions.
### 13) `$` sigil used on plain prop values
**Impact:** runtime `store_invalid_shape` errors.
**What happened:** child component treated normal prop values as Svelte stores.
**Rule:** In Svelte 5, props passed as values are plain values. Do not use `$prop` unless prop is an actual store.
**Verify:** In migrated components, replace `$lq__...` prop reads with plain `lq__...` prop access.
### 14) Null JSON blob fields not guarded
**Impact:** read/write crashes (`cannot access property of null`).
**What happened:** `*_json` / `*_kv_json` DB columns were null before first write.
**Rule:** Use optional chaining for reads and `?? {}` initialization before object spread writes.
**Verify:** Audit reads/writes for `cfg_json`, `data_json`, `poc_kv_json`, and similar nullable blob fields.
### 15) Service worker stale-tab behavior misunderstood
**Impact:** users run old code longer than expected, “cant reproduce” bug reports.
**What happened:** deployment assumptions ignored SW activation lifecycle.
**Rule:** Keep SW activation behavior explicit (`skipWaiting`, `clients.claim`) and evaluate trade-offs for session-heavy flows.
**Verify:** After deploy, validate that long-lived tabs pick up new SW behavior as intended.
---
## Archived Historical Items (Pruned)
These are retained as project memory but removed from the active mistake list because they are lower-signal or one-off.
1. **Bad `.d.ts` module declaration masking errors** (important incident, low recurrence recently).
2. **Launcher `file_purpose == 'admin'` filtering gap** (specific historical enum-audit miss).
3. **Deleting files with `rm`** (still a workflow rule, now maintained in bootstrap File Safety section rather than as a mistake entry).
---
## Related Docs
- `documentation/BOOTSTRAP__AI_Agent_Quickstart.md`
- `documentation/TODO__Agents.md`
- `documentation/GUIDE__SvelteKit2_Svelte5_DexieJS.md`
- `documentation/GUIDE__AE_API_V3_for_Frontend.md`
- `documentation/PROJECT__Stores_Svelte5_Migration.md`

View File

@@ -1,438 +1,197 @@
# Frontend Agent Task List # Frontend Agent Task List
> **Doc Owner:** Active frontend implementation team (human + agent)
> **Review Trigger:** Update when work starts, completes, changes priority, or moves to an archive.
> Use this file to track steps for complex features or bug fixes. > Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development. > **Status:** Stable — ongoing development.
> **Scope:** Active/open work only. Completed detail lives in archive files.
## 🔴 LCI October — Pres Mgmt Restoration (in progress 2026-06-12)
These features regressed over the last 6 months and must be working before the LCI conference.
Reference commit for original working implementation: `bb993a102`.
### Session POC (Champion/Moderator) — `session_view.svelte`
**Root cause of visible bugs:** The POC section is placed *below* the session hero card as a
separate disconnected block. In the original it was part of a structured `<ul>` with the session
name, code, datetime, location, and description all together. The current layout looks and feels
wrong to users.
- [x] **[Pres Mgmt] POC section — move inside session hero card** (2026-06-12)
Restructured hero card as a `<ul>` with datetime, room, and POC as rows inside the card.
Session name and code are now always visible (not just in edit_mode — that was a bug).
- [x] **[Pres Mgmt] POC assignment — "Select Person" flow broken** (2026-06-12)
Gated the select editor on `person_options_loaded` (`Object.keys($slct.person_obj_kv).length > 0`).
"Select Person" button renders as "Reload Person" after list is loaded.
- [x] **[Pres Mgmt] Email Session POC sign-in link — UI missing** (2026-06-12)
Restored email button in POC row with `sending/sent/error` state feedback.
Shown when `require__session_agree && show__email_access_link && poc_person_primary_email`.
- [x] **[Pres Mgmt] Copy Session POC access link — UI missing from session view** (2026-06-12)
Restored inline `MyClipboard` copy button in POC row for trusted staff.
Shown when `show__copy_access_link && trusted_access && poc_sign_in_url`.
### Presenter Sign-In
- [ ] **[Pres Mgmt] Presenter email sign-in link routes to wrong page**
`email_sign_in__event_presenter()` builds a URL to `/presenter/[id]?person_id=...&person_pass=...`.
The URL param parser (`sign_in_out.svelte`) is only mounted on the *session* page menu, not the
presenter page. A presenter clicking their email link lands on their page with no auth granted.
Fix: mount `Sign_in_out` in `presenter_page_menu.svelte` (same way session menu does it), or
change the email link to route to the session page (which already has the parser) and include
the presenter/presentation IDs as params — which is how it worked originally.
- [ ] **[Pres Mgmt] Presenter agreement not enforced before file upload**
`require__presenter_agree` is stored and displayed but the upload components are gated on
`auth__kv.presenter[id]` only, not on `presenter.agree`. A presenter who signs in but has not
agreed can still upload. The original blocked the upload section until `agree === true`.
### Session POC Sign-In
- [ ] **[Pres Mgmt] `session_page_menu.svelte` sign-in prop still wrong**
`event_session_id` prop passed to `Sign_in_out` was just changed from `event_id` to
`event_session_id` — verify this is actually `$lq__event_session_obj?.event_session_id`
(the real session ID string) not the URL param `url_session_id`. The sign-in component
uses this value to set `auth__kv.session[event_session_id]`.
---
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
**[Electron/Launcher] Clean up presentation file launch profiles** — BGH show revealed issues - [ ] **[Launcher/Electron] Wallpaper reliability (post-CMSC)**
with the profiles used to open/launch presentation files. Architecture decision: move launch - [ ] Use timestamp/randomized temp filename so macOS always sees a new path.
logic to the Svelte side so it can be changed without an Electron rebuild. Electron becomes a - [ ] Add resilient reconciliation loop or event-driven reapply on display topology changes.
thin OS primitive layer; all business logic lives in Svelte and device config.
Why this matters: the profile map is policy, while the native template is the exact runtime
command. Keeping those separate avoids a second hidden source of truth and keeps Electron from
guessing defaults.
**Electron groundwork (2026-05-11) — DONE:**
- [x] `run_osascript` hardened — temp `.scpt` file approach; handles multi-line + special chars
- [x] `native:copy-from-cache-to-temp` primitive added — copy to tmp, caller decides launch
- [x] `native:launch-from-cache` executes a provided `native_template` string — AppleScript or `shell:` prefix; no Electron-side fallback
- [x] `get_launch_profile()` in `launcher_file_cont.svelte` reads from device config then event config; resolves to a `native_template` string and passes it to `launch_from_cache`
- [x] Built-in Launcher defaults refactored into canonical profile names plus extension aliases (`ae_launcher__default_launch_profiles.ts`)
- [x] Device-level Launch Timing section added under Launcher Configuration → Device, with per-profile `launch_profiles[profile].post_delay_ms` overrides
- [x] **URL file launch support (2026-05-13)**`event_file.extension = 'URL'` (or filename starting with `http://`/`https://`) is treated as a non-downloaded URL. Background sync skips URL files so they are never treated as cacheable hosted files. Shared `AE_Comp_Hosted_Files_Download_Button` now hard-bypasses the download path for URL records. In native mode, `handle_open_file()` routes to `native.open_external({ url, app: 'chrome' })` with `'default'` fallback. Health section crash fixed (guarded `native_device` against undefined in preview/edit mode).
- [x] **[Launcher/Electron] Display mirroring auto-detection (2026-05-20)** — `native:set-display-layout` rewrote to auto-detect displays via `displayplacer list` when no `configStr` provided. Old code silently returned `{ success: false }` (swallowed by `.catch(() => {})` in relay). Now parses `displayplacer list` output, extracts quoted display strings, builds correct mirror/extend commands. Manual `configStr` still takes priority when provided.
**Svelte-side migration — remaining before May 26:**
- [x] **[Launcher] Built-in Svelte default profiles (2026-05-11)** — canonical profile constants live in `ae_launcher__default_launch_profiles.ts` with extension aliases and a `resolve_launch_profile()` 3-step fallback (device config → event config → built-in defaults). Covers macOS (`pptx`, `ppt`, `key`, `odp`, `pdf`), media (`mp4`, `mkv`, `mp3`, etc.), Windows/Parallels variants, and URL path.
- [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use
`copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one
`launch_from_cache`. Finer error handling at each step (verify copy succeeded before
attempting script; surface failure clearly in UI).
- [x] **[Launcher] Error handling + fallback (2026-05-14)** — post-script failure is non-fatal: surfaces `'fallback'` status with error detail (file already open). If `open_cmd` itself fails, falls back to `open_local_file_v2` (OS default); surfaces combined error detail if that also fails. UI renders `'open'` / `'fallback'` / `'error'` states distinctly.
- [ ] **[Launcher] Slide control scripts in Svelte config** — `control_presentation` AppleScript
one-liners are hardcoded in Electron (`shell_handlers.ts` lines 149159). The Svelte side
(`launcher_cfg_native_os.svelte`) already calls `native.control_presentation({ app, action })`
with next/prev/start/stop buttons wired and working. The Electron IPC call is stable enough
for current shows.
**Remaining (post June 10):** Move scripts to device config (`data_json.control_scripts`) or
Svelte constants so behavior can be changed without an Electron rebuild. Replace
`native.control_presentation()` call with `native.run_osascript(script)` using the
config-resolved script string. The `run_osascript` primitive is already in place; this is
purely a wiring/config task. Low priority — hardcoded scripts work fine for PowerPoint and
Keynote for now.
- [ ] **[Launcher] `kill_processes` target list in config** — The `kill_processes` IPC primitive
exists in Electron and is exposed via `electron_relay.ts`, but **is never called from anywhere
in Svelte** — no routes, no config components, no file launch flow. No UI exists for it yet.
**What needs doing (post June 10):**
- Decide where the "kill on cleanup" action lives: end of `handle_open_file()` (auto), or a
manual "Kill Apps" button in Native OS config, or both.
- Process names should come from device config (`data_json.kill_process_li`) with a built-in
fallback list (e.g. `['Microsoft PowerPoint', 'Keynote', 'LibreOffice Impress']`).
- The Native OS config section (`launcher_cfg_native_os.svelte`) is the natural home for a
manual kill button; auto-cleanup on file open would go in `launcher_file_cont.svelte`.
**Device tab length note:** The Device tab now has 6 collapsible sections (Sync Timers, Health,
Native OS, Wallpaper, Launch Timing, Updates). Consider moving Launch Timing + a future Kill
Apps control into a dedicated **"Presentation"** section or a fourth tab to keep Device focused
on machine/OS concerns rather than per-app launch behavior.
- [x] **[Launcher] Launcher config UI — launch_profiles editor (2026-05-14)** — Launch Timing section in `launcher_cfg_launch_timing.svelte` exposes per-profile `post_delay_ms` overrides with Save/Reset per row. PATCHes `event_device.other_json.launcher.launch_profiles` via V3 CRUD. Wired into `launcher_cfg.svelte`. Shown under Technical Mode (`$ae_loc.edit_mode`).
- [ ] **[Launcher] End-to-end test on macOS** — test pptx and key opens on a real podium Mac
before May 26 setup day. Verify: file copies to tmp correctly, script fires, app opens in
slideshow mode, error fallback works.
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
After setting the wallpaper 35 times in a session, macOS silently ignores further `set desktop
picture` calls even though the SvelteKit side reports "Saved & Applied ✓". Restore Default
(`restore_macos_default_wallpaper`) immediately unblocks it; closing/reopening Electron does
not. **Workaround:** use Restore Default, then re-apply. **Root cause:** macOS caches the
current wallpaper path and skips the AppleScript call when the downloaded file lands at the
same temp path. **Fix (post-CMSC):** in the Electron `set_wallpaper` handler
(`aether_app_native_electron`), append a timestamp or random suffix to the temp filename on
every download so macOS always sees a new path (e.g. `wallpaper_1748123456.jpg` instead of
`wallpaper.jpg`).
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
In live setups, wallpaper can randomly reset when projectors/displays are unplugged or
reattached. Add a resilient strategy that avoids unnecessary churn when wallpaper is already
correct.
**Options to evaluate:**
- periodic reconciliation loop with backoff/jitter and a cheap "is wallpaper already correct?"
check before applying
- event-driven reapply when display topology changes (new display detected), leveraging the
updated Electron display library
- hybrid: event-driven first, periodic safety check as fallback
**Goal:** keep podium/projector wallpaper stable without repeatedly setting wallpaper when no
change is needed.
- [ ] **[Launcher/Electron] `run_cmd`/`run_cmd_sync` — phantom `return_stdout` param (low priority)** —
`electron_relay.ts` passes `return_stdout` in the args object, but both IPC handlers ignore it
(stdout is always returned). Effectively a no-op, but creates a misleading API contract. Fix:
remove the param from relay calls, or honor it in the handlers. No behavioral impact currently.
- [ ] **[Launcher/Electron] `power_control` — sudo requirement not surfaced in UI (low priority)** —
`shutdown` and `reboot` require `sudo` on Linux. macOS works without it; Launcher only targets
macOS so this is a non-issue in production. If Linux podiums are ever deployed, `power_control`
shutdown/reboot will fail silently with a permissions error. No fix needed before CMSC.
- [ ] **[Launcher/Electron] `manage_recording` — aperture binary path on packaged builds** —
Handler resolves the aperture binary via a dev-relative path. Needs verification that the path
resolves correctly inside a packaged `.app` bundle (`app.asar` / `resources/`). Not blocking
for CMSC (recording not part of CMSC workflow). Verify on next packaged build test.
--- ---
## 🔴 Axonius DC — June 9 (Badge Printing) ## 🔴 Axonius DC — June 9 (Badge Printing)
**Setup/Registration:** June 8 | **Show:** June 9 **Setup/Registration:** June 8 | **Show:** June 9
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Axonius is using Epson C3500 printers - [x] **[Badges] Epson C3500 fanfold badge layout** — `badge_4x6_fanfold` layout CSS created,
with fanfold (continuous) badge stock. Create/configure a fanfold badge layout compatible wired, and documented. First live use: Axonius Adapt DC, June 9, 2026. (2026-05-15)
with the C3500 format. Must be ready before the June 8 setup/registration day.
### Badges follow-ups
- [ ] **[Badges] Implement review-link email delivery** — current Email Link actions only show
placeholder alerts. Send to `event_badge.email`, never the attendee-editable `email_override`.
- [ ] **[Badges] Unify review and kiosk edit permissions** — remote review reads
`event.mod_badges_json.edit_permissions`; print controls read template `cfg_json.controls_cfg`.
Define precedence or consolidate them so both flows enforce one documented policy.
- [ ] **[Badges] Use template badge types in search filter** — replace the hardcoded badge-type
list in `ae_comp__badge_search.svelte` with the active template's `badge_type_list`.
--- ---
## 🚧 Upcoming High Priority ## 🚧 V3 CRUD Migration (Surgical Cleanup)
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
### ~~[IDAA] Random "Access Denied" — Root Cause Review & Fixes~~ ✅ Resolved (2026-05-19) - [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
All known root causes fixed across 10+ commits to `src/routes/idaa/(idaa)/+layout.svelte`. `src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
Deploying as of 2026-05-19. Monitor for further member reports.
#### All fixes applied
- [x] **Server-side Novi verification migrated (key fix for hotel/VPN/Cloudflare-filtered networks)**
`verify_novi_uuid()` now calls `GET /v3/action/idaa/novi_member/{uuid}` through the Aether
backend (server-to-server, Redis-cached 4h) instead of making a browser-to-Novi call.
Eliminates false Access Denied for members on hotel/conference WiFi, VPNs, and corporate
networks where the member's IP was rejected by Novi/Cloudflare.
Frontend: `(idaa)/+layout.svelte` | Backend: FastAPI `aether_api_fastapi` | Docs: `GUIDE__AE_API_V3_for_Frontend.md` §12, `CLIENT__IDAA_and_customized_mods.md`
- [x] Novi TTL extended to **12 hours** (5 min → 45 min → 12 h) — covers a full conference day
- [x] Access Denied on iframe reload (sessionStorage URL preservation) — `2855e091f`
- [x] TTL cache bypassed when `$ae_loc` auth flags reset — `2855e091f`
- [x] "Verification Unavailable" screen distinct from "Access Denied" — `2855e091f`
- [x] "Try Again" without page reload (`retry_count` pattern) — `2855e091f`
- [x] 12 s AbortController hard timeout on Novi fetch — `e921ca973`
- [x] Network/AbortError gets 3 s grace + one retry — `e921ca973`
- [x] Clear Cache & Reload added to Access Denied state (iframe mode) — `2855e091f`
- [x] `VERIFY_TIMEOUT_MS` 8 s → 35 s (was firing mid-retry, causing premature Reset clicks) — `53fd5e7de`
- [x] `sessionStorage` try-catch (iOS Safari Private Browsing throws on access) — `53fd5e7de`
- [x] Appshell stores guarded behind `account_id``8850db89c`
- [x] Recovery meetings over-filtering bug (API `default_qry_str`) — `76e21b08f`
- [x] A→Z sort in recovery meetings API revalidation path — `c0386f27b`
- [x] `events.event` IDB content version bump (stale cache) — previous commit
#### Root layout SWR verified safe:
The root `+layout.ts` builds `ae_loc_init` as a plain site-config object (no `authenticated_access`,
`trusted_access`, or `access_type` fields). The root layout sync effect
`$ae_loc = { ...current_loc, ...ae_acct.loc }` therefore cannot overwrite Novi-set auth flags.
Confirmed safe — this is NOT a cause of Access Denied.
#### Remaining architectural note:
The long-term fix for the coarse `$ae_loc` reactivity (Svelte 4 store) causing Effect 2 to
re-run on unrelated writes is tracked under **[Stores] Svelte 4 → Svelte 5 State Migration**
below. The TTL + `verify_in_flight` guards are the current mitigation.
--- ---
### ~~[IDAA] Server-side Novi verification — 503 not auto-retried~~ ✅ Fixed (2026-05-20) ## 🚧 High Priority Workstreams
--- ### [Security] Site Passcode JWT Migration
### [Launcher/VLC] Linux playback — fullscreen + pause-on-end not working - [ ] **[Security] Verify `/authenticate_passcode` deployment** — confirm explicit role priority,
**Status:** Mac ✅ working perfectly; Linux 🚧 deferred for later investigation complete role flags, `auth_type: 'passcode'`, per-role TTLs, and minimum length validation.
**Date discovered:** 2026-05-20 - [ ] **[Security] Replace local passcode comparison** — migrate
`e_app_access_type.svelte` to server verification, JWT storage, and pending/error UI.
- [ ] **[Security] Remove client-side passcode delivery/storage** — stop caching
`access_code_kv_json`, remove `site_access_code_kv` from auth state, and remove passcode logging.
- [ ] **[Security] Enforce passcode JWT expiry on restore** — expired passcode sessions must
return to anonymous without affecting user-login JWT handling.
macOS VLC profile (direct binary path) successfully: Reference: `documentation/PROJECT__AE_Site_Passcode_Security.md`.
- Opens VLC with the media file
- Plays + pauses on the last frame (instead of returning to playlist)
- Fullscreen toggle works (Cmd+F via AppleScript post-script)
Linux VLC command (`vlc --no-play-and-exit --play-and-pause "{{path}}"`) currently: ### [Stores] Svelte 4 → Svelte 5 State Migration
- Does NOT go fullscreen The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
- Does NOT pause on the last frame (plays through, returns to playlist) `PersistedState` (from `runed`) for fine-grained updates. See `PROJECT__Stores_Svelte5_Migration.md`.
**Current state:** Both macOS and Linux commands in `ae_launcher__default_launch_profiles.ts`. - [x] **Events module — COMPLETE (2026-06-11):** `events_loc` fully retired. All 5 sub-stores
macOS is the primary venue deployment platform; Linux support is nice-to-have. (`badges_loc`, `leads_loc`, `pres_mgmt_loc`, `launcher_loc`, `events_auth_loc`) are on
`PersistedState`. Unused fields also pruned from `ae_stores.ts` and `ae_idaa_stores.ts`.
- [ ] **`idaa_loc` → PersistedState** — Highest remaining priority. Root cause of the IDAA
"Access Denied" corruption bug (`ae_loc` bootstrap writes stomp on `authenticated_access`).
Promote `novi_*` identity fields and `archives/bb/recovery_meetings` sub-objects.
- [ ] **`ae_loc` → PersistedState** — Largest scope. Extract `auth_loc` sub-store first
(the identity/permission fields are what get corrupted). Defer full migration until after `idaa_loc`.
- [ ] **Non-persisted writables** (`ae_sess`, `slct`, etc.) — Low priority; no coarse-reactivity problem.
**Investigation needed:** Determine if the VLC flags are being interpreted on Linux, ### [Data Layer] IDB sorting + content version rollout
or if there's a launcher execution layer issue (e.g. `shell:` prefix handling). Sorting baseline is now `build_tmp_sort` (ASC chain, no `.reverse()` on tmp-sort lists).
File: `src/lib/ae_events/ae_launcher__default_launch_profiles.ts``make_vlc_mirror_linux_profile()`.
--- **⚠️ Exception:** `ae_events__event.ts` and `ae_events__event_session.ts` use **legacy encoding**
(`priority ? 1 : 0`, priority=true→`'1'`). Their sort comparators must remain **descending**
until the modules are migrated to `build_tmp_sort`. `ae_events__event_presentation.ts` already
uses `build_tmp_sort` (overrides generic encoding in its `specific_processor`). See
`CLIENT__IDAA_and_customized_mods.md` → "Sort Encoding" for full table.
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c) - [ ] **[IDB Sort] Migrate `ae_events__event.ts` to `build_tmp_sort`** — requires bumping
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state `IDB_CONTENT_VERSIONS.events.event` (currently v3) and switching all event sort comparators
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field** to ascending. Check all pages that sort events before doing this.
of a Svelte 4 store subscribes to the **entire store** — coarse-grained reactivity. This is the - [ ] **[IDB Sort] Roll out to `ae_events__event_session`** after sort behavior review.
root cause of the IDAA Novi re-auth bug (2026-03-30): unrelated `$ae_loc` writes (e.g. iframe - [ ] **[IDB Sort] Roll out to `ae_events__event_presenter`** after sort behavior review.
height, SWR cfg reload) triggered the Novi verification effect repeatedly. - [ ] **[IDB Sort] Roll out to `ae_events__event_location`** after sort behavior review.
- [ ] **[IDB Sort] Roll out to `ae_core__person` + `ae_core__account`** after sort behavior review.
Migration target: replace `svelte-persisted-store` with Svelte 5 `$state`-based persistence - [ ] **[IDB Version] Roll out to `db_events.ts`** (session, presenter, badge, etc.).
(e.g. `runed` `PersistedState`, or a lightweight custom wrapper). This gives fine-grained - [ ] **[IDB Version] Roll out to `db_core.ts`** (site_domain, person, user).
reactivity — only effects that actually read a changed field re-run.
**Phased approach (do NOT do all at once):**
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
Decide: `runed` library vs. custom `$state` + localStorage wrapper. Audit all store consumers.
Identify stores in priority order. Estimate blast radius per store.
- [ ] **Phase B — Core auth stores (highest impact, start here):**
- `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] IDB Content Version System
Scaffolded in `store_versions.ts` (`IDB_CONTENT_VERSIONS` constant + `check_and_clear_idb_table()`
helper) and `core__idb_dexie.ts` (`check_and_clear_idb_tables()` batch helper). Mirrors
`AE_LOC_VERSION` but targets Dexie table contents rather than localStorage keys.
**Currently active:** `journals.journal_entry` (db_journals.ts), `events.event` (IDAA layout).
All other tables are defined but not yet wired.
**Real-world impact:** Stale IDB records from a `properties_to_save` change were the root cause
of the IDAA Recovery Meetings "no meetings found" bug — a ~1-year unresolved issue (20252026).
Fixed 2026-05-16 by wiring `events.event` into the IDAA layout and bumping its version to 2.
See `BOOTSTRAP__AI_Agent_Quickstart.md` mistake #13 for the full postmortem.
**How it works:**
- `check_and_clear_idb_table(db_table, 'module', 'table')` reads a localStorage key with the
expected version from `IDB_CONTENT_VERSIONS`
- On mismatch (or missing key), the Dexie table is cleared and the key is updated
- SWR repopulates from API on next access — no explicit reload needed
- Cost on version match: one `localStorage.getItem()` — effectively free
- Bump a table's version in `IDB_CONTENT_VERSIONS` when `properties_to_save` changes shape
**IDAA consideration:**
IDAA tables are already cleared by `indexedDB.deleteDatabase()` on sign-out/auth failure in
`(idaa)/+layout.svelte`. The content version check is a *complementary* deploy-time reset, not
a replacement.
**Tasks:**
- [x] Write `check_and_clear_idb_tables()` helper in `core__idb_dexie.ts` (2026-05-14)
- [x] Wire helper into `db_journals.ts` (pilot — `journal_entry: 2` cleared stale content_md_html) (2026-05-14)
- [x] Wire `events.event` into IDAA layout `(idaa)/+layout.svelte` + bump version to 2 (2026-05-16)
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, device, location, file)
- [ ] Roll out to `db_core.ts` (site_domain, person, user)
- [ ] Roll out to IDAA modules (`db_posts.ts`, `db_archives.ts`) — verify auth-wipe interaction first
- [ ] Consolidate the two `check_and_clear_idb_table*` helpers (single-table in `store_versions.ts`, batch in `core__idb_dexie.ts`)
### [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).
### [TypeScript] svelte-check hidden errors — discovered 2026-03-27
**HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from
`IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
lifted and the real pre-existing errors became visible.
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
**Current state (2026-03-31):** 32 errors, 0 warnings — all `ModalProps.children`.
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
### [Journals] Journal Entry Config follow-ups ### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
- [ ] **[Journals] Quick Add/import encryption behavior** — both creation paths currently
create plaintext entries; define the intended privacy UX and add encryption support before
claiming that these paths honor entry E2EE.
- [ ] **[Journals] Remove decrypted-content console preview** —
`ae_journals_decryption.ts` logs the first 30 plaintext characters after successful decryption.
Never log private journal content.
- [ ] **[Journals] Confirm outbound email-sharing requirement** — the archived UI project listed
this as unfinished, but no implementation exists. Confirm product/security requirements before
creating an email workflow for private journal content.
- [ ] **[Journals] Visibility / audience toggle contrast** — the flag buttons need a clearer ---
selected state in both light and dark mode.
- [ ] **[Journals] Footer button style** — the actual `Done` button should read like a real button,
not a seamless footer spacer.
- [ ] **[Journals] Entry passcode secondary auth** — `passcode_hash` stores a hash; compare the
entered passcode hash to the stored hash, gate entry loading, and honor the TTL-based access
window. This is secondary entry auth, not a plain-text passcode field.
- [ ] **[Journals] Summary AI shortcut** — add an AI summarize button next to Entry Details
Summary so staff can generate a summary directly from the modal.
- [ ] **[Journals] Archive On sizing** — constrain the Archive On control to a reasonable width
instead of letting it expand to full width.
- [ ] **[Journals] Archive On behavior** — define what Archive On actually means and wire the
behavior; it is currently just a UI field with no live effect.
- [x] **[IDAA] Do not cache IDAA data in IDB when access is denied (2026-04-19, audited 2026-04-28)** ## 🧪 Testing & Optimization
Full audit confirmed all protection layers are in place. No code changes required.
- All `+page.ts` / `+layout.ts` under `src/routes/idaa/` are clean — no SWR loads run before auth resolves.
- All `$effect` SWR calls in IDAA `+page.svelte` files are gated on `$idaa_loc.novi_verified || $ae_loc.trusted_access`.
- `(idaa)/+layout.svelte` purges `db_posts`, `db_archives`, `db_events` on auth failure, no-UUID/no-session, and inconsistent state.
- `sign_out()` calls `indexedDB.deleteDatabase()` on all IDAA databases.
- API 401/403 responses fail-fast in `api_get_object.ts` (throw before any IDB write).
- `idaa_trig` is in-memory `writable()` only — cannot carry stale trigger state across sessions.
- `$effect` auth guards in IDAA page components are reactivity guards (prevent spurious SWR calls on coarse `$ae_loc` writes), NOT auth-bypass guards. SvelteKit layout hierarchy already prevents child components from mounting when `(idaa)/+layout.svelte` blocks rendering.
- Doc: SvelteKit layout hierarchy security model captured in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` and `BOOTSTRAP__AI_Agent_Quickstart.md` (Mistake #7).
- [ ] **[IDAA] IDB fast-path contact search — Recovery Meetings (2026-04-08, updated 2026-05-19)**
**API path is now working**`default_qry_str` already includes contact name/email.
Two bugs were fixed 2026-05-19: (1) STORED GENERATED columns had stale values; forced
rebuild via fake updates. (2) Frontend secondary filter was re-checking text against
response fields, silently dropping API results that matched only via `default_qry_str`.
**Remaining gap — IDB fast-path only:** The local cache fast-path returns all cached meetings
without text filtering; users see the unfiltered list first, then the API-filtered result
replaces it. To make contact matches appear instantly from cache:
- `src/lib/ae_events/ae_events__event.ts` → fast-path filter in `search__event()`: parse
`contact_li_json` and include contact names/emails in the local text match.
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path display: same filter.
Backend enhancement (`contact_li_json_ext` whitelist) is not required for this — the IDB
records already store `contact_li_json` raw JSON which can be parsed client-side.
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.** - [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
The current search query can be taxing on the server. With ~150 active meetings, the view - [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
logic and supporting indexes need a performance review to ensure fast responses as the - [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
database grows. (Requested 2026-05-18)
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.** ---
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
Check all other event search pages that use `db_events.event.filter()` or a secondary
post-API text filter — they may have the same mismatch (local searches `name`/`description`
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
that has a full-text search input.
### [IDAA] Jitsi config editor + live site fix ## ⚙️ DevOps & Backend
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
Need to update site 17 in **production** to `https://api.oneskyit.com/api/jitsi_token`.
SQL: `UPDATE site SET cfg_json = JSON_SET(cfg_json, '$.jitsi_token_endpoint', 'https://api.oneskyit.com/api/jitsi_token') WHERE id = 17;`
- [ ] **Add IDAA Jitsi config editor UI** to the jitsi_reports page (administrator_access only), - [ ] **[Cleanup] Remove unused legacy API wrappers** — `create_ae_obj_crud()`,
alongside the existing Jitsi URL Builder section. Should allow editing key fields in `get_ae_obj_id_crud()`, and `update_ae_obj_id_crud()` are still exported from `api.ts` but
`site_cfg_json` without needing phpMyAdmin: no longer called anywhere in production code. V3 migration is 100% complete. Safe to delete:
- `jitsi_token_endpoint` — the JWT signing endpoint (needs to point to prod) definitions in `api.ts` (lines 109-260), `src/lib/ae_api/api_get__crud_obj_id.ts`, unused
- Jitsi domain default (currently hardcoded as `jitsi.dgrzone.com` fallback in the page) wrapper in `ae_core_functions.ts` (`get_site_domain_obj_from_fqdn`, `update_ae_obj_id_crud`).
- `novi_jitsi_mod_li` — list of Novi UUIDs who get moderator privileges - [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
Read from `$ae_loc.site_cfg_json`, PATCH the site record via V3 CRUD override currently uses a localStorage workaround (`launcher_loc.current.file_display_overrides`)
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
effect without re-login. table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
backend field (restoring global/cross-device persistence). Frontend code is in
`launcher_file_cont.svelte` — search for `file_display_overrides`.
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
- [x] **[DevOps] Service worker `skipWaiting` + `clients.claim`** — Root cause of "users see
old code / can't reproduce in dev testing": the SW sat in waiting state until all tabs closed.
IDAA members leave idaa.org open all day. Fixed 2026-06-03: both calls added to
`src/service-worker.js`. See mistake #16 in `BOOTSTRAP__AI_Agent_Quickstart.md`.
- [ ] **[DevOps] Nginx proxy buffer tuning** — Buffer settings copied from PHP guide; not
optimal for Node.js. `proxy_busy_buffers_size` technically exceeds safe limit. Re-examine
when enabling compression (now re-enabled) stabilizes.
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
### [IDAA] Jitsi Reports still incomplete ---
- [x] **Finish Jitsi Reports filters** — added Novi UUID exclusion plus meeting-name whitelist
filtering, with room-level unique counts based on Novi UUID when present. (2026-05-06)
### ~~[PWA] Service worker ignoring `chrome-extension://` requests~~ ✅ Fixed (2026-05-14)
Guard added to `src/service-worker.js` fetch handler: `if (!event.request.url.startsWith('http')) return;`
Also skips cross-origin requests entirely (origin check). No console errors from extension URLs.
### [CSS] Global placeholder text color — too dark in light mode
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making
placeholders indistinguishable from filled-in values. Most visible in badge print controls
where placeholders show the actual badge value (e.g. "John Smith").
Workaround: scoped `::placeholder` rule added to `ae_comp__badge_print_controls.svelte`
(gray-400 light / gray-500 dark) — `commit 7733ef8`.
**Long-term fix:** Add a global rule to the main CSS (e.g. `src/app.css` or a theme file):
```css
::placeholder {
color: #9ca3af; /* gray-400 */
opacity: 1; /* overrides Firefox's 0.54 default */
}
.dark ::placeholder {
color: #6b7280; /* gray-500 */
}
```
Once the global rule is in place, remove the scoped workaround from the badge controls.
### [Backend/DevOps] Re-add `Access-Control-Allow-Private-Network: true` CORS header
Chrome's Private Network Access (PNA) policy blocks public-origin iframes from fetching
private-network addresses. Symptom: when `dev-api.oneskyit.com` resolves to a LAN IP
(testing from home), Chrome blocks the site domain lookup → ghost account → `site_cfg_json`
never loads → `novi_idaa_api_key` is null → IDAA Novi verifier spins forever → timeout banner.
Firefox unaffected. Production unaffected (public IPs only).
- [ ] **Re-add PNA header to API CORS config**`dev-api` Nginx or FastAPI CORS middleware
must respond with `Access-Control-Allow-Private-Network: true` when Chrome sends
`Access-Control-Request-Private-Network: true` in the preflight. This was fixed ~1 month
ago and regressed. Check Nginx site config and FastAPI `CORSMiddleware` settings.
Low urgency (dev-only, Firefox workaround available), but blocks home-network iframe testing.
### [DevOps] Remaining deployment items
- [ ] **Simplify Dockerfile env file selection** — Currently the Dockerfile uses a `BUILD_MODE` arg to
select between `.env.dev`, `.env.test`, `.env.prod` during the Docker build. This is unnecessary
complexity: each server (test Linode, prod Linode, workstation) only ever runs one environment, so
there will only ever be one env file present in that server's app directory.
**The fix:** Each server's app dir (`/srv/apps/test_aether_app_sveltekit/`, etc.) should have a
plain `.env` file (gitignored, placed manually during server setup). The Dockerfile should just
`COPY . .` and `cp .env .env.runtime` unconditionally — no `if prod / elif test / else dev`
branching for env file selection.
**What this changes:**
- `aether_app_sveltekit/Dockerfile` — remove the `BUILD_MODE`-driven `cp` block; always use `.env`
- Each Linode app dir gets a plain `.env` instead of `.env.test` / `.env.prod`
- Workstation keeps `.env.local` (for `npm run dev`) and `.env.dev` (for `build:docker:dev`) —
those stay as-is since they legitimately coexist locally
- `BUILD_MODE` arg can stay if needed for other build differences; just stop using it to pick the env file
- Update `.gitignore` in sveltekit to un-ignore `.env.test` / remove stale entries if desired
Low risk but unnecessary churn — defer until after the next active show.
- [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from the 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.
### [Files] `db_events.file.clear()` on upload clears all cached files (2026-04-22)
In `ae_comp__event_files_upload.svelte` line 114, `db_events.file.clear()` wipes the entire
`file` Dexie table, not just files for the current session/presenter. Normally harmless (the
reload right after repopulates), but if multiple sessions' file lists are open simultaneously
they'd briefly flash empty. Low priority — only noticeable in multi-panel workflows.
### [General]
- **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-04)
## ✅ Completed (archived) ## ✅ Completed (archived)
See the full completed history in: See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md) [documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md) [documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-06.md](documentation/archive/TODO__Agents__ARCHIVE_2026-06.md)

View File

@@ -17,7 +17,7 @@ Deliverables (this PR)
- `documentation/PROJECT__AE_Docker_CI_BuildKit_implement.md` (this file) - `documentation/PROJECT__AE_Docker_CI_BuildKit_implement.md` (this file)
- `aether_container_env/Dockerfile.buildkit.example` — BuildKit-friendly multi-stage Dockerfile example. - `aether_container_env/Dockerfile.buildkit.example` — BuildKit-friendly multi-stage Dockerfile example.
- `aether_container_env/ci_buildx_example.sh` — standalone CI script examples (registry cache + local cache usage). - `aether_container_env/ci_buildx_example.sh` — standalone CI script examples (registry cache + local cache usage).
- `documentation/AE_Docker_CI_cache_policy.md` — cache rotation and prune guidance. - `documentation/GUIDE__Docker_CI_Cache_Policy.md` — cache rotation and prune guidance.
Tasks (implementation checklist) Tasks (implementation checklist)
- [ ] Review existing `Dockerfile`(s) under `aether_container_env/` and repository root. - [ ] Review existing `Dockerfile`(s) under `aether_container_env/` and repository root.
@@ -50,4 +50,4 @@ Next steps for the container team
Files included in this PR for reference: Files included in this PR for reference:
- `aether_container_env/Dockerfile.buildkit.example` - `aether_container_env/Dockerfile.buildkit.example`
- `aether_container_env/ci_buildx_example.sh` - `aether_container_env/ci_buildx_example.sh`
- `documentation/AE_Docker_CI_cache_policy.md` - `documentation/GUIDE__Docker_CI_Cache_Policy.md`

View File

@@ -1,11 +1,17 @@
# PROJECT: AE Events Badges — Review Form & Print Font Controls # Archived Project: AE Events Badges — Review Form & Print Font Controls
**Created:** 2026-02-27 **Created:** 2026-02-27
**Last Updated:** 2026-03-18 **Completed and Archived:** 2026-06-12
**Last Verified Against Source:** 2026-06-12
**Branch:** `ae_app_3x_llm` **Branch:** `ae_app_3x_llm`
**Priority:** HIGH — first live event is Axonius, NYC, mid-April 2026
**Owner:** Scott Idem / One Sky IT **Owner:** Scott Idem / One Sky IT
**Status:** ✅ TASK 1 COMPLETE | ✅ TASK 2 COMPLETE | ✅ TASK 3 COMPLETE | ✅ TASK 4.1 COMPLETE | ⏳ TASK 4.0 OPEN **Status:** Complete — review form, kiosk controls, auto-scaling, QR rendering, layouts, and print tracking are implemented.
The original project scope is complete and this document is retained as implementation history.
Current behavior is documented in `documentation/MODULE__AE_Events_Badges.md` and
`documentation/MODULE__AE_Events_Badge_Templates.md`. Remaining email-delivery and permission-config
unification work is tracked in `documentation/TODO__Agents.md`. Planning statements later in this
archived document describe the state at the time they were written and are not current instructions.
--- ---
@@ -44,32 +50,24 @@ Both flows should respect the same permission model:
- Permissions are configured per-event in `event.mod_badges_json.edit_permissions`. - Permissions are configured per-event in `event.mod_badges_json.edit_permissions`.
Hardcoded defaults are used until that config is implemented. Hardcoded defaults are used until that config is implemented.
**Current gap (TASK 4):** The print page edit button is currently gated to trusted_access only. **Task 4 outcome:** The print controls now implement field-level editing. Authenticated users
It needs to be accessible to attendees at the kiosk (with appropriate field-level gating), can edit template-approved fields, trusted staff can correct names, and trusted staff in global
matching the permission model already implemented in `ae_comp__badge_review_form.svelte`. Edit Mode can edit all fields. First printing is available at public kiosk access; reprinting
requires trusted access plus Edit Mode. Remote review uses event-level `edit_permissions`, while
the print controls currently use template-level `controls_cfg`; unification is tracked separately.
--- ---
## Next Up for Badges (TASK 4) ## Task 4 Outcomes
### 0. Kiosk Editing — Print Page Permission Model Alignment ### 0. Kiosk Editing — Complete
**This is the most important gap before the first live event.**
Currently the print page edit button is staff-only (trusted_access gate). At the kiosk, `ae_comp__badge_print_controls.svelte` provides the inline controls and live preview. Its default
attendees need to be able to edit their own fields (same attendee-level permissions as the authenticated fields are title, affiliations, location, lead tracking, and pronouns; template
review form), with staff-only fields gated appropriately. `controls_cfg` can narrow the fields shown and editable. Email delivery remains a placeholder;
when implemented it must send to `event_badge.email`, never `email_override`.
Work needed: ### 1. Auto-Scaling Badge Text — Complete
- Wire the same `can_edit_fields` / `can_edit(field)` permission logic into the print page
that `ae_comp__badge_review_form.svelte` already uses.
- The edit panel on the print page should show attendee-editable fields to all authenticated
users, and staff-only fields to trusted_access+.
- The badge render (v1 or v2) should update live as the attendee edits fields.
- Consider whether the print page needs its own inline edit panel (sidebar or overlay)
or whether it should share/reuse the review form component alongside the badge render.
- **Do NOT use `email_override` as the send-to address** — always use `event_badge.email`.
### 1. Auto-Scaling Badge Text — In Progress
`ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale). `ae_comp__badge_obj_view.svelte` using `element_fit_text.svelte` (binary search auto-scale).
Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button. Toggle between v1 (heuristic) and v2 (auto-scale) on the print page via the `v1`/`v2` header button.
Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges. Heights tuned per layout in `fit_heights` derived object. Still needs visual tuning with real badges.
@@ -105,10 +103,11 @@ badge data, gated by `allow_tracking` on the badge.
## Implementation Status ## Implementation Status
### TASK 4.0: Kiosk Editing — NOT STARTED (updated 2026-03-18) ### TASK 4.0: Kiosk Editing — COMPLETE (verified 2026-06-12)
Print page edit access needs to be opened to attendee-level permissions, not just trusted_access. The print controls implement authenticated field editing, trusted name correction, trusted + Edit
The permission model, field list, and `can_edit()` helper from `ae_comp__badge_review_form.svelte` Mode full editing, and live preview. The print path uses template `controls_cfg`; the review path
should be the reference. See Design Intent section above. uses event `mod_badges_json.edit_permissions`. Aligning those configuration sources is a follow-up,
not a blocker to the completed kiosk controls.
**Note (2026-03-18):** `style_href` and `duplex` are both fully implemented and verified in code — **Note (2026-03-18):** `style_href` and `duplex` are both fully implemented and verified in code —
the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badge render gates the MODULE doc TODO list was stale. `duplex` is in `properties_to_save`; v2 badge render gates

View File

@@ -1,7 +1,9 @@
# Project Plan: Aether AE Obj Field Editor v3 (Consolidated) # Project Plan: Aether AE Obj Field Editor v3 (Consolidated)
> **Status:** 🟡 Mostly Complete — Phase 3 items + GUIDE update remaining > **Status:** ✅ **Completed and archived 2026-06-12**
> **Date:** February 13, 2026 (last updated: 2026-03-20) > **Completion:** V3 component deployed and documented. Legacy V1/V2 removed. Searchable dropdowns deferred as optional enhancement.
> **Created:** 2026-02-13
> **Last Updated:** 2026-06-12
> **Target Component:** `src/lib/elements/element_ae_obj_field_editor.svelte` > **Target Component:** `src/lib/elements/element_ae_obj_field_editor.svelte`
> **Replaces:** `element_ae_crud.svelte` and `element_ae_crud_v2.svelte` > **Replaces:** `element_ae_crud.svelte` and `element_ae_crud_v2.svelte`
@@ -30,15 +32,17 @@ Consolidate the legacy CRUD components into a single, high-performance "Aether O
- [x] Add a "Save" loading state with Lucide's `LoaderCircle` spinner. - [x] Add a "Save" loading state with Lucide's `LoaderCircle` spinner.
- [x] Implement a clear "Cancel" path that restores the original value. - [x] Implement a clear "Cancel" path that restores the original value.
### Phase 3: Field Type Parity (IN PROGRESS) ### Phase 3: Field Type Parity (COMPLETE)
- [x] Support `text`, `textarea`, `select`, `tiptap`, and `checkbox`. - [x] Support `text`, `textarea`, `select`, `tiptap`, and `checkbox`.
- [x] Add `datetime` support using native browser pickers — `date` and `datetime-local` inputs implemented. - [x] Add `datetime` support using native browser pickers — `date` and `datetime-local` inputs implemented.
- [ ] Implement searchable dropdowns for the `select` type. - [x] Add `number` field type support.
- [ ] *(Optional)* Implement searchable dropdowns for the `select` type — deferred as UX enhancement; basic select works for all current use cases.
### Phase 4: Migration & Cleanup ### Phase 4: Migration & Cleanup (COMPLETE)
- [x] Create a playground route for V3 verification (`/testing/ae_obj_field_editor`). - [x] Create a playground route for V3 verification (`/testing/ae_obj_field_editor`).
- [x] Deprecate and remove `v1` and `v2` files — `element_ae_crud.svelte` and `element_ae_crud_v2.svelte` removed 2026-03-20. - [x] Deprecate and remove `v1` and `v2` files — `element_ae_crud.svelte` and `element_ae_crud_v2.svelte` removed 2026-03-20.
- [ ] Update `GUIDE__Development.md` with the new usage patterns. - [x] Update `GUIDE__Development.md` with the new usage patterns — documented at lines 57-109.
- [x] Production deployment — 50+ active usages across session views, person views, locations, presentations, and leads.
## ⚠️ Security & Reliability Stabilization (NEW) ## ⚠️ Security & Reliability Stabilization (NEW)
- [x] **Account Context:** Fixed 403 errors by unifying API helpers to the `/v3/crud/` standard. - [x] **Account Context:** Fixed 403 errors by unifying API helpers to the `/v3/crud/` standard.

View File

@@ -1,9 +1,15 @@
# Aether Journals UI Update (2026) # Archived Project: Aether Journals UI Update (2026)
> **Status:** 🚧 Phase 4 Active (Security/Encryption Blockers remain; Journal Entry config rework in progress) > **Status:** Completed and archived 2026-06-12
> **Last Updated:** 2026-05-05 > **Last Verified Against Source:** 2026-06-12
> **Primary Agent:** Frontend SvelteKit Agent > **Primary Agent:** Frontend SvelteKit Agent
The UI modernization scope is complete: V3 CRUD, Quick Add, Append/Prepend,
import/export, auto-save, configuration modals, decryption isolation, and the
Journals style pass are implemented. Unfinished security and product follow-ups
were transferred to `documentation/TODO__Agents.md`; current operational behavior
and limitations live in `documentation/MODULE__AE_Journals.md`.
## 1. Project Overview ## 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. 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.
@@ -29,7 +35,7 @@ This document outlines the modernization of the Journals module UI in the Svelte
* **Definitions:** `app/ae_obj_types_def.py` -> `app/object_definitions/journals.py` * **Definitions:** `app/ae_obj_types_def.py` -> `app/object_definitions/journals.py`
* **Endpoints:** `/v3/crud/journal/...` and `/v3/crud/journal_entry/...` * **Endpoints:** `/v3/crud/journal/...` and `/v3/crud/journal_entry/...`
### Frontend (In Progress) ### Frontend (Completed UI modernization scope)
* **State Management:** `src/lib/ae_journals/ae_journals_stores.ts` * **State Management:** `src/lib/ae_journals/ae_journals_stores.ts`
* **Local Storage:** Dexie.js (`db_journals`) * **Local Storage:** Dexie.js (`db_journals`)
* **API Client:** `src/lib/api/api.ts` -> `get_ae_obj` * **API Client:** `src/lib/api/api.ts` -> `get_ae_obj`
@@ -68,7 +74,7 @@ This document outlines the modernization of the Journals module UI in the Svelte
- [x] Implement Bulk Export/Import system. - [x] Implement Bulk Export/Import system.
- [x] Establish centralized Export Template engine. - [x] Establish centralized Export Template engine.
### Phase 4: Polish & Security (ACTIVE) ### Phase 4: Polish & Security (UI scope complete; security follow-ups transferred)
- [x] Implement Auto-Save toggle and visual status indicators. - [x] Implement Auto-Save toggle and visual status indicators.
- [x] Extract decryption workflow to non-reactive helper. - [x] Extract decryption workflow to non-reactive helper.
- [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI. - [x] **Standardize Configuration Modals:** Refactored Module, Journal, and Entry configuration into a unified tabbed UI.
@@ -81,9 +87,9 @@ This document outlines the modernization of the Journals module UI in the Svelte
- [x] **Dark mode fixes:** Entry content hover, journal view section/description background and text colors. - [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] **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). - [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. - [ ] Solidify E2EE passcode system for Journals and Entries. See active task list.
- [ ] Audit encryption flow for Quick Added and Imported entries. - [ ] Audit encryption flow for Quick Added and Imported entries. See active task list.
- [ ] Integrate Outbound Email sharing. - [ ] Integrate Outbound Email sharing. Deferred pending product confirmation.
--- ---

View File

@@ -1,7 +1,8 @@
# Project: CRUD V3 Final Migration # Project: CRUD V3 Final Migration
> **Status:** Active / In Progress > **Status:** ✅ **Completed and archived 2026-06-12**
> **Last Updated:** 2026-01-20 > **Completion:** All production code migrated to V3. Legacy wrappers remain defined but unused.
> **Last Updated:** 2026-06-12
> **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/...`). > **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/...`).
--- ---
@@ -21,23 +22,23 @@ While the **Journals** and **Identity (User/Account)** modules have been success
The following files have been identified as using legacy CRUD wrappers. The following files have been identified as using legacy CRUD wrappers.
### 🔴 High Priority: Events Module ### 🔴 High Priority: Events Module
The entire `ae_events` library is heavily dependent on legacy `v2` list and `v1` CRUD wrappers. - [x] `src/lib/ae_events/ae_events__event_session.ts` (Migrated 2026-01-30)
- [x] `src/lib/ae_events/ae_events__event_presenter.ts` (Migrated 2026-01-30)
- [ ] `src/lib/ae_events/ae_events__event_session.ts` - [x] `src/lib/ae_events/ae_events__event_presentation.ts` (Migrated 2026-01-30)
- [ ] `src/lib/ae_events/ae_events__event_presenter.ts` - [x] `src/lib/ae_events/ae_events__event_location.ts` (Migrated 2026-01-30)
- [ ] `src/lib/ae_events/ae_events__event_presentation.ts` - [x] `src/lib/ae_events/ae_events__event_badge_template.ts` (Migrated 2026-01-30)
- [ ] `src/lib/ae_events/ae_events__event_location.ts` - [x] `src/lib/ae_events/ae_events__event_device.ts` (Migrated 2026-01-30)
- [ ] `src/lib/ae_events/ae_events__event_badge_template.ts`
- [ ] `src/lib/ae_events/ae_events__event_device.ts`
- [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28) - [x] `src/lib/ae_events/ae_events__exhibit.ts` (Migrated 2026-01-28)
- [ ] `src/lib/ae_events/ae_events__event_file.ts` - [x] `src/lib/ae_events/ae_events__event_file.ts` (Migrated 2026-01-30)
### 🟠 Medium Priority: Core & Sponsorships ### 🟠 Medium Priority: Core & Sponsorships
Legacy patterns persisting in core logic and config modules. Legacy patterns persisting in core logic and config modules.
- [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts` - [ ] `src/lib/ae_sponsorships/ae_sponsorships_functions.ts`
- [ ] `src/lib/ae_core/core__hosted_files.ts` (Uses `get_ae_obj_id_crud`) - [x] `src/lib/ae_core/core__hosted_files.ts` (Migrated 2026-01-20)
- [ ] `src/lib/ae_core/core__site.ts` (Uses `get_ae_obj_id_crud`) - [x] `src/lib/ae_core/core__site.ts` (Migrated 2026-01-26; bootstrap path uses V3 `search_ae_obj` by `fqdn`)
- [x] `src/lib/ae_core/core__site_domain.ts` (Retired 2026-06-02; helper removed after bootstrap migration to `core__site.ts`)
- [ ] `src/lib/ae_core/ae_core_functions.ts` (STILL USES `get_ae_obj_id_crud` / `update_ae_obj_id_crud`)
- [ ] `src/lib/ae_core/core__country_subdivisions.ts` - [ ] `src/lib/ae_core/core__country_subdivisions.ts`
- [ ] `src/lib/ae_core/core__time_zones.ts` - [ ] `src/lib/ae_core/core__time_zones.ts`
- [ ] `src/lib/ae_core/core__countries.ts` - [ ] `src/lib/ae_core/core__countries.ts`
@@ -48,9 +49,10 @@ Specific UI components that make direct API calls instead of using store functio
- [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`) - [ ] `src/lib/elements/element_data_store.svelte` (Direct `create_ae_obj_crud`)
- [x] `src/lib/elements/element_data_store_v2.svelte` - [x] `src/lib/elements/element_data_store_v2.svelte`
- [ ] `src/routes/events/[event_id]/event_page_menu.svelte` - [ ] `src/routes/events/[event_id]/event_page_menu.svelte`
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.svelte` - [x] `src/routes/events/[event_id]/(pres_mgmt)/session/ae_comp__event_session_alert.svelte` (Migrated to `update_ae_obj`)
- [ ] `src/routes/events/ae_comp__event_session_obj_li.svelte` - [ ] `src/routes/events/ae_comp__event_session_obj_li.svelte`
- [ ] `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte` - [ ] `src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_id_edit.svelte`
- [ ] `src/routes/events/[event_id]/(pres_mgmt)/presenter/[presenter_id]/ae_comp__event_presenter_form_agree.svelte` (STILL USES `update_ae_obj_id_crud`)
--- ---

View File

@@ -0,0 +1,110 @@
# Aether Events — Unified Launcher Configuration (Vision v3.1)
> **Status:** Strategic Design / Unified Proposal
> **Author:** Gemini CLI (Interactive Agent)
> **Target:** Full consistency across all configuration modules.
## 1. Unified Design Language
To eliminate the "created by 3 different people" feel, all components must strictly adhere to this shared specification.
### 1.1 Color Palette & Semantics
- **Primary (Blue):** Main actions, active tabs, and standard configuration toggles.
- **Secondary (Green):** Safe actions (Connect, Sync, Apply).
- **Warning (Orange):** Technical overrides that require caution (Timers, Native Shell).
- **Error (Red):** Destructive actions (Resets, Shutdown, Kill Apps).
- **Surface (Gray):** Containers, input backgrounds, and inactive states.
### 1.2 Typography & Spacing
- **Section Headers:** `text-sm font-bold uppercase tracking-tight` (Provided by Wrapper).
- **Field Labels:** `text-[10px] font-bold uppercase tracking-wider opacity-60 mb-1`.
- **Sub-Descriptions:** `text-[9px] italic opacity-40 leading-snug mt-1`.
- **Status Badges:** `text-[8px] font-bold uppercase tracking-tighter`.
- **Grid Standard:**
* Single Column for complex fields.
* `grid-cols-2` with `gap-4` for standard inputs.
* `grid-cols-3` or `grid-cols-4` only for small buttons or icon toggles.
---
## 2. Structural Reorganization (The "Aether" Layout)
The menu is now a **Vertical Sidebar Modal**. This allows for persistent navigation while dedicating the large right pane to content.
### Tab 1: 🖥️ Display (General Operator)
*Focus: What the screen looks like.*
- **Category: Layout & UI**
- Presets: Oral/Default vs Poster Kiosk (One-tap setup).
- Toggles: Header, Menu, Footer, Times visibility.
- Formatting: Clock (12/24h), Date formats.
- **Category: Screen Saver**
- Idle Timeout (Minutes).
- Mode: Image Cycle vs Video vs Custom.
### Tab 2: 🔌 Connectivity (Onsite Tech)
*Focus: How it talks to the network.*
- **Category: WebSocket Control**
- Connection Status & Signal Strength.
- Controller Mode: Local vs Remote vs Push.
- Group Code: Channel sharding for multi-room management.
- **Category: API Context**
- Current Endpoint, Account, and Site context.
### Tab 3: 🔄 Sync & Health (Onsite Tech)
*Focus: Data integrity and performance.*
- **Category: Sync Engine**
- Status: Active vs Paused.
- Action: Force Sync Location (recursive metadata fetch).
- Stats: Cached Files vs Total Files (Progress bar).
- **Category: System Telemetry**
- CPU & RAM usage (Visual gauges).
- Heartbeat monitor (Last success timestamp).
- Device Identity: Hostname, IP list, Local paths.
### Tab 4: 🛠️ Native Shell (Specialized / Mac)
*Focus: OS-level capabilities.*
- **Category: App Control**
- Window: Maximize, Kiosk Mode, Fullscreen.
- Automation: Kill presentation apps (Clean slate).
- Remote: Virtual clicker (Prev/Next/Start/Stop).
- **Category: System Action**
- Displays: Extend vs Mirror (Native bridge).
- Folders: Open Cache / Open Temp.
- Power: Reboot / Shutdown (With confirmation).
### Tab 5: 🖼️ Wallpaper (Branding)
*Focus: Event-specific aesthetics.*
- **Category: Customization**
- Primary Display: URL/Preset.
- Secondary/Projector: URL/Preset.
- Action: Apply to OS (Native) + Preview (Web).
### Tab 6: 🧪 Advanced (Developer Mode)
*Focus: Fine-tuning and updates.*
- **Category: Performance**
- Polling Intervals (Event, Device, Room, Session, Presenter).
- Cache Sharding (Prefix length).
- **Category: Launch Logic**
- Per-Profile Post-Open Delays (ms).
- **Category: Updates**
- Source: File vs URL.
- Version: Current vs Target.
- Action: Download/Install.
### Tab 7: 🧹 Maintenance (Emergency)
*Focus: Troubleshooting.*
- **Category: Resets**
- Wipe IndexedDB (Module selective).
- Clear LocalStorage (Reset config).
- **Category: Diagnostics**
- Raw Device JSON inspector.
- Terminal Command Entry.
---
## 3. Implementation Plan: The "Cohesion" Refactor
1. **Standardize `Launcher_Cfg_Section.svelte`:** Ensure padding and spacing are baked into the wrapper so children don't have to define it.
2. **Create `Launcher_Cfg_Field.svelte`:** A new helper component to handle the Label + Description + Input pattern consistently.
3. **Audit Sub-Components:** Update all 10 components to use the new colors, grid patterns, and typography.
4. **Polish Transitions:** Ensure the Modal entry and Tab switching are butter-smooth with Svelte 5 transitions.

View File

@@ -0,0 +1,34 @@
# Documentation Archive Index
This directory preserves completed projects, superseded proposals, historical task logs, and legacy technical references.
Archived files are not authoritative for current implementation. Start with `documentation/README__Docs_Index.md` and the active module/guides directory.
## Categories
### Completed or Superseded Projects
Files prefixed with `PROJECT__` document completed implementation phases or superseded project plans.
### Historical Proposals
Files prefixed with `PROPOSAL__` preserve design ideas that are not the current implementation source.
### Task History
Files prefixed with `TODO__Agents__ARCHIVE_` contain completed task history by month.
### Legacy References
Files prefixed with `REFERENCE__Legacy_` are retained for historical context but contain pre-V3, pre-runes, or otherwise superseded guidance.
Do not copy implementation patterns from legacy references without validating them against current source and active guides.
## Restore Policy
Move an archived doc back to the active documentation root only when:
1. Its subject is active again.
2. Its content has been reviewed against current source.
3. Legacy paths, IDs, stores, and API conventions have been updated.
4. It is added to `documentation/README__Docs_Index.md`.

View File

@@ -2,7 +2,7 @@
- [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 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] **[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] **[Stores] Phase 2b — TypeScript interfaces for defaults sub-files**: `SiteCfgJson`, `AePerson`, `AeUser`, `AccessType`, `AuthLocState`; `BadgesLocState/SessState`; `SectionState`, `LauncherLocState/SessState`; `LeadsLocState/SessState`, `TmpLicense`; `PresMgmtLocState/SessState`. svelte-check: 0 errors. (2026-03-16) - [x] **[Stores] Phase 2b — TypeScript interfaces for defaults sub-files**: `SiteCfgJson`, `AePerson`, `AeUser`, `AccessType`, `AuthLocState`; `BadgesLocState/SessState`; `SectionState`, `LauncherLocState/SessState`; `LeadsLocState/SessState`, `TmpLicense`; `PresMgmtLocState/SessState`. svelte-check: 0 errors. (2026-03-16)
- [x] **[UI]** Style Review Phase 1 & 2 complete — all non-frozen, non-IDAA routes migrated: FA→Lucide (events, pres_mgmt, core, badges, leads, hosted_files), `variant-*``preset-*` (all modules), `code_to_html` badge dict refactored to Lucide component map, FA CDN scoped to IDAA layout, global `svg.lucide { display: inline }` CSS rule added to fix icon inline flow. See `documentation/PROJECT__AE_Style_Review.md`. (2026-03-16) - [x] **[UI]** Style Review Phase 1 & 2 complete — all non-frozen, non-IDAA routes migrated: FA→Lucide (events, pres_mgmt, core, badges, leads, hosted_files), `variant-*``preset-*` (all modules), `code_to_html` badge dict refactored to Lucide component map, FA CDN scoped to IDAA layout, global `svg.lucide { display: inline }` CSS rule added to fix icon inline flow. See `documentation/archive/PROJECT__AE_Style_Review_2026-03.md`. (2026-03-16)
- [x] **[UI]** Pres Mgmt Phase 3 — FA→Lucide icon migration across all 24 pres_mgmt files. (2026-03-16) - [x] **[UI]** Pres Mgmt Phase 3 — FA→Lucide icon migration across all 24 pres_mgmt files. (2026-03-16)
- [x] **[IDAA]** `ae_idaa_comp__event_obj_id_edit.svelte` — inlined Tailwind utilities, removed `<style>` block; eliminated all 23 `@apply`/`@reference` svelte-check warnings. (2026-03-16) - [x] **[IDAA]** `ae_idaa_comp__event_obj_id_edit.svelte` — inlined Tailwind utilities, removed `<style>` block; eliminated all 23 `@apply`/`@reference` svelte-check warnings. (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] **[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)

View File

@@ -1,326 +1,54 @@
# Frontend Agent Task List # Frontend Agent Task List (Archived May 2026)
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development.
## ✅ Completed (2026-05)
## 🔴 BGH Conference — April 21 (Must Fix Before Event) ### [API] GET/POST retry hardening — differentiate timeout aborts vs intentional aborts
**Status:** ✅ Completed (2026-05-21)
- GET/POST now explicitly distinguish abort class in helper code.
- Timeout-triggered aborts are retryable via existing retry loop; intentional aborts fail fast.
- Backoff behavior retained (`2s -> 4s -> 6s -> 8s`).
- Validation done via Playwright tests.
- [x] **[Locations] Event Locations list does not auto-load** — added `+page.ts` to trigger ### [API] PATCH/DELETE retry hardening — parity with GET/POST
`load_ae_obj_li__event_location` on page load. Also fixed session query using stale **Status:** ✅ Completed (2026-05-21)
`event_location_id_random` index (should be `event_location_id`). (2026-04-19) - PATCH and DELETE now implement the same retry-classification model used in GET/POST.
- Added explicit fail-fast for 400/401/403/422.
- DELETE now triggers the session-expired banner on 401/403.
- [x] **[Files] Warn/error on `.ppt`/`.doc` upload** — warning rows shown per-file in upload table; ### [Testing] V3 API performance probe (basic stress rounds)
non-trusted users are fully blocked (`file_list_status = 'blocked_legacy'`); trusted users see **Status:** ✅ Completed baseline harness (2026-05-21)
warnings but can still upload. Covers `.ppt`, `.doc` (block) and other legacy exts (warn-only). - Implemented a gated Playwright probe for quick repeated list-query timing against live V3 endpoints.
(2026-04-19) - Writes reports to `tests/results/`.
- [x] **[Files] Hide internal-purpose files from Launcher by default** — renamed `hide_draft` prop ### [IDAA] Random "Access Denied" — Root Cause Review & Fixes
to `show_internal_purpose_files` in `launcher_file_cont.svelte`; logic flipped so `false` (the **Status:** ✅ Resolved (2026-05-19)
default) hides files with `file_purpose == 'outline'`, `'draft'`, or `'admin'`. Store key renamed - Server-side Novi verification migrated to V3 action endpoint.
from `hide_content__draft_files` (inverted, misleading) to `show_content__internal_files: false` - Extended Novi TTL to 12 hours.
(show-on-opt-in, consistent with all other `show_content__*` flags). Updated across all 8 Launcher - Hardened retry and timeout logic in `+layout.svelte`.
templates that pass this prop. (2026-04-19, revised 2026-04-20)
- [x] **[Launcher] Remove duplicate session API call on session select** — `menu_session_list.svelte` ### [IDAA] Server-side Novi verification — 503 not auto-retried
was calling `load_ae_obj_id__event_session` directly AND then `goto()` triggered `+page.ts` which **Status:** ✅ Fixed (2026-05-20)
also called it — two concurrent calls per session click. Removed the direct call entirely;
`+page.ts` is now the sole owner of session data loading. `goto()` promise assigned to
`ae_promises.slct__event_session_id` to drive the existing `{#await}` spinner. (2026-04-20)
- [ ] **[Electron/Launcher] Deploy + test Aether Native Electron app on Mac laptops** — build, ### [IDAA] Jitsi Reports filters
deploy, and verify on onsite Mac laptops. Additional testing of cache/launch flow still needed **Status:** ✅ Finished (2026-05-06)
before April 21. - Added Novi UUID exclusion plus meeting-name whitelist filtering.
- [x] **[Pres Mgmt] POC column shown in "Sessions at this Location"** — wired
`hide__session_poc={!pres_mgmt_loc.current.show__session_li_poc_field}` in
`ae_comp__event_location_obj_li.svelte`; also set `hide__session_location={true}` since
location is implicit in that context. (2026-04-19)
---
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
- [ ] **[Electron/Launcher] Clean up presentation file launch scripts** — BGH show revealed
issues with the scripts used to open/launch presentation files through the Electron Launcher.
Audit and improve the launch/open flow (script reliability, error handling, file path resolution).
Must be tested and ready before May 26 setup day.
---
## 🔴 Axonius DC — June 9 (Badge Printing)
**Setup/Registration:** June 8 | **Show:** June 9
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Axonius is using Epson C3500 printers
with fanfold (continuous) badge stock. Create/configure a fanfold badge layout compatible
with the C3500 format. Must be ready before the June 8 setup/registration day.
---
## 🚧 Upcoming High Priority
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field**
of a Svelte 4 store subscribes to the **entire store** — coarse-grained reactivity. This is the
root cause of the IDAA Novi re-auth bug (2026-03-30): unrelated `$ae_loc` writes (e.g. iframe
height, SWR cfg reload) triggered the Novi verification effect repeatedly.
Migration target: replace `svelte-persisted-store` with Svelte 5 `$state`-based persistence
(e.g. `runed` `PersistedState`, or a lightweight custom wrapper). This gives fine-grained
reactivity — only effects that actually read a changed field re-run.
**Phased approach (do NOT do all at once):**
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
Decide: `runed` library vs. custom `$state` + localStorage wrapper. Audit all store consumers.
Identify stores in priority order. Estimate blast radius per store.
- [ ] **Phase B — Core auth stores (highest impact, start here):**
- `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)
### [TypeScript] svelte-check hidden errors — discovered 2026-03-27
**HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from
`IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
lifted and the real pre-existing errors became visible.
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
**Current state (2026-03-31):** 32 errors, 0 warnings — all `ModalProps.children`.
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Visibility / audience toggle contrast** — the flag buttons need a clearer
selected state in both light and dark mode.
- [ ] **[Journals] Footer button style** — the actual `Done` button should read like a real button,
not a seamless footer spacer.
- [ ] **[Journals] Entry passcode secondary auth** — `passcode_hash` stores a hash; compare the
entered passcode hash to the stored hash, gate entry loading, and honor the TTL-based access
window. This is secondary entry auth, not a plain-text passcode field.
- [ ] **[Journals] Summary AI shortcut** — add an AI summarize button next to Entry Details
Summary so staff can generate a summary directly from the modal.
- [ ] **[Journals] Archive On sizing** — constrain the Archive On control to a reasonable width
instead of letting it expand to full width.
- [ ] **[Journals] Archive On behavior** — define what Archive On actually means and wire the
behavior; it is currently just a UI field with no live effect.
- [x] **[IDAA] Do not cache IDAA data in IDB when access is denied (2026-04-19, audited 2026-04-28)**
Full audit confirmed all protection layers are in place. No code changes required.
- All `+page.ts` / `+layout.ts` under `src/routes/idaa/` are clean — no SWR loads run before auth resolves.
- All `$effect` SWR calls in IDAA `+page.svelte` files are gated on `$idaa_loc.novi_verified || $ae_loc.trusted_access`.
- `(idaa)/+layout.svelte` purges `db_posts`, `db_archives`, `db_events` on auth failure, no-UUID/no-session, and inconsistent state.
- `sign_out()` calls `indexedDB.deleteDatabase()` on all IDAA databases.
- API 401/403 responses fail-fast in `api_get_object.ts` (throw before any IDB write).
- `idaa_trig` is in-memory `writable()` only — cannot carry stale trigger state across sessions.
- `$effect` auth guards in IDAA page components are reactivity guards (prevent spurious SWR calls on coarse `$ae_loc` writes), NOT auth-bypass guards. SvelteKit layout hierarchy already prevents child components from mounting when `(idaa)/+layout.svelte` blocks rendering.
- Doc: SvelteKit layout hierarchy security model captured in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` and `BOOTSTRAP__AI_Agent_Quickstart.md` (Mistake #7).
- [ ] **[IDAA] Make `contact_li_json_ext` searchable — Recovery Meeting contact search (2026-04-08)**
Members cannot search for meetings by contact name or email. `contact_li_json` data is not
included in `default_qry_str` and MariaDB cannot substring-search a JSON longtext directly.
The `event` table already has `contact_li_json_ext` (STORED GENERATED, indexed) to work around this.
**Backend (blocked on this first):** Add `contact_li_json_ext` to the searchable fields
whitelist for the `event` object type — likely a one-line change in `ae_obj_types_def.py`
or the event object definition. Message sent to backend agent 2026-04-08.
**Frontend (after backend ships):**
- `src/lib/ae_events/ae_events__event.ts``search__event()`: add `contact_li_json_ext`
as an OR condition alongside `default_qry_str` when `qry_str` is present.
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
`contact_li_json` and include contact names/emails in the local text match check.
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage in other event search pages.**
The backend was updated 2026-03-31 to expose `default_qry_str` in API responses.
Frontend fix applied to Recovery Meetings (`+page.svelte` + `properties_to_save`).
Check all other event search pages that use `db_events.event.filter()` or a secondary
post-API text filter — they may have the same mismatch (local searches `name`/`description`
only while server uses `default_qry_str`). Start with: any route under `/events/` or `/idaa/`
that has a full-text search input.
### [IDAA] Jitsi config editor + live site fix
- [ ] **Fix live site (id=17) `jitsi_token_endpoint` pointing to dev-api:** DB has
`https://dev-api.oneskyit.com/api/jitsi_token` for both site 10 and site 17 (IDAA live).
Need to update site 17 in **production** to `https://api.oneskyit.com/api/jitsi_token`.
SQL: `UPDATE site SET cfg_json = JSON_SET(cfg_json, '$.jitsi_token_endpoint', 'https://api.oneskyit.com/api/jitsi_token') WHERE id = 17;`
- [ ] **Add IDAA Jitsi config editor UI** to the jitsi_reports page (administrator_access only),
alongside the existing Jitsi URL Builder section. Should allow editing key fields in
`site_cfg_json` without needing phpMyAdmin:
- `jitsi_token_endpoint` — the JWT signing endpoint (needs to point to prod)
- Jitsi domain default (currently hardcoded as `jitsi.dgrzone.com` fallback in the page)
- `novi_jitsi_mod_li` — list of Novi UUIDs who get moderator privileges
Read from `$ae_loc.site_cfg_json`, PATCH the site record via V3 CRUD
(`PATCH /v3/crud/site/{id}/`), reload `$ae_loc.site_cfg_json` on save so it takes
effect without re-login.
### [IDAA] Jitsi Reports still incomplete
- [x] **Finish Jitsi Reports filters** — added Novi UUID exclusion plus meeting-name whitelist
filtering, with room-level unique counts based on Novi UUID when present. (2026-05-06)
### [PWA] Service worker ignoring `chrome-extension://` requests ### [PWA] Service worker ignoring `chrome-extension://` requests
Browser console shows repeated errors: **Status:** ✅ Fixed (2026-05-14)
```text - Added guard to filter out non-http/https requests before Attempting to cache.
TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
```
The service worker's fetch/install handler is trying to cache requests with `chrome-extension://`
URLs (injected by browser extensions), which the Cache API rejects. Fix: filter out non-`http`/`https`
requests before attempting to cache. In the service worker fetch handler, add a guard:
```js
if (!event.request.url.startsWith('http')) return; // skip chrome-extension:// etc.
```
Locate in `static/service-worker.js` or the Vite PWA plugin config. Low severity — doesn't break
functionality, but pollutes the console and may cause unhandled promise rejections.
### [CSS] Global placeholder text color — too dark in light mode ### [Electron/Launcher] Display mirroring auto-detection
Placeholder text inherits full input text color in light mode (Tailwind CSS default), making **Status:** ✅ Completed (2026-05-20)
placeholders indistinguishable from filled-in values. Most visible in badge print controls - `native:set-display-layout` now auto-detects displays via `displayplacer list`.
where placeholders show the actual badge value (e.g. "John Smith").
Workaround: scoped `::placeholder` rule added to `ae_comp__badge_print_controls.svelte` ### [Launcher] Force Sync Location
(gray-400 light / gray-500 dark) — `commit 7733ef8`. **Status:** ✅ Completed (2026-05-21)
- Implemented manual trigger and background engine logic to pre-cache all location files.
**Long-term fix:** Add a global rule to the main CSS (e.g. `src/app.css` or a theme file): ### [Launcher] Chronological Download Priority
```css **Status:** ✅ Completed (2026-05-21)
::placeholder { - Refactored download queue to prioritize Event Assets > Early Sessions > Presentation Order > Created Date.
color: #9ca3af; /* gray-400 */
opacity: 1; /* overrides Firefox's 0.54 default */
}
.dark ::placeholder {
color: #6b7280; /* gray-500 */
}
```
Once the global rule is in place, remove the scoped workaround from the badge controls.
### [Launcher] Error handling + fallback
**Status:** ✅ Completed (2026-05-14)
### [Backend/DevOps] Re-add `Access-Control-Allow-Private-Network: true` CORS header - Post-script failure surfaces 'fallback' status; `open_cmd` failure falls back to OS default.
Chrome's Private Network Access (PNA) policy blocks public-origin iframes from fetching
private-network addresses. Symptom: when `dev-api.oneskyit.com` resolves to a LAN IP
(testing from home), Chrome blocks the site domain lookup → ghost account → `site_cfg_json`
never loads → `novi_idaa_api_key` is null → IDAA Novi verifier spins forever → timeout banner.
Firefox unaffected. Production unaffected (public IPs only).
- [ ] **Re-add PNA header to API CORS config**`dev-api` Nginx or FastAPI CORS middleware
must respond with `Access-Control-Allow-Private-Network: true` when Chrome sends
`Access-Control-Request-Private-Network: true` in the preflight. This was fixed ~1 month
ago and regressed. Check Nginx site config and FastAPI `CORSMiddleware` settings.
Low urgency (dev-only, Firefox workaround available), but blocks home-network iframe testing.
### [DevOps] Remaining deployment items
- [ ] **Simplify Dockerfile env file selection** — Currently the Dockerfile uses a `BUILD_MODE` arg to
select between `.env.dev`, `.env.test`, `.env.prod` during the Docker build. This is unnecessary
complexity: each server (test Linode, prod Linode, workstation) only ever runs one environment, so
there will only ever be one env file present in that server's app directory.
**The fix:** Each server's app dir (`/srv/apps/test_aether_app_sveltekit/`, etc.) should have a
plain `.env` file (gitignored, placed manually during server setup). The Dockerfile should just
`COPY . .` and `cp .env .env.runtime` unconditionally — no `if prod / elif test / else dev`
branching for env file selection.
**What this changes:**
- `aether_app_sveltekit/Dockerfile` — remove the `BUILD_MODE`-driven `cp` block; always use `.env`
- Each Linode app dir gets a plain `.env` instead of `.env.test` / `.env.prod`
- Workstation keeps `.env.local` (for `npm run dev`) and `.env.dev` (for `build:docker:dev`) —
those stay as-is since they legitimately coexist locally
- `BUILD_MODE` arg can stay if needed for other build differences; just stop using it to pick the env file
- Update `.gitignore` in sveltekit to un-ignore `.env.test` / remove stale entries if desired
**Do not touch before the April 21 show.** Low risk but unnecessary churn right before an event.
- [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from the 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.
### [Files] Download button — wrong ID used in `handle_click()` (2026-04-22)
`ae_comp__hosted_files_download_button.svelte` resolves `file_id` for the download call as
`hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id`. When called from
Manage Files with an `event_file_obj`, `hosted_file_obj.id` = `event_file_id` (set by
`_process_generic_props` via the `_random` strip logic), so the chain stops at the wrong value.
The download call goes to `/v3/action/hosted_file/{event_file_id}/download` instead of using the
correct `hosted_file_id`. May work if the backend accepts event_file_id at that endpoint —
needs live verification.
**Status (2026-04-22):** Tested — downloads ARE working despite the wrong ID. The backend
V3 action endpoint appears to silently accept `event_file_id` at the `hosted_file` download
path (or maps between the two). Working by accident, not by design. Needs proper fix before
it breaks — if the backend ever tightens that endpoint, all Manage Files downloads will 404.
**Fix:** In `handle_click()` and both `$effect` blocks and the `content` snippet, replace:
```ts
const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id;
```
with:
```ts
const file_id = hosted_file_obj?.hosted_file_id ?? hosted_file_id;
```
The direct-download `<a>` path is unaffected (already uses `event_file_id` → correct endpoint).
### [Files] `db_events.file.clear()` on upload clears all cached files (2026-04-22)
In `ae_comp__event_files_upload.svelte` line 114, `db_events.file.clear()` wipes the entire
`file` Dexie table, not just files for the current session/presenter. Normally harmless (the
reload right after repopulates), but if multiple sessions' file lists are open simultaneously
they'd briefly flash empty. Low priority — only noticeable in multi-panel workflows.
### [General]
- **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-04)
## ✅ Completed (archived)
See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)

View File

@@ -0,0 +1,139 @@
# Frontend Agent Task List
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable — ongoing development.
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
open (deferred — manual button sufficient for CMSC).
- [x] **[Launcher] Hidden/deleted files still visible in Presenter file list** — Fixed by
API-to-Dexie stale-record pruning plus Launcher background refresh loops for file lists.
`ae_events__event_file.ts` now prunes stale records after refresh, and
`launcher_background_sync.svelte` refreshes/prunes selected session and presenter file lists.
(`fix(launcher): refresh file lists periodically to prune deleted/hidden files`, 2026-05)
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
Append timestamp/random suffix to temp filename so macOS always sees a new path.
- [ ] **[Launcher/Electron] Wallpaper drift after display hotplug (post-CMSC)** —
Add resilient reconciliation loop or event-driven reapply on topology change.
---
## 🔴 Axonius DC — June 9 (Badge Printing)
**Setup/Registration:** June 8 | **Show:** June 9
- [ ] **[Badges] Epson C3500 fanfold badge layout** — Create/configure a fanfold badge layout
compatible with the Epson C3500 continuous stock format.
---
## 🚧 V3 CRUD Migration (Surgical Cleanup)
Finalizing the 100% adoption of V3 Standard endpoints and retirement of legacy wrappers.
- [x] **[Badges] Presenter Agreement Form** — migrated to `update_ae_obj` (2026-05-21)
- [x] **[Core] Site Domain Bootstrap Refactor** — Bootstrap path is already on V3 in
`ae_core__site.ts` via `lookup_site_domain()` using `api.search_ae_obj` with FQDN filter
(used by `src/routes/+layout.ts`).
Follow-up cleanup complete: retired legacy helper `core__site_domain.ts`. (2026-06-02)
- [ ] **[Core] Legacy Utility Helpers** — Refactor `ae_core_functions.ts` to use V3 helpers.
- [ ] **[Cleanup] Delete Legacy Wrappers** — Once all callsites are migrated, remove
`src/lib/ae_api/api_get__crud_obj_id.ts` and the legacy exports from `api.ts`.
---
## 🚧 High Priority Workstreams
### [Stores] Svelte 4 → Svelte 5 State Migration
The app uses `svelte-persisted-store` (coarse reactivity). Migration target: replace with Svelte 5
`$state`-based persistence for fine-grained updates.
- [ ] **Phase A — Project plan + wrapper decision:** Write `PROJECT__Stores_Svelte5_Migration.md`.
- [ ] **Phase B — Core auth stores (highest impact):** `ae_loc`, `idaa_loc`.
- [ ] **Phase C — Remaining persisted stores:** `ae_api`, `ae_events_stores`.
- [ ] **Phase D — Non-persisted writable stores:** `ae_sess`, `slct`, `ae_snip`, etc.
### [IDB Sort] `build_tmp_sort` rollout
Shared utility in `src/lib/ae_core/core__idb_sort.ts` — fixes priority direction (inverted,
true→'0' sorts first ASC) and zero-pads sort field (8 chars). No `.reverse()` needed.
Sort chain: `group → priority DESC → sort ASC → [module-specific fields] → name`.
**⚠️ Never use `.reverse()` on a `tmp_sort_*`-sorted list — inverted priority makes it wrong.**
Documented in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` (IDB Sort section).
- [x] `ae_events__event_presentation` — group + priority + sort + start_datetime + code + name
- [x] `ae_journals__journal` + `ae_journals__journal_entry` — group + priority + sort + name + updated_on
- [ ] `ae_events__event_session` — roll out when sort behavior is reviewed
- [ ] `ae_events__event_presenter` — roll out when sort behavior is reviewed
- [ ] `ae_events__event_location` — roll out when sort behavior is reviewed
- [x] `ae_posts__post` + `ae_posts__post_comment` — migrated to `build_tmp_sort` with 8-char padding; BB comment list consumer updated for ASC tmp_sort ordering. (2026-06-02)
- [ ] `ae_core__person` + `ae_core__account` — roll out when sort behavior is reviewed
### [Stores] IDB Content Version System
- [x] Write `check_and_clear_idb_tables()` helper.
- [x] Wire helper into `db_journals.ts` and IDAA layout.
- [ ] Roll out to `db_events.ts` (module-wide: session, presenter, badge, etc.).
- [ ] Roll out to `db_core.ts` (site_domain, person, user).
### [TypeScript] svelte-check hidden errors
- [x] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.**
Verified no remaining `children={...}` bindings on `<Modal>` and `npx svelte-check` is clean. (2026-06-02)
### [Journals] Journal Entry Config follow-ups
- [ ] **[Journals] Entry passcode secondary auth** — implement `passcode_hash` comparison.
- [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.
---
## 🧪 Testing & Optimization
- [ ] **[IDAA] IDB fast-path contact search** — parse `contact_li_json` in `search__event()`.
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
- [ ] **[IDAA / Events] Audit `default_qry_str` coverage** in all other event search pages.
- [ ] **[Launcher/VLC] Linux playback investigation** — fullscreen + pause-on-end flags.
---
## ⚙️ DevOps & Backend
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
backend field (restoring global/cross-device persistence). Frontend code is in
`launcher_file_cont.svelte` — search for `file_display_overrides`.
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
---
## ✅ Completed (archived)
See the full completed history in:
[documentation/archive/TODO__Agents__ARCHIVE_2026-03.md](documentation/archive/TODO__Agents__ARCHIVE_2026-03.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-04.md](documentation/archive/TODO__Agents__ARCHIVE_2026-04.md)
[documentation/archive/TODO__Agents__ARCHIVE_2026-05.md](documentation/archive/TODO__Agents__ARCHIVE_2026-05.md)

273
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "osit-aether-app-svelte", "name": "osit-aether-app-svelte",
"version": "3.00.10", "version": "3.00.20",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "osit-aether-app-svelte", "name": "osit-aether-app-svelte",
"version": "3.00.10", "version": "3.00.20",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.20.0", "@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0", "@codemirror/commands": "^6.10.0",
@@ -26,12 +26,10 @@
"@lucide/svelte": "^0.*.0", "@lucide/svelte": "^0.*.0",
"@popperjs/core": "^2.11.0", "@popperjs/core": "^2.11.0",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.10",
"axios": "^1.7.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"dexie": "^4.0.0", "dexie": "^4.0.0",
"flowbite-svelte": "^1.28.1", "flowbite-svelte": "^1.28.1",
"html5-qrcode": "^2.3.8", "html5-qrcode": "^2.3.8",
"lucide-svelte": "^0.*.0",
"marked": "^17.0.0", "marked": "^17.0.0",
"openai": "^6.10.0", "openai": "^6.10.0",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
@@ -3929,23 +3927,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3976,19 +3957,6 @@
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4101,18 +4069,6 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commondir": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -4240,15 +4196,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -4299,20 +4246,6 @@
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -4332,24 +4265,6 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -4357,33 +4272,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.27.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
@@ -4935,42 +4823,6 @@
"mini-svg-data-uri": "^1.4.3" "mini-svg-data-uri": "^1.4.3"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -5003,43 +4855,6 @@
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5065,18 +4880,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5092,33 +4895,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -5654,15 +5430,6 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/lucide-svelte": {
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.577.0.tgz",
"integrity": "sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==",
"license": "ISC",
"peerDependencies": {
"svelte": "^3 || ^4 || ^5.0.0-next.42"
}
},
"node_modules/lz-string": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -5693,36 +5460,6 @@
"node": ">= 20" "node": ">= 20"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mini-svg-data-uri": { "node_modules/mini-svg-data-uri": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
@@ -6317,12 +6054,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "osit-aether-app-svelte", "name": "osit-aether-app-svelte",
"version": "3.00.20", "version": "3.00.30",
"description": "One Sky IT's Aether App created with Svelte, SvelteKit, Tailwind CSS, Lucide, Font Awesome, and Skeleton UI. -Scott Idem", "description": "One Sky IT's Aether App created with Svelte, SvelteKit, Tailwind CSS, Lucide, Font Awesome, and Skeleton UI. -Scott Idem",
"homepage": "https://oneskyit.com/", "homepage": "https://oneskyit.com/",
"private": true, "private": true,
@@ -105,12 +105,10 @@
"@lucide/svelte": "^0.*.0", "@lucide/svelte": "^0.*.0",
"@popperjs/core": "^2.11.0", "@popperjs/core": "^2.11.0",
"@tailwindcss/vite": "^4.1.10", "@tailwindcss/vite": "^4.1.10",
"axios": "^1.7.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"dexie": "^4.0.0", "dexie": "^4.0.0",
"flowbite-svelte": "^1.28.1", "flowbite-svelte": "^1.28.1",
"html5-qrcode": "^2.3.8", "html5-qrcode": "^2.3.8",
"lucide-svelte": "^0.*.0",
"marked": "^17.0.0", "marked": "^17.0.0",
"openai": "^6.10.0", "openai": "^6.10.0",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",

View File

@@ -163,9 +163,16 @@ html.light {
background-color: rgb(55 65 81); /* gray-700 */ background-color: rgb(55 65 81); /* gray-700 */
border-color: rgb(75 85 99); /* gray-600 */ border-color: rgb(75 85 99); /* gray-600 */
} }
.input::placeholder,
.textarea::placeholder {
font-style: italic;
opacity: 0.6;
}
.dark .input::placeholder, .dark .input::placeholder,
.dark .textarea::placeholder { .dark .textarea::placeholder {
color: rgb(156 163 175); /* gray-400 — legible at reduced opacity */ color: rgb(156 163 175); /* gray-400 */
font-style: italic;
opacity: 0.8; /* gray-400 is already dim; subtle additional fade */
} }
/* Option elements in dark selects — forces browser native dark chrome */ /* Option elements in dark selects — forces browser native dark chrome */
.dark .select option { .dark .select option {

View File

@@ -1,3 +1,5 @@
import { browser } from '$app/environment';
import { ae_auth_error } from '$lib/stores/ae_stores';
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
/** /**
@@ -11,7 +13,7 @@ export const delete_object = async function delete_object({
headers = {}, headers = {},
params = {}, params = {},
data = {}, data = {},
timeout = 60000, timeout = 20000,
return_meta = false, return_meta = false,
log_lvl = 0, log_lvl = 0,
retry_count = 5 retry_count = 5
@@ -97,9 +99,15 @@ export const delete_object = async function delete_object({
} }
for (let attempt = 1; attempt <= retry_count; attempt++) { for (let attempt = 1; attempt <= retry_count; attempt++) {
// Keep timeout handle at attempt scope so catch can always clear it.
let timeoutId: ReturnType<typeof setTimeout> | null = null;
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => { // AbortError alone is ambiguous. Track helper-timeout aborts so
// caller/navigation aborts can still fail fast with no retry.
let did_timeout_abort = false;
timeoutId = setTimeout(() => {
did_timeout_abort = true;
console.error( console.error(
`API DELETE request timed out after ${timeout}ms.` `API DELETE request timed out after ${timeout}ms.`
); );
@@ -120,12 +128,48 @@ export const delete_object = async function delete_object({
url.toString(), url.toString(),
fetchOptions fetchOptions
).catch(function (error: any) { ).catch(function (error: any) {
if (
error?.name === 'AbortError' ||
error?.name === 'TypeError' ||
error?.message?.includes('aborted')
) {
if (log_lvl > 1) {
console.log(
'API DELETE: Request aborted or browser-terminated.',
error
);
}
return error;
}
console.log( console.log(
'API DELETE Object *fetch* request was aborted or failed in an unexpected way.', 'API DELETE Object *fetch* request was aborted or failed in an unexpected way.',
error error
); );
return error;
}); });
clearTimeout(timeoutId); if (timeoutId) clearTimeout(timeoutId);
// Error object was returned from fetch catch block; decide retry class.
if (
response instanceof Error ||
(response &&
(response.name === 'AbortError' ||
response.name === 'TypeError'))
) {
if (response.name === 'AbortError') {
if (did_timeout_abort) {
throw new Error(
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
);
}
return false;
}
throw new Error(
`Network error (attempt ${attempt}): ${response.message}`
);
}
if (!response) { if (!response) {
throw new Error( throw new Error(
@@ -151,7 +195,24 @@ export const delete_object = async function delete_object({
errorBody errorBody
); );
if (response.status >= 400 && response.status < 404) { // Fail fast on client/auth/validation failures.
if (
response.status === 400 ||
response.status === 401 ||
response.status === 403 ||
response.status === 422
) {
if (response.status === 401 || response.status === 403) {
console.warn(
`AUTH DIAGNOSTICS (DELETE): Headers sent for ${endpoint}:`,
{
has_api_key: !!headers_cleaned['x-aether-api-key'],
has_account_id: !!headers_cleaned['x-account-id']
}
);
// Signal the root layout to show the session-expired banner.
if (browser) ae_auth_error.set({ type: 'expired', ts: Date.now() });
}
return false; return false;
} }
@@ -174,6 +235,8 @@ export const delete_object = async function delete_object({
? json.data ? json.data
: json; : json;
} catch (error) { } catch (error) {
// Ensure per-attempt timeout is always cleared on failure.
if (timeoutId) clearTimeout(timeoutId);
console.error(`API DELETE error on attempt ${attempt}:`, error); console.error(`API DELETE error on attempt ${attempt}:`, error);
if (attempt === retry_count) { if (attempt === retry_count) {
@@ -181,9 +244,12 @@ export const delete_object = async function delete_object({
return false; return false;
} }
if (log_lvl) { // Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy.
console.log(`Retrying... (${attempt}/${retry_count})`); const delay_ms = Math.min(2000 * attempt, 8000);
} console.log(
`API DELETE: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
);
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
} }
} }
}; };

View File

@@ -14,7 +14,7 @@ export const get_object = async function get_object({
headers = {}, headers = {},
params = {}, params = {},
data = {}, data = {},
timeout = 90000, timeout = 20000,
return_meta = false, return_meta = false,
return_blob = false, return_blob = false,
filename = '', filename = '',
@@ -73,9 +73,6 @@ export const get_object = async function get_object({
url.searchParams.append(key, params[key]) url.searchParams.append(key, params[key])
); );
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
// Clean and merge headers without mutating the original api_cfg // Clean and merge headers without mutating the original api_cfg
const headers_cleaned: key_val = {}; const headers_cleaned: key_val = {};
const merged_headers = { ...api_cfg['headers'], ...headers }; const merged_headers = { ...api_cfg['headers'], ...headers };
@@ -169,10 +166,11 @@ export const get_object = async function get_object({
console.log('Final cleaned headers:', headers_cleaned); console.log('Final cleaned headers:', headers_cleaned);
} }
// signal is injected per-attempt inside the retry loop so each retry gets
// a fresh AbortController with its own independent timeout.
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
method: 'GET', method: 'GET',
headers: headers_cleaned, headers: headers_cleaned,
signal: controller.signal,
// Be explicit about CORS behavior and redirect handling to avoid // Be explicit about CORS behavior and redirect handling to avoid
// environment-dependent defaults that can cause opaque failures. // environment-dependent defaults that can cause opaque failures.
mode: 'cors', mode: 'cors',
@@ -203,10 +201,24 @@ export const get_object = async function get_object({
return false; return false;
} }
// Fresh AbortController per attempt — ensures each retry has its own
// independent timeout. Sharing a single controller across retries leaves
// retries unprotected once the first attempt's clearTimeout() runs.
const controller = new AbortController();
// Track whether THIS helper's timeout fired. AbortError alone is ambiguous:
// it can mean timeout OR intentional caller abort (navigation/unmount).
// We only retry timeout-aborts; intentional aborts should fail fast.
let did_timeout_abort = false;
const timeoutId = setTimeout(() => {
did_timeout_abort = true;
console.warn(`API GET: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
controller.abort();
}, timeout);
try { try {
const response = await fetch_method( const response = await fetch_method(
url.toString(), url.toString(),
fetchOptions { ...fetchOptions, signal: controller.signal }
).catch(function (error: any) { ).catch(function (error: any) {
// SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs // SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
if ( if (
@@ -231,21 +243,36 @@ export const get_object = async function get_object({
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
// Check if we should stop due to abort or network failure // Check if we should stop due to abort or network failure.
if ( if (
response instanceof Error || response instanceof Error ||
(response && (response &&
(response.name === 'TypeError' || (response.name === 'TypeError' ||
response.name === 'AbortError')) response.name === 'AbortError'))
) { ) {
// If it was an explicit abort, definitely stop // AbortError can be either timeout or intentional abort.
if (response.name === 'AbortError') return false; // Retry only helper-owned timeout aborts; fail fast on caller abort.
if (response.name === 'AbortError') {
if (did_timeout_abort) {
throw new Error(
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
);
}
return false;
}
if (log_lvl > 1) // TypeError = transient network failure (ERR_NETWORK_CHANGED,
console.log( // ERR_NETWORK_IO_SUSPENDED, hotel/conference WiFi blip, etc.).
'API GET Object: Detected NetworkError or TypeError. Failing fast.' // IMPORTANT: throw here so the retry loop's catch block handles it with
); // backoff. Returning false would bypass retries entirely.
return false; //
// WHY THIS WAS BROKEN: The Jan 2026 "offline-first fast-paths" commit
// (a10accfaa) changed .catch() to return the error as a value instead of
// not returning (undefined). The undefined path fell through to the
// `if (!response)` throw which DID retry. The explicit `return error` +
// this `return false` block silently killed the retry for the most common
// failure mode on conference/hotel WiFi.
throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
} }
if (!response) { if (!response) {
@@ -438,6 +465,8 @@ export const get_object = async function get_object({
} }
} }
} catch (error) { } catch (error) {
// Ensure the per-attempt timeout timer is always cancelled on failure.
clearTimeout(timeoutId);
console.log( console.log(
`API GET object request *fetch* error on attempt ${attempt}:`, `API GET object request *fetch* error on attempt ${attempt}:`,
error error
@@ -448,10 +477,13 @@ export const get_object = async function get_object({
return false; return false;
} }
// Log retry information // Backoff before retrying. Without a delay, rapid retries on a flaky
if (log_lvl) { // connection accomplish nothing and add noise. Caps at 8s so later
console.log(`Retrying... (${attempt}/${retry_count})`); // attempts don't wait excessively. Gives the network time to recover
} // (ERR_NETWORK_CHANGED is typically a sub-second WiFi roam event).
const delay_ms = Math.min(2000 * attempt, 8000);
console.log(`API GET: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
} }
} }
}; };

View File

@@ -13,7 +13,7 @@ export const patch_object = async function patch_object({
headers = {}, headers = {},
params = {}, params = {},
data = {}, data = {},
timeout = 60000, timeout = 20000,
return_meta = false, return_meta = false,
log_lvl = 0, log_lvl = 0,
retry_count = 5 retry_count = 5
@@ -153,9 +153,15 @@ export const patch_object = async function patch_object({
} }
for (let attempt = 1; attempt <= retry_count; attempt++) { for (let attempt = 1; attempt <= retry_count; attempt++) {
// Keep timeout handle at attempt scope so catch can always clear it.
let timeoutId: ReturnType<typeof setTimeout> | null = null;
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => { // AbortError alone is ambiguous. Track whether the helper timeout
// fired so we can retry timeout-aborts but fail fast on caller abort.
let did_timeout_abort = false;
timeoutId = setTimeout(() => {
did_timeout_abort = true;
console.error( console.error(
`API PATCH request timed out after ${timeout}ms.` `API PATCH request timed out after ${timeout}ms.`
); );
@@ -173,12 +179,52 @@ export const patch_object = async function patch_object({
url.toString(), url.toString(),
fetchOptions fetchOptions
).catch(function (error: any) { ).catch(function (error: any) {
// Keep noisy abort/network conditions out of high-level logs.
if (
error?.name === 'AbortError' ||
error?.name === 'TypeError' ||
error?.message?.includes('aborted')
) {
if (log_lvl > 1) {
console.log(
'API PATCH: Request aborted or browser-terminated.',
error
);
}
return error;
}
console.log( console.log(
'API PATCH Object *fetch* request was aborted or failed in an unexpected way.', 'API PATCH Object *fetch* request was aborted or failed in an unexpected way.',
error error
); );
return error;
}); });
clearTimeout(timeoutId); if (timeoutId) clearTimeout(timeoutId);
// Error object was returned from fetch catch block; decide retry class.
if (
response instanceof Error ||
(response &&
(response.name === 'AbortError' ||
response.name === 'TypeError'))
) {
if (response.name === 'AbortError') {
// Retry only helper-timeout aborts. Caller/navigation aborts
// should fail fast to avoid duplicate mutation side-effects.
if (did_timeout_abort) {
throw new Error(
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
);
}
return false;
}
// Transient browser/network failure class.
throw new Error(
`Network error (attempt ${attempt}): ${response.message}`
);
}
if (!response) { if (!response) {
throw new Error( throw new Error(
@@ -292,6 +338,8 @@ export const patch_object = async function patch_object({
? json.data ? json.data
: json; : json;
} catch (error) { } catch (error) {
// Ensure per-attempt timeout is always cleared on failure.
if (timeoutId) clearTimeout(timeoutId);
console.error(`API PATCH error on attempt ${attempt}:`, error); console.error(`API PATCH error on attempt ${attempt}:`, error);
if (attempt === retry_count) { if (attempt === retry_count) {
@@ -299,9 +347,12 @@ export const patch_object = async function patch_object({
return false; return false;
} }
if (log_lvl) { // Backoff before retrying. Caps at 8s to match GET/POST policy.
console.log(`Retrying... (${attempt}/${retry_count})`); const delay_ms = Math.min(2000 * attempt, 8000);
} console.log(
`API PATCH: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
);
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
} }
} }
}; };

View File

@@ -15,7 +15,7 @@ export const post_object = async function post_object({
params = {}, params = {},
data = {}, data = {},
form_data = null, form_data = null,
timeout = 90000, timeout = 20000,
return_meta = false, return_meta = false,
return_blob = false, return_blob = false,
filename = '', filename = '',
@@ -200,13 +200,19 @@ export const post_object = async function post_object({
} }
for (let attempt = 1; attempt <= retry_count; attempt++) { for (let attempt = 1; attempt <= retry_count; attempt++) {
try { // Declared at loop scope (not inside try) so the catch block can clearTimeout.
const controller = new AbortController(); // Fresh controller per attempt — same rationale as api_get_object.ts.
const timeoutId = setTimeout(() => { const controller = new AbortController();
console.error(`API POST request timed out after ${timeout}ms.`); // AbortError is not specific enough by itself. Distinguish timeout-aborts
controller.abort(); // (retryable transient class) from intentional caller aborts (fail-fast).
}, timeout); let did_timeout_abort = false;
const timeoutId = setTimeout(() => {
did_timeout_abort = true;
console.warn(`API POST: Request timed out after ${timeout}ms (attempt ${attempt}/${retry_count}).`);
controller.abort();
}, timeout);
try {
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
method: 'POST', method: 'POST',
headers: headers_cleaned, headers: headers_cleaned,
@@ -245,19 +251,28 @@ export const post_object = async function post_object({
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
// Check if we should stop due to abort or network failure // Check if we should stop due to abort or network failure.
if ( if (
response instanceof Error || response instanceof Error ||
(response && (response &&
(response.name === 'TypeError' || (response.name === 'TypeError' ||
response.name === 'AbortError')) response.name === 'AbortError'))
) { ) {
if (response.name === 'AbortError') return false; // Retry timeout-aborts from this helper; do not retry caller aborts
if (log_lvl > 1) // (route change/unmount/manual cancellation).
console.log( if (response.name === 'AbortError') {
'API POST Object: Detected NetworkError or TypeError. Failing fast.' if (did_timeout_abort) {
); throw new Error(
return false; `Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
);
}
return false;
}
// TypeError = transient network failure. Throw into the retry loop
// so backoff-and-retry applies. Same fix as api_get_object.ts — see
// comment there for the full history of why this was broken.
throw new Error(`Network error (attempt ${attempt}): ${response.message}`);
} }
if (!response) { if (!response) {
@@ -411,6 +426,8 @@ export const post_object = async function post_object({
} }
} }
} catch (error) { } catch (error) {
// Ensure the per-attempt timeout timer is always cancelled on failure.
clearTimeout(timeoutId);
console.error(`API POST error on attempt ${attempt}:`, error); console.error(`API POST error on attempt ${attempt}:`, error);
if (attempt === retry_count) { if (attempt === retry_count) {
@@ -418,9 +435,10 @@ export const post_object = async function post_object({
return false; return false;
} }
if (log_lvl) { // Backoff before retrying — same rationale as api_get_object.ts.
console.log(`Retrying... (${attempt}/${retry_count})`); const delay_ms = Math.min(2000 * attempt, 8000);
} console.log(`API POST: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`);
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
} }
} }
}; };

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
// *** Import Svelte specific // *** Import Svelte specific
import * as Lucide from 'lucide-svelte'; import * as Lucide from '@lucide/svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
// *** Import Aether specific variables and functions // *** Import Aether specific variables and functions
@@ -186,16 +186,13 @@ let is_url_file = $derived.by(() => {
let direct_download_url = $derived.by(() => { let direct_download_url = $derived.by(() => {
if (!show_direct_download || !hosted_file_obj) return ''; if (!show_direct_download || !hosted_file_obj) return '';
// IMPORTANT: For Direct Link Mode, we MUST use the V3 Action endpoint to support Random String IDs. // Use event_file endpoint when event_file_id is present (canonical per API guide §5).
// Legacy endpoints often expect integer IDs and will return 404 for string IDs. // Fall back to hosted_file endpoint for standalone hosted_file objects.
const file_id = if (hosted_file_obj.event_file_id) {
hosted_file_obj.event_file_id || return `${$ae_api.base_url}/v3/action/event_file/${hosted_file_obj.event_file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
hosted_file_obj.hosted_file_id || }
hosted_file_id; const file_id = hosted_file_obj.hosted_file_id || hosted_file_id;
const obj_type_path = hosted_file_obj.event_file_id return `${$ae_api.base_url}/v3/action/hosted_file/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
? 'event_file'
: 'hosted_file';
return `${$ae_api.base_url}/v3/action/${obj_type_path}/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&key=${$ae_api.account_id}`;
}); });
async function handle_click() { async function handle_click() {
@@ -346,7 +343,20 @@ async function handle_click() {
disabled={require_auth && !$ae_loc.authenticated_access} disabled={require_auth && !$ae_loc.authenticated_access}
class={variant_classes} class={variant_classes}
onclick={handle_click} onclick={handle_click}
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}> title={
`Download this file:
${final_filename}
[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...
Hosted ID: ${file_id}
File size: ${hosted_file_obj.file_size ? ae_util.format_bytes(hosted_file_obj.file_size) : 'Unknown size'}
Created on: ${ae_util.iso_datetime_formatter(hosted_file_obj.created_on, 'datetime_short')}
Updated on: ${ae_util.iso_datetime_formatter(hosted_file_obj.updated_on, 'datetime_short')}
Open with: ${hosted_file_obj.open_in_os == 'win' ? 'Windows' : hosted_file_obj.open_in_os == 'mac' ? 'macOS' : hosted_file_obj.open_in_os == 'linux' ? 'Linux' : '--not set--'}
Linked to Type: ${linked_to_type ?? '--none--'} ID: ${linked_to_id ?? '---'}`
}>
{@render content()} {@render content()}
</button> </button>
{/if} {/if}

View File

@@ -2,7 +2,7 @@
// untrack import removed — task_id sync now uses direct $effect (no untrack needed) // untrack import removed — task_id sync now uses direct $effect (no untrack needed)
// Imports // Imports
// Import components and elements // Import components and elements
import * as Lucide from 'lucide-svelte'; import * as Lucide from '@lucide/svelte';
import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte'; import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte';
// Import storage, functions, and libraries // Import storage, functions, and libraries

View File

@@ -0,0 +1,54 @@
/**
* src/lib/ae_core/core__idb_sort.ts
*
* Shared utility for computing tmp_sort_* fields stored in Dexie.
* All fields are designed for ascending .sortBy() — no .reverse() needed.
*
* Encoding rules:
* priority — inverted boolean: true→'0', false→'1' so priority=true sorts first (ASC)
* sort — zero-padded integer string so "00000010" < "00000020" (correct numeric order)
* all other fields — appended as-is; ISO 8601 datetimes already sort correctly
*
* Usage:
* const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
* prefix: [obj.group ?? '0'], // fields before priority (optional)
* priority: obj.priority,
* sort: obj.sort,
* fields_1: [obj.start_datetime], // appended to base for tmp_sort_1
* fields_2: [obj.name], // appended after fields_1 for tmp_sort_2
* fields_3: [obj.updated_on], // appended after fields_2 for tmp_sort_3
* });
*/
export function build_tmp_sort({
prefix = [],
priority,
sort,
fields_1 = [],
fields_2 = [],
fields_3 = [],
pad_width = 8
}: {
prefix?: (string | null | undefined)[];
priority?: boolean | null;
sort?: number | string | null;
fields_1?: (string | null | undefined)[];
fields_2?: (string | null | undefined)[];
fields_3?: (string | null | undefined)[];
pad_width?: number;
}): { tmp_sort_1: string; tmp_sort_2: string; tmp_sort_3: string } {
const clean = (v: string | null | undefined): string => v ?? '';
const p = priority ? '0' : '1';
const s = String(Number(sort ?? 0)).padStart(pad_width, '0');
const parts_base = [...prefix.map(clean), p, s].join('_');
const parts_1 = fields_1.map(clean).filter(Boolean).join('_');
const parts_2 = fields_2.map(clean).filter(Boolean).join('_');
const parts_3 = fields_3.map(clean).filter(Boolean).join('_');
const tmp_sort_1 = [parts_base, parts_1].filter(Boolean).join('_');
const tmp_sort_2 = [tmp_sort_1, parts_2].filter(Boolean).join('_');
const tmp_sort_3 = [tmp_sort_2, parts_3].filter(Boolean).join('_');
return { tmp_sort_1, tmp_sort_2, tmp_sort_3 };
}

View File

@@ -1,75 +0,0 @@
export interface Site_Domain {
id: string;
// id_random: string;
site_id: string;
site_id_random?: string;
fqdn: string;
access_key?: null | string;
required_referrer?: null | string;
valid_for?: null | number; // In hours
enable: null | boolean;
hide?: null | boolean;
priority?: null | boolean;
sort?: null | number;
group?: null | string;
notes?: null | string;
created_on: Date;
updated_on?: null | Date;
}
import { api } from '$lib/api/api';
/**
* Fetches a site_domain object by its Fully Qualified Domain Name (FQDN).
*
* @param api_cfg - The API configuration object.
* @param fqdn - The FQDN of the site domain to fetch.
* @param timeout - The request timeout in milliseconds.
* @param log_lvl - The logging level.
* @returns The site domain object or null if not found.
*/
export async function load_ae_obj_by_fqdn__site_domain({
api_cfg,
fqdn,
timeout = 7000,
log_lvl = 0
}: {
api_cfg: any;
fqdn: string;
timeout?: number;
log_lvl?: number;
}): Promise<any> {
if (log_lvl) {
console.log(
`*** load_ae_obj_by_fqdn__site_domain() *** api.base_url=${api_cfg.base_url}, fqdn=${fqdn}, timeout=${timeout}`
);
}
const params = {};
try {
const site_domain_obj = await api.get_ae_obj_id_crud({
api_cfg: api_cfg,
no_account_id: true, // This seems to be a special case for this endpoint
obj_type: 'site_domain',
obj_id: fqdn, // NOTE: This is the FQDN, not the ID.
use_alt_table: true,
use_alt_base: true,
params: params,
timeout: timeout,
log_lvl: log_lvl
});
if (site_domain_obj) {
return site_domain_obj;
} else {
console.log('No results returned.');
return null;
}
} catch (error) {
console.log('No results returned or failed.', error);
return null;
}
}

View File

@@ -357,7 +357,7 @@ async function _handle_nested_loads(
for_obj_type: 'event_location', for_obj_type: 'event_location',
for_obj_id: current_location_id, for_obj_id: current_location_id,
enabled: 'all', enabled: 'all',
limit: 25, hidden: 'all',
log_lvl log_lvl
}).then((res) => (location_obj.event_file_li = res)) }).then((res) => (location_obj.event_file_li = res))
); );

View File

@@ -2,6 +2,7 @@ import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie'; import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_events } from '$lib/ae_events/db_events'; import { db_events } from '$lib/ae_events/db_events';
import type { ae_EventPresentation } from '$lib/types/ae_types'; import type { ae_EventPresentation } from '$lib/types/ae_types';
@@ -680,6 +681,18 @@ export async function process_ae_obj__event_presentation_props({
if (obj.event_session_id_random) if (obj.event_session_id_random)
obj.event_session_id = obj.event_session_id_random; obj.event_session_id = obj.event_session_id_random;
if (obj.event_id_random) obj.event_id = obj.event_id_random; if (obj.event_id_random) obj.event_id = obj.event_id_random;
// Override generic tmp_sort_* with presentation-specific encoding via
// build_tmp_sort. Order: priority DESC → sort ASC → start_datetime ASC → code ASC → name ASC
const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
prefix: [obj.group ?? '0'],
priority: obj.priority,
sort: obj.sort,
fields_1: [obj.start_datetime, obj.code],
fields_2: [obj.name]
});
obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
return obj; return obj;
} }
}); });

View File

@@ -597,10 +597,13 @@ export async function email_sign_in__event_presenter({
return null; return null;
} }
const subject = `Pres Mgmt Hub Sign In Link for Presenter: ${to_name ?? 'Presenter'}`; const subject = `Pres Mgmt Hub Sign In Link for Presenter: ${to_name ?? 'Presenter'}`;
// Routes to the session page (which has the sign-in handler mounted) not /presenter/[id]
// which has no sign-in handler. Includes presenter_id + presentation_id so the handler
// can grant presenter-level auth (not just session read access).
const sign_in_url = encodeURI( const sign_in_url = encodeURI(
`${base_url}/events/${event_id}/presenter/${event_presenter_id}?person_id=${person_id}&person_pass=${person_passcode}` `${base_url}/events/${event_id}/session/${event_session_id}?person_id=${person_id}&person_pass=${person_passcode}&presenter_id=${event_presenter_id}&presentation_id=${event_presentation_id}`
); );
const body_html = `<div>${to_name},<p>Your sign-in link for ${presentation_name ?? 'Presentation'} (Session: ${session_name ?? 'Session'}): <a href="${sign_in_url}">${sign_in_url}</a></p></div>`; const body_html = `<div>${to_name},<p>Your sign-in link for ${presentation_name ?? 'Presentation'} (Session: ${session_name ?? 'Session'}): <a href="${sign_in_url}">${sign_in_url}</a></p><p>This link takes you to the session page — your presentation and file upload sections will be available after you sign in.</p></div>`;
return await api.send_email({ return await api.send_email({
api_cfg, api_cfg,
from_email: 'noreply+presmgmt@oneskyit.com', from_email: 'noreply+presmgmt@oneskyit.com',

View File

@@ -738,11 +738,6 @@ export async function search__event_session({
search_query.and.push({ field: 'enable', op: 'eq', value: 1 }); search_query.and.push({ field: 'enable', op: 'eq', value: 1 });
else if (enabled === 'not_enabled') else if (enabled === 'not_enabled')
search_query.and.push({ field: 'enable', op: 'eq', value: 0 }); search_query.and.push({ field: 'enable', op: 'eq', value: 0 });
if (hidden === 'hidden')
search_query.and.push({ field: 'hide', op: 'eq', value: 1 });
else if (hidden === 'not_hidden')
search_query.and.push({ field: 'hide', op: 'eq', value: 0 });
if (location_name) { if (location_name) {
search_query.and.push({ search_query.and.push({
field: 'event_location_name', field: 'event_location_name',
@@ -774,6 +769,7 @@ export async function search__event_session({
view, view,
limit, limit,
offset, offset,
hidden,
log_lvl log_lvl
}); });
@@ -833,7 +829,7 @@ export async function email_sign_in__event_session({
}) { }) {
const subject = `Pres Mgmt Hub Sign In Link for ${session_name}`; const subject = `Pres Mgmt Hub Sign In Link for ${session_name}`;
const sign_in_url = encodeURI( const sign_in_url = encodeURI(
`${base_url}/events/${event_id}/session/${event_session_id}?person_id=${person_id}&person_pass=${person_passcode}` `${base_url}/events/${event_id}/session/${event_session_id}?person_id=${person_id}&person_pass=${person_passcode}&session_id=${event_session_id}`
); );
const body_html = `<div>${to_name},<p>Your sign-in link for ${session_name}: <a href="${sign_in_url}">${sign_in_url}</a></p></div>`; const body_html = `<div>${to_name},<p>Your sign-in link for ${session_name}: <a href="${sign_in_url}">${sign_in_url}</a></p></div>`;
return await api.send_email({ return await api.send_email({

View File

@@ -55,32 +55,35 @@ export interface LaunchProfile {
/** /**
* macOS VLC profile — uses direct binary path for max reliability. * macOS VLC profile — uses direct binary path for max reliability.
* Bypasses `open -a` argument-handling quirks that could lose file path or re-use existing process. * Bypasses `open -a` argument-handling quirks that could lose file path or re-use existing process.
*
* WHY nohup + &:
* run_cmd uses exec() which blocks until the child process exits (or the 30s timeout fires).
* The direct VLC binary forks a GUI process then exits — exec returns early and the code
* proceeds to the post_script. The old post_script polled for VLC focus (up to 10s) then
* sent Cmd+F, which was firing exactly 1015 seconds into playback and stopping the video.
* nohup + & detaches VLC immediately so exec returns in ~0ms, decoupling run_cmd from
* VLC's lifecycle entirely.
*
* WHY --fullscreen:
* Starting VLC fullscreen via flag avoids the need to send Cmd+F via AppleScript. The old
* keystroke approach was the proximate cause of the video stopping — Cmd+F may have hit the
* wrong VLC window, triggered a menu action, or paused playback during the fullscreen
* transition. Using the flag is simpler and more reliable.
*
* WHY > /dev/null 2>&1:
* VLC logs verbosely to stdout/stderr. exec() buffers output (1MB default). Without
* redirection the buffer could overflow and kill VLC mid-playback.
*/ */
function make_vlc_mirror_mac_profile(): LaunchProfile { function make_vlc_mirror_mac_profile(): LaunchProfile {
return { return {
app: 'VLC (macOS)', app: 'VLC (macOS)',
display_mode: 'mirror', display_mode: 'mirror',
// Direct binary path ensures VLC receives media file + flags reliably. open_cmd: 'nohup /Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause --fullscreen "{{path}}" > /dev/null 2>&1 &',
// `--no-play-and-exit` prevents closing on end, `--play-and-pause` holds final frame. post_delay_ms: 3000,
open_cmd: '/Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause "{{path}}"', // Activate VLC after it has had time to open. Fullscreen is already set by the flag
post_delay_ms: 1000, // above — this just ensures VLC is the frontmost app and the presenter sees it.
// Poll until VLC is frontmost before sending Cmd+F. A fixed delay is unreliable because
// VLC cold-start on a loaded conference Mac can take 3-5 seconds.
// Polling (15 × 0.5 s = up to 7.5 s after the initial wait) fires as soon as VLC is ready.
post_script: `tell application "VLC" post_script: `tell application "VLC"
activate activate
end tell
repeat 15 times
delay 0.5
tell application "System Events"
if frontmost of process "VLC" is true then exit repeat
end tell
end repeat
delay 0.3
tell application "System Events"
tell process "VLC"
keystroke "f" using command down
end tell
end tell` end tell`
}; };
} }
@@ -104,10 +107,10 @@ const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = {
display_mode: 'extend', display_mode: 'extend',
open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"', open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"',
post_delay_ms: 1000, post_delay_ms: 1000,
post_script: `tell application "Microsoft PowerPoint" post_script: `repeat 20 times
activate tell application "Microsoft PowerPoint"
end tell activate
repeat 15 times end tell
delay 0.5 delay 0.5
tell application "System Events" tell application "System Events"
if frontmost of process "Microsoft PowerPoint" is true then exit repeat if frontmost of process "Microsoft PowerPoint" is true then exit repeat
@@ -148,10 +151,10 @@ const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = {
display_mode: 'extend', display_mode: 'extend',
open_cmd: 'open -a "LibreOffice" "{{path}}"', open_cmd: 'open -a "LibreOffice" "{{path}}"',
post_delay_ms: 1000, post_delay_ms: 1000,
post_script: `tell application "LibreOffice" post_script: `repeat 20 times
activate tell application "LibreOffice"
end tell activate
repeat 15 times end tell
delay 0.5 delay 0.5
tell application "System Events" tell application "System Events"
if frontmost of process "soffice" is true then exit repeat if frontmost of process "soffice" is true then exit repeat
@@ -170,10 +173,10 @@ const ACROBAT_MAC_MIRROR_PROFILE: LaunchProfile = {
display_mode: 'mirror', display_mode: 'mirror',
open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"', open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"',
post_delay_ms: 1000, post_delay_ms: 1000,
post_script: `tell application "Adobe Acrobat Reader DC" post_script: `repeat 20 times
activate tell application "Adobe Acrobat Reader DC"
end tell activate
repeat 15 times end tell
delay 0.5 delay 0.5
tell application "System Events" tell application "System Events"
if frontmost of process "AdobeReader" is true then exit repeat if frontmost of process "AdobeReader" is true then exit repeat
@@ -215,10 +218,10 @@ const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = {
display_mode: 'extend', display_mode: 'extend',
open_cmd: 'open -a "LibreOffice" "{{path}}"', open_cmd: 'open -a "LibreOffice" "{{path}}"',
post_delay_ms: 1500, post_delay_ms: 1500,
post_script: `tell application "LibreOffice" post_script: `repeat 20 times
activate tell application "LibreOffice"
end tell activate
repeat 15 times end tell
delay 0.5 delay 0.5
tell application "System Events" tell application "System Events"
if frontmost of process "soffice" is true then exit repeat if frontmost of process "soffice" is true then exit repeat
@@ -237,10 +240,10 @@ const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = {
display_mode: 'mirror', display_mode: 'mirror',
open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"', open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"',
post_delay_ms: 1500, post_delay_ms: 1500,
post_script: `tell application "Acrobat Reader Windows" post_script: `repeat 20 times
activate tell application "Acrobat Reader Windows"
end tell activate
repeat 15 times end tell
delay 0.5 delay 0.5
tell application "System Events" tell application "System Events"
if frontmost of process "Acrobat Reader Windows" is true then exit repeat if frontmost of process "Acrobat Reader Windows" is true then exit repeat

View File

@@ -36,6 +36,52 @@ export interface BadgeTemplateCfg {
// Leave unset (or "0") for no bleed. // Leave unset (or "0") for no bleed.
bleed?: string; bleed?: string;
// Header image vertical offset. CSS length applied as margin-top on the badge_header div.
// Default (unset) = "2rem" (matches the prior hardcoded mt-8).
// Negative values shift the image toward the top edge; larger values push it down.
// Any CSS length works: "-0.5in", "1rem", "8px".
header_margin_top?: string;
// Border drawn below the badge header image. Set header_border_color to enable.
// Unset = no border (default). Any valid CSS hex color.
header_border_color?: string;
// Thickness of the header bottom border. Any CSS length. Default "2px" when color is set.
header_border_width?: string;
// Per-side padding of the badge_header div. Any CSS length. Unset = 0.5rem (Tailwind p-2 default).
// Bottom padding creates space between the header image and the border line (e.g. "1.45in").
header_padding_top?: string;
header_padding_right?: string;
header_padding_bottom?: string;
header_padding_left?: string;
// Punch-out hole markers: show X overlays at the physical badge clip slot positions.
// Slots are pre-perforated on the badge stock — markers guide attendees to push them out.
// Hole dimensions: 5/8in wide × 1/8in tall, 1/4in from top, 3/8in from left/right edges.
// Center hole: horizontally centered, same vertical position.
// Colors: per-slot _fg/_bg override the shared fg/bg fallback. Unset = component defaults.
// fg = stroke + line color (hex). bg = rectangle fill color (hex).
punch_holes?: {
left?: boolean;
right?: boolean;
center?: boolean;
fg?: string; // shared fallback stroke/line color
bg?: string; // shared fallback fill color
left_fg?: string;
left_bg?: string;
left_rainbow?: boolean; // animated hue-rotate; overrides fg/bg base color with saturated red
right_fg?: string;
right_bg?: string;
right_rainbow?: boolean;
center_fg?: string;
center_bg?: string;
center_rainbow?: boolean;
slow_pulse?: boolean; // when true: slow breathing pulse instead of fast linear cycle
// Extra horizontal inset per side (mm) beyond the 1mm base safety margin.
// Shrinks the visible marker width to keep it inside the physical hole on
// printers or badge stock with variance. Default 2 when unset (see view component).
inset_x_mm?: number;
};
// Allow arbitrary extra keys to preserve forward-compatibility. // Allow arbitrary extra keys to preserve forward-compatibility.
[key: string]: any; [key: string]: any;
} }

View File

@@ -4,6 +4,7 @@ import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie'; import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import type { ae_Journal } from '$lib/types/ae_types'; import type { ae_Journal } from '$lib/types/ae_types';
@@ -885,9 +886,16 @@ export async function process_ae_obj__journal_props({
const updated = const updated =
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString(); obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
obj.tmp_sort_3 = `${obj.group ?? '0'}_${obj.priority ? 1 : 0}_${obj.sort ?? '0'}_${ const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
obj.name prefix: [obj.group ?? '0'],
}_${updated}`; priority: obj.priority,
sort: obj.sort,
fields_2: [obj.name],
fields_3: [updated]
});
obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
obj.tmp_sort_3 = tmp_sort_3;
obj.combined_passcode = `${obj.passcode ?? ''}:${obj.private_passcode ?? ''}`; obj.combined_passcode = `${obj.passcode ?? ''}:${obj.private_passcode ?? ''}`;
return obj; return obj;

View File

@@ -4,6 +4,7 @@ import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie'; import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_journals } from '$lib/ae_journals/db_journals'; import { db_journals } from '$lib/ae_journals/db_journals';
import type { ae_JournalEntry } from '$lib/types/ae_types'; import type { ae_JournalEntry } from '$lib/types/ae_types';
@@ -1050,19 +1051,20 @@ export async function process_ae_obj__journal_entry_props({
obj.history = history; obj.history = history;
obj.history_md_html = history_md_html; obj.history_md_html = history_md_html;
// Journal entry-specific computed sort fields, overriding generic ones if needed // Journal entry-specific computed sort fields via build_tmp_sort.
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); // Order: priority DESC → sort ASC → name ASC → updated ASC (all ascending, no .reverse())
const updated = const updated =
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString(); obj.updated_on ?? obj.created_on ?? new Date(0).toISOString();
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ const { tmp_sort_1, tmp_sort_2, tmp_sort_3 } = build_tmp_sort({
sort_val prefix: [obj.group ?? ''],
}_${updated}`; priority: obj.priority,
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ sort: obj.sort,
sort_val fields_2: [obj.name],
}_${obj.name ?? ''}_${updated}`; fields_3: [updated]
obj.tmp_sort_3 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ });
sort_val obj.tmp_sort_1 = tmp_sort_1;
}_${obj.name ?? ''}_${updated}`; obj.tmp_sort_2 = tmp_sort_2;
obj.tmp_sort_3 = tmp_sort_3;
return obj; return obj;
} }

View File

@@ -75,8 +75,10 @@ export function journal_entry_matches_search(
} }
export function journal_entry_compare_for_list(a: any, b: any): number { export function journal_entry_compare_for_list(a: any, b: any): number {
// tmp_sort_1 is built by build_tmp_sort() for ascending comparison:
// priority=true encodes as '0', priority=false as '1', so ASC puts priority first.
return ( return (
(b?.tmp_sort_1 ?? '').localeCompare(a?.tmp_sort_1 ?? '') || (a?.tmp_sort_1 ?? '').localeCompare(b?.tmp_sort_1 ?? '') ||
(b?.updated_on ?? '').localeCompare(a?.updated_on ?? '') || (b?.updated_on ?? '').localeCompare(a?.updated_on ?? '') ||
(b?.journal_entry_id ?? '').localeCompare(a?.journal_entry_id ?? '') (b?.journal_entry_id ?? '').localeCompare(a?.journal_entry_id ?? '')
); );

View File

@@ -1,5 +1,6 @@
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie'; import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_posts } from '$lib/ae_posts/db_posts'; import { db_posts } from '$lib/ae_posts/db_posts';
@@ -570,15 +571,16 @@ export async function process_ae_obj__post_props({
if (!obj.account_id_random) obj.account_id_random = account_id; if (!obj.account_id_random) obj.account_id_random = account_id;
} }
obj.name = obj.title; obj.name = obj.title;
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
const updated = prefix: [obj.group ?? ''],
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString(); priority: obj.priority,
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ sort: obj.sort,
sort_val fields_1: [obj.updated_on ?? obj.created_on ?? ''],
}_${updated}`; fields_2: [obj.updated_on ?? '', obj.created_on ?? ''],
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ pad_width: 8
sort_val });
}_${obj.updated_on ?? ''}_${obj.created_on ?? ''}`; obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
return obj; return obj;
} }

View File

@@ -1,5 +1,6 @@
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { api } from '$lib/api/api'; import { api } from '$lib/api/api';
import { build_tmp_sort } from '$lib/ae_core/core__idb_sort';
import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie'; import { db_save_ae_obj_li__ae_obj } from '$lib/ae_core/core__idb_dexie';
import { db_posts } from '$lib/ae_posts/db_posts'; import { db_posts } from '$lib/ae_posts/db_posts';
@@ -383,15 +384,16 @@ export async function process_ae_obj__post_comment_props({
obj_type: 'post_comment', obj_type: 'post_comment',
log_lvl, log_lvl,
specific_processor: (obj) => { specific_processor: (obj) => {
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0'); const { tmp_sort_1, tmp_sort_2 } = build_tmp_sort({
const updated = prefix: [obj.group ?? ''],
obj.updated_on ?? obj.created_on ?? new Date(0).toISOString(); priority: obj.priority,
obj.tmp_sort_1 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ sort: obj.sort,
sort_val fields_1: [obj.updated_on ?? obj.created_on ?? ''],
}_${updated}`; fields_2: [obj.updated_on ?? '', obj.created_on ?? ''],
obj.tmp_sort_2 = `${obj.group ?? ''}_${obj.priority ? '1' : '0'}_${ pad_width: 8
sort_val });
}_${obj.updated_on ?? ''}_${obj.created_on ?? ''}`; obj.tmp_sort_1 = tmp_sort_1;
obj.tmp_sort_2 = tmp_sort_2;
return obj; return obj;
} }

View File

@@ -27,7 +27,11 @@ export const iso_datetime_formatter = function iso_datetime_formatter(
named_format: string = 'datetime_iso_no_seconds', // date_iso, datetime_iso_no_seconds named_format: string = 'datetime_iso_no_seconds', // date_iso, datetime_iso_no_seconds
// Pass true/false to resolve to the correct 12h or 24h variant automatically. // Pass true/false to resolve to the correct 12h or 24h variant automatically.
// null (default) leaves named_format unchanged — all existing call sites unaffected. // null (default) leaves named_format unchanged — all existing call sites unaffected.
use_12h: boolean | null = null use_12h: boolean | null = null,
// When true, treats a naive datetime string (no Z / offset) as UTC so dayjs
// converts it to local browser time on display. Use for timestamps stored as
// UTC in the DB but returned without a timezone indicator.
treat_as_utc: boolean = false
) { ) {
// console.log('*** iso_datetime_formatter() ***'); // console.log('*** iso_datetime_formatter() ***');
@@ -74,6 +78,12 @@ export const iso_datetime_formatter = function iso_datetime_formatter(
raw_datetime = new Date(); // Get the current datetime if one was not passed. raw_datetime = new Date(); // Get the current datetime if one was not passed.
} }
// Append 'Z' to naive UTC strings so dayjs converts to local browser time.
// Guards against double-appending if the backend ever adds timezone info.
if (treat_as_utc && typeof raw_datetime === 'string' && !raw_datetime.match(/Z$|[+-]\d{2}:?\d{2}$/)) {
raw_datetime = raw_datetime + 'Z';
}
if (use_12h !== null) { if (use_12h !== null) {
named_format = use_12h named_format = use_12h
? (TO_12H[named_format] ?? named_format) ? (TO_12H[named_format] ?? named_format)

View File

@@ -1,4 +1,4 @@
import * as Lucide from 'lucide-svelte'; import * as Lucide from '@lucide/svelte';
/** /**
* Returns a Lucide icon component based on the provided file extension. * Returns a Lucide icon component based on the provided file extension.

View File

@@ -30,7 +30,6 @@ import {
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
// import { core_func } from '$lib/ae_core/ae_core_functions'; // import { core_func } from '$lib/ae_core/ae_core_functions';
// Ideally the Event related stores should not be imported here? // Ideally the Event related stores should not be imported here?
import { events_loc } from '$lib/stores/ae_events_stores';
import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte'; import { pres_mgmt_loc } from '$lib/stores/ae_events_stores__pres_mgmt.svelte';
// import { db_events } from "$lib/db_events"; // import { db_events } from "$lib/db_events";

File diff suppressed because it is too large Load Diff

View File

@@ -165,9 +165,25 @@ $effect(() => {
$effect(() => { $effect(() => {
const account_id = $slct.account_id; const account_id = $slct.account_id;
const api_ready = !!$ae_api?.base_url; const api_ready = !!$ae_api?.base_url;
const entry = $lq__ds_obj; const entry = $lq__ds_obj as ae_DataStore | null | undefined;
if (browser && api_ready && !entry && ds_loading_status === 'starting') { // Don't fire until the bootstrap Sync Effect has set a real account_id.
// Without this guard, the fetch runs with null account_id and the
// localStorage scavenge in get_object picks up a stale account_id from a
// previous session, returning the wrong account's record.
if (!browser || !account_id || !api_ready || ds_loading_status !== 'starting') return;
// Also reload when IDB has a record but it belongs to a different account
// (not null/global and not the current account). This handles the case where
// a previous dev/demo session left account-specific rows in IDB that score
// as the "best" liveQuery match even though they are for the wrong account.
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__ds__code'; trigger = 'load__ds__code';
} }
}); });

View File

@@ -16,9 +16,7 @@ import {
slct, slct,
slct_trigger slct_trigger
} from '$lib/stores/ae_stores'; } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import { import {
events_loc,
events_sess, events_sess,
events_slct, events_slct,
events_trigger events_trigger
@@ -79,6 +77,9 @@ let ae_promises: key_val = $state({});
let ae_tmp: key_val = $state({}); let ae_tmp: key_val = $state({});
ae_tmp.show__file_li = true; ae_tmp.show__file_li = true;
ae_tmp.show__direct_download = pres_mgmt_loc.current.show__direct_download; ae_tmp.show__direct_download = pres_mgmt_loc.current.show__direct_download;
// Strip :443 from https URLs — redundant and clutters shareable links.
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
// let ae_triggers: key_val = {}; // let ae_triggers: key_val = {};
onMount(() => { onMount(() => {
@@ -151,30 +152,15 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
<div class="float-right flex flex-row items-center"> <div class="float-right flex flex-row items-center">
<button <button
type="button" type="button"
onclick={() => { onclick={async () => {
console.log('*** Refresh button clicked ***'); await events_func.load_ae_obj_li__event_file({
db_events.file.clear();
// let params = {
// qry__enabled: 'all',
// qry__hidden: 'all',
// }
events_func.load_ae_obj_li__event_file({
api_cfg: $ae_api, api_cfg: $ae_api,
for_obj_type: link_to_type, for_obj_type: link_to_type,
for_obj_id: link_to_id, for_obj_id: link_to_id,
enabled: 'all', enabled: 'all',
hidden: 'all', hidden: 'all',
// params: params,
try_cache: true try_cache: true
}); });
// ae_tmp.show__file_li = false;
// console.log(`$lq__event_file_obj_li:`, $lq__event_file_obj_li);
// $slct_trigger = 'load__event_file_obj_li';
// ae_tmp.show__file_li = true;
}} }}
class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-warning border-warning-500 m-1 border p-1 transition hover:transition-all *:hover:inline" class="btn btn-sm preset-tonal-tertiary hover:preset-tonal-warning border-warning-500 m-1 border p-1 transition hover:transition-all *:hover:inline"
class:hidden={!$ae_loc.edit_mode || !$ae_loc.authenticated_access} class:hidden={!$ae_loc.edit_mode || !$ae_loc.authenticated_access}
@@ -333,7 +319,7 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
</span> </span>
<MyClipboard <MyClipboard
value={encodeURI( value={encodeURI(
`${$ae_api.base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.filename)}&key=${$ae_api.account_id}` `${base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.filename)}&key=${$ae_api.account_id}`
)} )}
btn_text="Copy Original" btn_text="Copy Original"
btn_title="Copy the direct download link to the clipboard." btn_title="Copy the direct download link to the clipboard."
@@ -342,10 +328,10 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
<MyClipboard <MyClipboard
value={encodeURI( value={encodeURI(
`${$ae_api.base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${event_file_obj?.event_session_code}-${ae_util.clean_filename(event_file_obj?.event_session_name).substring(0, 20)}-${ae_util.clean_filename(event_file_obj?.event_presenter_full_name)}.${event_file_obj?.extension}&key=${$ae_api.account_id}` `${base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.event_session_code ?? '')}_${ae_util.clean_filename(event_file_obj?.event_presentation_name ?? event_file_obj?.event_session_name ?? '').substring(0, 30)}_${ae_util.clean_filename(event_file_obj?.event_presenter_full_name ?? '')}.${event_file_obj?.extension}&key=${$ae_api.account_id}`
)} )}
btn_text="Copy Renamed" btn_text="Copy Renamed"
btn_title="Copy the renamed download link to the clipboard." btn_title="Copy the renamed download link to the clipboard. Format: [session-code]_[presentation-name]_[presenter-name].[ext]"
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500" btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500"
></MyClipboard> ></MyClipboard>
</div> </div>

View File

@@ -1,100 +1,26 @@
// store_versions MUST be first import — its side-effect wipes stale localStorage
// before svelte-persisted-store hydrates from it.
import { AE_EVENTS_LOC_VERSION } from '$lib/stores/store_versions';
import { persisted } from 'svelte-persisted-store';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import type { key_val } from '$lib/stores/ae_stores'; import type { key_val } from '$lib/stores/ae_stores';
import { // Static display title for the Events module. Always this value — nothing writes to it.
badges_loc_defaults, // Use this constant instead of $events_loc.title (which is being retired).
badges_sess_defaults export const EVENTS_MODULE_TITLE = `OSIT's Æ Events`;
} from '$lib/stores/ae_events_stores__badges_defaults';
import { import { badges_sess_defaults } from '$lib/stores/ae_events_stores__badges_defaults';
launcher_loc_defaults, import { launcher_sess_defaults } from '$lib/stores/ae_events_stores__launcher_defaults';
launcher_sess_defaults import { leads_sess_defaults } from '$lib/stores/ae_events_stores__leads_defaults';
} from '$lib/stores/ae_events_stores__launcher_defaults'; import { pres_mgmt_sess_defaults } from '$lib/stores/ae_events_stores__pres_mgmt_defaults';
import {
leads_loc_defaults,
leads_sess_defaults
} from '$lib/stores/ae_events_stores__leads_defaults';
import {
pres_mgmt_loc_defaults,
pres_mgmt_sess_defaults
} from '$lib/stores/ae_events_stores__pres_mgmt_defaults';
// Deployment version stamp. Compared against events_sess.ver in events/+layout.svelte // Deployment version stamp. Compared against events_sess.ver in events/+layout.svelte
// to detect stale persisted data after a deploy (triggers a reload). Bump this alongside // to detect stale persisted data after a deploy (triggers a reload). Bump this alongside
// events_session_data_struct.ver. See store_versions.ts for the schema-level wipe mechanism. // events_session_data_struct.ver. See store_versions.ts for the schema-level wipe mechanism.
const ver = '2025-10-16_2139'; const ver = '2025-10-16_2139';
/* *** BEGIN *** Initialize events_local_data_struct */
// Persisted to localStorage. Retains user preferences and event-specific config across
// browser sessions. See store_versions.ts for the schema-level invalidation mechanism.
const events_local_data_struct: key_val = {
__version: AE_EVENTS_LOC_VERSION, // Schema version gate — see store_versions.ts
ver: ver, // Deployment stamp — compared against events_sess.ver to trigger reloads.
name: 'Aether - Events',
title: `OSIT's Æ Events`,
ds: {},
events_cfg_json: {},
event_id: null,
// all, disabled, enabled
qry__enabled: 'enabled',
// all, hidden, not_hidden
qry__hidden: 'not_hidden',
qry__limit: 20,
qry__offset: 0,
// The show details is intended for things like meta data and additional details that are not always needed.
show_details: false,
auth__person: {}, // allow, id, name, email, passcode, etc
// The auth__entered_key (usually email or person_id) and auth__entered_passcode is found under events_sess.entered_key and events_sess.entered_passcode because it should be temporary.
// auth__entered_passcode: null,
// auth__kv tracks which IDs the browser client is permitted to access.
// Each entry is an ID mapped to true, false, 'read', or 'write'.
// Keys should be no older than a configurable max age (checked on read).
auth__kv: {
event: {},
exhibit: {},
location: {},
session: {},
presentation: {},
presenter: {},
person: {}
},
// Badge Printing — see ae_events_stores__badges_defaults.ts
badges: badges_loc_defaults,
// Event Presentation Launcher — see ae_events_stores__launcher_defaults.ts
launcher: launcher_loc_defaults,
// Lead Retrievals (Exhibit) — see ae_events_stores__leads_defaults.ts
leads: leads_loc_defaults,
// Presentation Management — see ae_events_stores__pres_mgmt_defaults.ts
pres_mgmt: pres_mgmt_loc_defaults
};
export const events_loc: Writable<key_val> = persisted(
'ae_events_loc',
events_local_data_struct
);
/* *** BEGIN *** Initialize events_session_data_struct */ /* *** BEGIN *** Initialize events_session_data_struct */
// In-memory only (writable, not persisted). Resets on page load. // In-memory only (writable, not persisted). Resets on page load.
const events_session_data_struct: key_val = { const events_session_data_struct: key_val = {
// Deployment stamp — compared against events_loc.ver in events/+layout.svelte. // Deployment version stamp — bump alongside ver above when pushing breaking changes.
ver: ver, ver: ver,
log_lvl: 1, log_lvl: 1,
@@ -102,7 +28,6 @@ const events_session_data_struct: key_val = {
ds: { ds: {
submit_status: null submit_status: null
}, },
ds_loaded: {},
qry__enabled: 'enabled', // all, disabled, enabled qry__enabled: 'enabled', // all, disabled, enabled
qry__hidden: 'not_hidden', // all, hidden, not_hidden qry__hidden: 'not_hidden', // all, hidden, not_hidden
@@ -162,31 +87,14 @@ const events_slct_obj_template: key_val = {
event_obj_li: [], event_obj_li: [],
// Sub-level event_ // Sub-level event_
abstract_id: null,
abstract_obj: {},
abstract_obj_li: [],
badge_id: null, badge_id: null,
badge_obj: {}, badge_obj: {},
badge_obj_li: [], badge_obj_li: [],
badge_template_id: null,
badge_template_obj: {},
badge_template_obj_li: [],
device_id: null,
device_obj: {},
device_obj_li: [],
exhibit_id: null, exhibit_id: null,
exhibit_obj: {}, exhibit_obj: {},
exhibit_obj_li: [], exhibit_obj_li: [],
// Rename these to badge_tracking_*?
exhibit_tracking_id: null,
exhibit_tracking_obj: {},
exhibit_tracking_obj_li: [],
file_id: null, file_id: null,
file_obj: {}, file_obj: {},
file_obj_li: [], file_obj_li: [],
@@ -218,8 +126,6 @@ const events_slct_obj_template: key_val = {
session_obj_li: [], session_obj_li: [],
event_session_obj: {}, event_session_obj: {},
lq__presenter_obj: {}, // Testing passing a LiveQuery object around...
auth__event_presenter_id: null, auth__event_presenter_id: null,
auth__event_presentation_id: null auth__event_presentation_id: null
}; };

View File

@@ -0,0 +1,30 @@
import { PersistedState } from 'runed';
const events_auth_loc_defaults = {
auth__person: {
id: null as string | null,
person_id: null as string | null,
entered_key: null as string | null,
email: null as string | null,
full_name: null as string | null,
presenter_id: null as string | null,
presentation_id: null as string | null,
session_id: null as string | null
},
auth__kv: {
event: {} as Record<string, any>,
exhibit: {} as Record<string, any>,
location: {} as Record<string, any>,
session: {} as Record<string, any>,
presentation: {} as Record<string, any>,
presenter: {} as Record<string, any>,
person: {} as Record<string, any>
}
};
export const events_auth_loc = new PersistedState('ae_events_auth_loc', events_auth_loc_defaults, {
serializer: {
serialize: JSON.stringify,
deserialize: (raw: string) => ({ ...events_auth_loc_defaults, ...JSON.parse(raw) })
}
});

View File

@@ -15,4 +15,11 @@
import { PersistedState } from 'runed'; import { PersistedState } from 'runed';
import { badges_loc_defaults } from './ae_events_stores__badges_defaults'; import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults); // Custom deserializer merges stored JSON with defaults so that new fields added
// after a user's first session get their default values rather than undefined.
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults, {
serializer: {
serialize: JSON.stringify,
deserialize: (raw: string) => ({ ...badges_loc_defaults, ...JSON.parse(raw) })
}
});

View File

@@ -97,8 +97,8 @@ export const default_trusted_can_edit: string[] = [
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface BadgesLocState { export interface BadgesLocState {
auto_view: boolean; auto_view: boolean;
show_hidden: boolean; // 'default' = hide hidden+disabled | 'show_hidden' = show hidden | 'show_all' = show hidden+disabled
show_not_enabled: boolean; visibility_filter: 'default' | 'show_hidden' | 'show_all';
show_printed: boolean; show_printed: boolean;
allow_reprint: boolean; allow_reprint: boolean;
show_element__cfg: boolean; show_element__cfg: boolean;
@@ -110,6 +110,7 @@ export interface BadgesLocState {
qry_printed_status: string; // 'all' | 'printed' | 'not_printed' qry_printed_status: string; // 'all' | 'printed' | 'not_printed'
qry_affiliations: string | null; qry_affiliations: string | null;
qry_sort_order: string; qry_sort_order: string;
qry_result_limit: number; // UI override for max results (edit mode only; tier max enforced by stepper)
status_qry__search: string | null; status_qry__search: string | null;
use_id_li: boolean; use_id_li: boolean;
search_status: string | null; search_status: string | null;
@@ -137,6 +138,10 @@ export interface BadgesLocState {
trusted_search_min_chars: number; trusted_search_min_chars: number;
// Timestamp when the remote config was last mirrored locally // Timestamp when the remote config was last mirrored locally
remote_cfg_last_synced_on: string | null; remote_cfg_last_synced_on: string | null;
// After-print navigation method. true (default) = SvelteKit goto() — faster, no full reload.
// false = window.location.href — full page reload, use as a fallback if goto causes issues.
// Per-device: stored in localStorage, not synced to the event config.
print_nav_use_goto: boolean;
} }
export interface BadgesSessState { export interface BadgesSessState {
@@ -158,8 +163,7 @@ export interface BadgesSessState {
export const badges_loc_defaults: BadgesLocState = { export const badges_loc_defaults: BadgesLocState = {
auto_view: true, auto_view: true,
show_hidden: false, // Hidden (archived) badges are excluded from the main list. visibility_filter: 'default', // 'default' | 'show_hidden' | 'show_all'
show_not_enabled: false,
show_printed: false, show_printed: false,
allow_reprint: false, allow_reprint: false,
@@ -178,6 +182,7 @@ export const badges_loc_defaults: BadgesLocState = {
qry_printed_status: 'all', // 'all' | 'printed' | 'not_printed' qry_printed_status: 'all', // 'all' | 'printed' | 'not_printed'
qry_affiliations: null, // null = no affiliation filter qry_affiliations: null, // null = no affiliation filter
qry_sort_order: '', // '' = default sort order qry_sort_order: '', // '' = default sort order
qry_result_limit: 25,
status_qry__search: null, status_qry__search: null,
use_id_li: true, use_id_li: true,
@@ -202,7 +207,8 @@ export const badges_loc_defaults: BadgesLocState = {
auth_search_min_chars: 2, auth_search_min_chars: 2,
trusted_search_result_limit: 150, trusted_search_result_limit: 150,
trusted_search_min_chars: 1, trusted_search_min_chars: 1,
remote_cfg_last_synced_on: null remote_cfg_last_synced_on: null,
print_nav_use_goto: true
}; };
// In-memory badge state — resets on page load. // In-memory badge state — resets on page load.

View File

@@ -0,0 +1,9 @@
import { PersistedState } from 'runed';
import { launcher_loc_defaults } from './ae_events_stores__launcher_defaults';
export const launcher_loc = new PersistedState('ae_launcher_loc', launcher_loc_defaults, {
serializer: {
serialize: JSON.stringify,
deserialize: (raw: string) => ({ ...launcher_loc_defaults, ...JSON.parse(raw) })
}
});

View File

@@ -7,6 +7,8 @@
* launcher_sess_defaults → events_sess.launcher (in-memory, resets on page load) * launcher_sess_defaults → events_sess.launcher (in-memory, resets on page load)
*/ */
import type { LaunchProfile } from '$lib/ae_events/ae_launcher__default_launch_profiles';
/** 3-way section collapse state used throughout the Launcher UI. */ /** 3-way section collapse state used throughout the Launcher UI. */
export type SectionState = 'collapsed' | 'auto' | 'pinned'; export type SectionState = 'collapsed' | 'auto' | 'pinned';
@@ -91,6 +93,18 @@ export interface LauncherLocState {
* device/OS without deploying to the Mac laptop. * device/OS without deploying to the Mac laptop.
*/ */
native_test_mode: boolean; native_test_mode: boolean;
/**
* Per-file display mode overrides, keyed by event_file_id.
* Stored locally (per-device) because event_file has no cfg_json column yet.
* TODO: migrate to event_file.cfg_json once the backend column is added.
*/
file_display_overrides: Record<string, 'extend' | 'mirror' | 'none'>;
/**
* Local per-device launch profile overrides, keyed by profile/extension name.
* Overrides DEFAULT_LAUNCH_PROFILES for this device only.
* Priority: device API config > this local override > built-in defaults.
*/
launch_profiles: Record<string, Partial<LaunchProfile>> | null;
} }
export interface LauncherSessState { export interface LauncherSessState {
@@ -119,6 +133,9 @@ export interface LauncherSessState {
trigger_reload__event_session_obj_li: string | null; trigger_reload__event_session_obj_li: string | null;
trigger_reload__event_location_obj_id: string | null; trigger_reload__event_location_obj_id: string | null;
trigger_reload__event_location_obj_li: string | null; trigger_reload__event_location_obj_li: string | null;
trigger__force_location_sync: any;
trigger__ws_connect: any; trigger__ws_connect: any;
trigger__ws_disconnect: any; trigger__ws_disconnect: any;
} }
@@ -216,7 +233,9 @@ export const launcher_loc_defaults: LauncherLocState = {
controller_client_id: null, controller_client_id: null,
native_test_mode: false, native_test_mode: false,
wallpaper_applied_url: null, wallpaper_applied_url: null,
wallpaper_applied_url_external: null wallpaper_applied_url_external: null,
file_display_overrides: {},
launch_profiles: null
// controller_cmd: null, // controller_cmd: null,
// controller_trigger_send: null, // controller_trigger_send: null,
}; };
@@ -254,6 +273,8 @@ export const launcher_sess_defaults: LauncherSessState = {
trigger_reload__event_location_obj_id: null, trigger_reload__event_location_obj_id: null,
trigger_reload__event_location_obj_li: null, trigger_reload__event_location_obj_li: null,
trigger__force_location_sync: null,
trigger__ws_connect: null, trigger__ws_connect: null,
trigger__ws_disconnect: null trigger__ws_disconnect: null
}; };

View File

@@ -18,4 +18,9 @@
import { PersistedState } from 'runed'; import { PersistedState } from 'runed';
import { leads_loc_defaults } from './ae_events_stores__leads_defaults'; import { leads_loc_defaults } from './ae_events_stores__leads_defaults';
export const leads_loc = new PersistedState('ae_leads_loc', leads_loc_defaults); export const leads_loc = new PersistedState('ae_leads_loc', leads_loc_defaults, {
serializer: {
serialize: JSON.stringify,
deserialize: (raw: string) => ({ ...leads_loc_defaults, ...JSON.parse(raw) })
}
});

View File

@@ -8,6 +8,7 @@
export interface LeadsLocState { export interface LeadsLocState {
__version: number; __version: number;
show_details: boolean; // Show extra metadata/details in the leads tab (was events_loc.show_details)
show_option__paid_tab: boolean; show_option__paid_tab: boolean;
show_content__scan_alert: boolean; show_content__scan_alert: boolean;
show_content__scan_requirements: boolean; show_content__scan_requirements: boolean;
@@ -80,6 +81,7 @@ export interface LeadsSessState {
// Persisted leads config — survives browser sessions. // Persisted leads config — survives browser sessions.
export const leads_loc_defaults: LeadsLocState = { export const leads_loc_defaults: LeadsLocState = {
__version: 1, __version: 1,
show_details: false, // Show extra metadata/details in the leads tab (was events_loc.show_details)
show_option__paid_tab: true, show_option__paid_tab: true,
show_content__scan_alert: true, // Workaround for QR scanner edge-case bug. show_content__scan_alert: true, // Workaround for QR scanner edge-case bug.
show_content__scan_requirements: true, show_content__scan_requirements: true,

View File

@@ -15,4 +15,9 @@
import { PersistedState } from 'runed'; import { PersistedState } from 'runed';
import { pres_mgmt_loc_defaults } from './ae_events_stores__pres_mgmt_defaults'; import { pres_mgmt_loc_defaults } from './ae_events_stores__pres_mgmt_defaults';
export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults); export const pres_mgmt_loc = new PersistedState('ae_pres_mgmt_loc', pres_mgmt_loc_defaults, {
serializer: {
serialize: JSON.stringify,
deserialize: (raw: string) => ({ ...pres_mgmt_loc_defaults, ...JSON.parse(raw) })
}
});

View File

@@ -244,6 +244,7 @@ export interface PresMgmtSessState {
recent_files: string | null; recent_files: string | null;
presenters_agree: string | null; presenters_agree: string | null;
presenters_biography: string | null; presenters_biography: string | null;
file_downloads: string | null;
}; };
rpt__session_no_files: boolean; rpt__session_no_files: boolean;
rpt__session_poc_agree: boolean; rpt__session_poc_agree: boolean;
@@ -414,7 +415,8 @@ export const pres_mgmt_sess_defaults: PresMgmtSessState = {
status_rpt: { status_rpt: {
recent_files: null, recent_files: null,
presenters_agree: null, presenters_agree: null,
presenters_biography: null presenters_biography: null,
file_downloads: null
}, },
rpt__session_no_files: true, rpt__session_no_files: true,
rpt__session_poc_agree: false, rpt__session_poc_agree: false,

View File

@@ -23,30 +23,12 @@ const idaa_local_data_struct: key_val = {
// Timestamp (ms since epoch) when the last successful verification occurred. // Timestamp (ms since epoch) when the last successful verification occurred.
// Used to cache verification results and avoid repeated Novi API calls. // Used to cache verification results and avoid repeated Novi API calls.
novi_verified_ts: null, novi_verified_ts: null,
// If set to a ms timestamp, verification attempts should be skipped until this time.
// Used to honor rate-limits and Retry-After behavior.
novi_rate_limited_until: null,
// Populated from $ae_loc.site_cfg_json at IDAA layout mount — not managed here. // Populated from $ae_loc.site_cfg_json at IDAA layout mount — not managed here.
// See routes/idaa/(idaa)/+layout.svelte for the override logic. // See routes/idaa/(idaa)/+layout.svelte for the override logic.
novi_admin_li: [], novi_admin_li: [],
novi_trusted_li: [], novi_trusted_li: [],
novi_jitsi_mod_li: [], novi_jitsi_mod_li: [],
novi_archives_base_url: 'https://www.idaa.org/idaa-archives',
novi_bb_base_url: 'https://www.idaa.org/idaa-bulletin-board',
novi_meetings_base_url: 'https://www.idaa.org/idaa-meetings',
ds: {},
idaa_cfg_json: {},
// all, disabled, enabled
qry__enabled: 'enabled',
// all, hidden, not_hidden
qry__hidden: 'not_hidden',
qry__limit: 20,
qry__offset: 0,
archives: { archives: {
enabled: 'enabled', // all, disabled, enabled enabled: 'enabled', // all, disabled, enabled
hidden: 'not_hidden', // all, hidden, not_hidden hidden: 'not_hidden', // all, hidden, not_hidden

View File

@@ -0,0 +1,149 @@
import { PersistedState } from 'runed';
// ---- Types -------------------------------------------------------------------
interface IdaaArchivesLoc {
enabled: string; // 'all' | 'disabled' | 'enabled'
hidden: string; // 'all' | 'hidden' | 'not_hidden'
limit: number;
offset: number;
edit_kv: Record<string, any>; // Tracks which archive objects are being edited.
edit__archive_obj: any;
edit__archive_content_obj: any;
}
interface IdaaBbLoc {
enabled: string; // 'all' | 'disabled' | 'enabled'
hidden: string; // 'all' | 'hidden' | 'not_hidden'
limit: number;
offset: number;
edit_kv: Record<string, any>; // Tracks which post objects are being edited.
edit__post_obj: any;
edit__post_comment_obj: any;
show_list__post_obj_li: boolean;
qry__enabled: string;
qry__hidden: string;
qry__limit: number;
qry__offset: number;
qry__order_by: string; // For the IDB index query
qry__order_by_li: Record<string, string>; // For the SQL query
}
interface IdaaRecoveryMeetingsLoc {
edit_kv: Record<string, any>; // Tracks which meeting objects are being edited.
edit__event_obj: any;
qry__enabled: string;
qry__hidden: string;
qry__limit: number;
qry__order_by: string; // For the IDB index query; name, updated_on/created_on
qry__order_by_li: Record<string, string>; // For the SQL query
qry__offset: number;
qry__fulltext_str: string | null;
qry__physical: string | null;
qry__type: string | null;
qry__virtual: string | null;
// When true, only show meetings the member has starred. Favorites are stored
// server-side in event.mod_meetings_json.favorite (array of Novi UUIDs), so
// they persist across browsers without requiring a Novi API write capability.
qry__favorites_only: boolean;
// Collapse the "Meeting Info" data store panel. Persisted so preference survives reloads.
ds_info_collapsed: boolean;
}
export interface IdaaLocState {
novi_uuid: string | null;
novi_email: string | null;
novi_full_name: string | null;
// True after a successful Novi API verification (UUID confirmed to be a real Novi member).
// False on load, on verification failure, or for non-Novi sign-in paths.
novi_verified: boolean;
// Timestamp (ms since epoch) when the last successful verification occurred.
// Used to cache verification results and avoid repeated Novi API calls.
novi_verified_ts: number | null;
// Populated from $ae_loc.site_cfg_json at IDAA layout mount — not managed here.
// See routes/idaa/(idaa)/+layout.svelte for the override logic.
novi_admin_li: string[];
novi_trusted_li: string[];
novi_jitsi_mod_li: string[];
archives: IdaaArchivesLoc;
bb: IdaaBbLoc;
recovery_meetings: IdaaRecoveryMeetingsLoc;
}
// ---- Defaults ----------------------------------------------------------------
export const idaa_loc_defaults: IdaaLocState = {
novi_uuid: null,
novi_email: null,
novi_full_name: null,
novi_verified: false,
novi_verified_ts: null,
novi_admin_li: [],
novi_trusted_li: [],
novi_jitsi_mod_li: [],
archives: {
enabled: 'enabled',
hidden: 'not_hidden',
limit: 150,
offset: 0,
edit_kv: {},
edit__archive_obj: null,
edit__archive_content_obj: null
},
bb: {
enabled: 'enabled',
hidden: 'not_hidden',
limit: 50,
offset: 0,
edit_kv: {},
edit__post_obj: null,
edit__post_comment_obj: null,
show_list__post_obj_li: true,
qry__enabled: 'enabled',
qry__hidden: 'not_hidden',
qry__limit: 25,
qry__offset: 0,
qry__order_by: 'updated_on',
qry__order_by_li: { updated_on: 'DESC', created_on: 'DESC' }
},
recovery_meetings: {
edit_kv: {},
edit__event_obj: null,
qry__enabled: 'enabled',
qry__hidden: 'not_hidden',
qry__limit: 100,
qry__order_by: 'updated_on',
qry__order_by_li: {
priority: 'DESC',
sort: 'DESC',
updated_on: 'DESC',
created_on: 'DESC',
name: 'ASC'
},
qry__offset: 0,
qry__fulltext_str: null,
qry__physical: null,
qry__type: null,
qry__virtual: null,
qry__favorites_only: false,
ds_info_collapsed: false
}
};
// ---- Store -------------------------------------------------------------------
// Note: the top-level spread in deserialize ensures new top-level fields get
// their defaults after a browser upgrade. Nested objects (archives, bb,
// recovery_meetings) are replaced wholesale from localStorage — if a new field
// is added to a nested object, it will not auto-populate until the user clears
// their storage. This is the same trade-off as the events sub-stores.
export const idaa_loc = new PersistedState('ae_idaa_loc', idaa_loc_defaults, {
serializer: {
serialize: JSON.stringify,
deserialize: (raw: string) => ({ ...idaa_loc_defaults, ...JSON.parse(raw) })
}
});

View File

@@ -91,8 +91,6 @@ const ae_app_local_data_defaults: key_val = {
qry__limit: 20, qry__limit: 20,
qry__offset: 0, qry__offset: 0,
qr_scanner_version: 'one',
admin: { admin: {
show_element__sql_qry: false, show_element__sql_qry: false,
show_element__sql_qry_results: false show_element__sql_qry_results: false
@@ -143,7 +141,6 @@ const ae_app_local_data_defaults: key_val = {
add_to_use_files_method: 'upload' // upload, select add_to_use_files_method: 'upload' // upload, select
}, },
ds: {},
hub: { hub: {
show_element__cfg: true, show_element__cfg: true,
show_element__cfg_detail: false, show_element__cfg_detail: false,
@@ -156,56 +153,6 @@ const ae_app_local_data_defaults: key_val = {
qr: {} qr: {}
}, },
mod: {
archives: {},
events: {
event_id: null,
show_edit__event_presenter_obj: false,
show_list__event_presenter_obj_li: true,
show_view__event_presenter_obj: false,
submit_status: null,
// When set, new presenters are automatically assigned to this session.
default_session_id: null
},
journals: {},
posts: {},
sponsorships: {
cfg_id: null,
for_type: null,
for_id: null,
// Max complimentary guests per sponsorship tier (0 = no tier).
// These are fallback defaults; the active event config should override via events_cfg_json.
level_guest_max_li: {
0: 0,
1: 4,
2: 8,
3: 8,
4: 8,
5: 8,
6: 16,
7: 16
},
show_edit__sponsorship_obj: false,
show_list__sponsorship_obj_li: true,
show_view__sponsorship_obj: false,
show_question__accommodations: false,
submit_status: null // 'saving', 'created', 'updated'
}
// testing: {},
}
}; };
export const ae_loc: Writable<key_val> = persisted( export const ae_loc: Writable<key_val> = persisted(
@@ -258,31 +205,6 @@ const ae_app_session_data_defaults: key_val = {
clip_complete: null clip_complete: null
}, },
hub: {
show_xyz: null,
account_id_qry_status: null,
event_badge_id_status_qry__search: null,
event_presenter_id_qry_status: null,
site_domain_id_qry_status: null,
sponsorship_id_qry_status: null,
sponsorship_cfg_id_qry_status: null,
qr: {}
},
mod: {
archives: {},
events: {},
journals: {},
posts: {},
sponsorships: {
disable_submit__sponsorship_obj: false,
slct__level_num: 0,
show_question__accommodations: false,
submit_status: null // 'saving', 'created', 'updated', 'saved'
},
testing: {}
},
person: { person: {
show_report__person_li: false, show_report__person_li: false,
@@ -291,7 +213,6 @@ const ae_app_session_data_defaults: key_val = {
show__modal_change_password: false, show__modal_change_password: false,
download: {},
// Per-file progress keyed by file_id (downloads) or temp_id (uploads). // Per-file progress keyed by file_id (downloads) or temp_id (uploads).
// Shape: { status, endpoint, filename, size_total, size_loaded, percent_completed } // Shape: { status, endpoint, filename, size_total, size_loaded, percent_completed }
api_download_kv: {}, api_download_kv: {},

View File

@@ -86,7 +86,7 @@ export const IDB_CONTENT_VERSIONS = {
journal_entry: 3 // 2026-05-14: removed content_md_html + history_md_html from properties_to_save journal_entry: 3 // 2026-05-14: removed content_md_html + history_md_html from properties_to_save
}, },
events: { events: {
event: 2, // Bumped 2026-05-16: force-clear stale IDB data causing "no meetings found" on IDAA event: 3, // Bumped 2026-06-03: precautionary clear after sort-direction and account_id gate fixes
event_session: 1, event_session: 1,
event_presenter: 1, event_presenter: 1,
event_badge: 1, event_badge: 1,

View File

@@ -12,6 +12,8 @@ import '../app.css';
// *** Import other supporting libraries // *** Import other supporting libraries
import { import {
RefreshCw, RefreshCw,
WifiOff,
ChevronDown,
} from '@lucide/svelte'; } from '@lucide/svelte';
// Highlight JS // Highlight JS
@@ -408,16 +410,45 @@ $effect(() => {
</svelte:head> </svelte:head>
{#if browser && (is_offline || api_unreachable)} {#if browser && (is_offline || api_unreachable)}
<div {#if show_connection_details}
class="preset-filled-error-200-800 fixed top-0 right-0 left-0 z-100 flex items-center justify-center gap-4 p-4 text-center shadow-2xl print:hidden"> <!-- Expanded banner -->
<span class="text-xl font-bold" <div
>{is_offline ? 'Offline' : api_error_msg}</span> class="preset-filled-error-200-800 fixed top-0 right-0 left-0 z-100 flex items-center justify-center gap-4 p-4 text-center shadow-2xl print:hidden">
<button <WifiOff size="1.2em" class="shrink-0 opacity-70" />
class="btn btn-sm preset-tonal-surface" <div class="flex flex-col items-center gap-0.5">
onclick={() => window.location.reload()}> <span class="text-xl font-bold leading-tight">
{is_offline ? 'Device Offline' : api_error_msg}
</span>
<span class="text-xs opacity-75">
{#if is_offline}
Cached event data is still available for search and print · Edits require network
{:else}
The Aether server could not be reached — check your connection or contact support
{/if}
</span>
</div>
<button
class="btn btn-sm preset-tonal-surface"
onclick={() => window.location.reload()}>
<RefreshCw size="1em" class="opacity-60" /> Retry <RefreshCw size="1em" class="opacity-60" /> Retry
</button> </button>
</div> <button
class="btn btn-sm preset-tonal-surface"
title="Collapse"
onclick={() => { show_connection_details = false; }}>
<ChevronDown size="1em" />
</button>
</div>
{:else}
<!-- Collapsed indicator — small chip, top-right corner -->
<button
class="preset-filled-error-200-800 fixed top-2 right-2 z-100 flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-bold shadow-lg print:hidden"
title={is_offline ? 'Device Offline — click to expand' : `${api_error_msg} click to expand`}
onclick={() => { show_connection_details = true; }}>
<WifiOff size="0.9em" class="shrink-0" />
{is_offline ? 'Offline' : 'API Error'}
</button>
{/if}
{/if} {/if}
{#if browser && flag_expired} {#if browser && flag_expired}

Some files were not shown because too many files have changed in this diff Show More