- Update default qry__limit to 100 in idaa_loc - Add 75 to limit_steps in recovery meetings query component - Bump AE_IDAA_LOC_VERSION to 2 to apply changes to existing users - Update IDAA documentation and TODO__Agents.md with SQL optimization task - Mark implemented UI/UX ideas as done in documentation
27 KiB
Aether UI/UX — Future Ideas
Collection of concrete UX improvements for the Aether frontend. Each entry includes the rationale, current behavior, proposed change, and implementation notes. Date: 2026-05-17
IDAA Recovery Meetings
1. Guided empty state with active filters — ✅ Implemented 2026-05-18
Current behavior: When filters return 0 results, the page shows: "No recovery meetings found matching your criteria." The member has no indication whether this is a bug, genuinely no data, or just overly narrow filters.
Proposed change: When filters are active AND the result count is 0, show a helpful prompt instead of the bare message:
No meetings found for these filters.
Try broadening your search or [Clear all filters →]
Implementation notes:
- Add a
has_active_filtersderived in+page.sveltethat checks whether any ofqry__physical,qry__virtual,qry__type, orqry__fulltext_stris set. - In the template's
{:else}block (line ~443), branch onhas_active_filters:true→ show the guided message + "Clear Filters" buttonfalse→ show the existing escape-hatch flow (timed "Refresh Meeting Cache" button after 8 seconds, since zero unfiltered results always indicates a problem)
- The "Clear Filters" button resets all four filter fields to
null/''and bumpssearch_versionto trigger a fresh unfiltered search. - Distinct from the
errorstate — this is a successful search (qry__status === 'done') with an empty result set.
Implemented: src/routes/idaa/(idaa)/recovery_meetings/+page.svelte. has_active_filters
derived checks qry__physical, qry__virtual, qry__type, and qry__fulltext_str. Empty
state branches on has_active_filters: active filters → guided message + "Clear Filters"
button; no active filters → existing escape-hatch flow (timed "Refresh Meeting Cache" after
8 seconds).
2. Quick-filter chips below the search bar — ✅ Implemented 2026-05-18
Current behavior: Members toggle filters via small checkboxes (Virtual, In-person) and radio buttons (All, IDAA, Caduceus, Family Recovery). These require precise mouse/tap targeting and scanning several lines of filter UI to discover and use.
Proposed change: Add a row of preset chip buttons directly below the search input:
[🖥 Virtual] [🏠 In-Person] [🩺 IDAA] [Caduceus] [Family Recovery] [All Types]
- Each chip toggles the corresponding filter (
qry__virtual,qry__physical,qry__type) and triggers an immediate search. - Selected chips get a filled/pressed style; unselected chips are outlined.
- "All Types" is the default selected state (no type filter). Clicking another type chip deselects "All Types" (radio behavior for the type dimension). Virtual and In-person are independent toggles (checkbox behavior — can select both).
- The existing checkboxes/radio buttons remain as the underlying state storage
(
$idaa_loc.recovery_meetings.*). The chips are a convenience layer — they write to the same store fields and callhandle_search_trigger().
Implementation notes:
- Place in
ae_idaa_comp__event_obj_qry.sveltebetween the search input row and the current filter rows. - Optionally hide the existing checkbox/radio filter rows when the chips are present (or keep both — the checkboxes serve as accessible form controls; the chips are the primary visual interaction).
- On mobile, chips wrap to a second row naturally with
flex-wrap.
Implemented: ae_idaa_comp__event_obj_qry.svelte. Chips replaced the old
checkbox/radio/select UI entirely rather than layering on top. Two chip rows:
Row 1 — My Meetings (first), Virtual, In-Person. Row 2 — All / IDAA / Caduceus /
Family Recovery type chips. Cycling sort button replaces separate sort options
(see item below). Max Results uses a +/− stepper. Sort and max are in a third
row below the chips, inside the same <form> constraint.
3. Language: "Searching..." vs "Loading..."
Current behavior: The loading state always shows the same message:
🔄 Searching...
This appears on initial page load (when the user hasn't typed anything) and after the user clicks Search or toggles a filter. The word "Searching" implies the user initiated a search, which is misleading on initial page load — it's a cold cache load, not an active search.
Proposed change: Distinguish the two loading contexts:
| Context | Message |
|---|---|
| Initial page load (no filters, no search text) | "Loading meetings..." |
| User clicked Search or toggled a filter | "Searching..." (keep current) |
Implementation notes:
- In
+page.sveltetemplate around line 422, check whetherqry__fulltext_stris empty AND no filter checkboxes/radios are active. If so, show "Loading meetings..."; otherwise show "Searching...". - This is purely a label change — no logic changes needed. The condition can be the
same
has_active_filtersderived from item #1. - Also update the list component's standalone loading state in
ae_idaa_comp__event_obj_li.svelteline 556-558 to use the same distinction.
4. Filter row collapsing on mobile
Current behavior: The query bar has three filter rows (Location checkboxes, Type radios, Max/Sort selects) plus the search input row and the action button row. Combined, this takes roughly 200px of vertical space. On mobile — especially inside the Novi iframe on a phone — meeting cards are pushed below the fold.
Proposed change: On viewports below md (768px), collapse the Location and Type
filter rows behind a "Filters ▾" toggle. The Max Results and Sort selects stay visible
since they're used frequently. The action buttons (Show Hidden, Create Meeting, Export)
move inside the collapsed panel or stay visible based on available width.
[Search input............................] [Search]
[Filters ▾] [Max: 150 ▾] [Sort: Last Updated ▾]
Clicking "Filters ▾" expands the panel with Location checkboxes and Type radios.
Implementation notes:
- Use a
$statebooleanshow_filters(session-only, resets on page load). - Wrap the filter rows in a
{#if show_filters}block. - Persist in
$idaa_sess.recovery_meetings.show_filters_expandedif you want the state to survive navigation within the module (same tab session). - The Tailwind
md:breakpoint works for the collapse trigger:class:hidden={!show_filters}combined withclass:md:blockto always show on desktop. - Test inside the Novi iframe — Bootstrap v3 may add its own
hiddenbehavior onmdbreakpoints that conflicts with Tailwind's.
5. Human-readable schedule line on cards
Current behavior: The meeting card displays weekdays as a flat, dense span list:
Sunday Monday Wednesday Friday
The timezone is shown separately as (America/Chicago), and the start time is in
a compact 7:00 PM format. These three pieces of information are visually separated
and require the member to mentally assemble the schedule.
Proposed change: Render a computed one-liner that combines them:
🕐 Mondays, Wednesdays, Fridays at 7:00 PM CT
- Weekday names are built from the
weekday_*booleans on the event object. - "Mondays, Wednesdays" uses the range-joining convention (comma-separated, "and" before the last item for two days; "Mondays through Fridays" for consecutive spans of 3+ days).
- Timezone abbreviation is extracted from
timezone(e.g.,America/Chicago→CT,America/New_York→ET). A small lookup table handles the common ones; fall back to the raw timezone string for unknown values. - If
timezoneis null/missing, fall back to the current flat display — don't silently drop information. - Today's meetings could optionally get a subtle "Today" badge or highlight (extra polish, not required for the initial version).
Implementation notes:
- Add a
$derivedinae_idaa_comp__event_obj_li.sveltethat computes the schedule string from the event object'sweekday_*fields,recurring_start_time, andtimezone. - Helper function in
ae_utilfor the weekday list → natural language string (e.g.,['Monday', 'Wednesday', 'Friday']→"Mondays, Wednesdays, and Fridays"). - Helper function or small lookup for timezone → abbreviation.
- Fall back to the current flat display when
timezoneis missing to avoid losing information.
6. Show result count during search, not just after
Current behavior: The result count badge ("Results: 25") only appears inside the
list wrapper component (ae_idaa_comp__event_obj_li.svelte line 98-108) when the
visible result list is non-empty. During loading, the user sees only a spinner with
no indication of how many meetings exist or what the search is operating on.
Proposed change: Show a result count line at the page level (in +page.svelte)
that is always visible once the first search completes:
25 of 140 meetings ← after search completes, with result count + total
Searching 140 meetings... ← during initial load (cold cache, no prior result)
0 results for these filters ← empty but filters are active (ties into item #1)
Implementation notes:
- Lift the count display from the list component to
+page.svelte, placed between the query bar (Comp__event_obj_qry) and the list wrapper. - The total count is available from the IDB fast path: after the initial unfiltered
search populates
db_events.event, the total isdb_events.event.count()(or the count of records matchingaccount_id). - The visible count is
event_id_li.lengthafter search completes. - Store the last known total in a
$statevariable so it persists across searches (the total changes infrequently). Refresh the total on the first search after page load. - Format:
{visible} of {total} meetingswhen filters/search are active;{visible} meetingswhen browsing all (no active filters). - During loading with no prior results: show "Loading meetings..." (from item #3) rather than a count.
7. "Live Now" and "Starting Soon" indicators
Current behavior: Meetings are shown in a static list. To find one happening now, a member must scan the "When" line of multiple cards and compare the time to their own clock.
Proposed change: Add a high-visibility badge or pulse indicator for meetings that are currently in progress or starting in the next 15 minutes.
- "LIVE NOW" (Green pulse badge) → if
current_timeis within[start, start + 1 hour]. - "STARTING SOON" (Yellow badge) → if
current_timeis within[start - 15 min, start]. - On the card, move the "Join Zoom" or "Join Jitsi" button to the very top or make it significantly larger when the meeting is live.
Implementation notes:
- Add a
$derivedstateis_liveandis_starting_soonto the card component. - Requires calculating "current time in meeting's timezone" using
Temporalor a date helper. - Ensure the pulse animation is subtle and respects
prefers-reduced-motion.
8. Local Timezone Conversion
Current behavior: Meetings show their native timezone (e.g., "7:00 PM America/Chicago"). The "Your TZ" line is currently a placeholder and doesn't perform conversion.
Proposed change: Automatically detect the member's browser timezone and show the converted time if it differs from the meeting's native timezone.
🕐 7:00 PM CT (8:00 PM ET your time)
Implementation notes:
- Use
Intl.DateTimeFormat().resolvedOptions().timeZoneto get the user's TZ. - If
user_tz !== meeting_tz, perform the conversion. - If the conversion results in a different day (e.g., late night ET vs early morning Europe), prefix with "Tomorrow at..." or "Yesterday at...".
9. Favorites / "My Meetings" — ✅ Implemented 2026-05-18
Current behavior: Members scan the full list every time they want to find their regular weekly meeting.
Proposed change: Add a "Star" icon to every meeting card.
- Starring a meeting adds it to a
favoriteslist stored in$idaa_loc. - Favorited meetings are pinned to the top of the list by default, regardless of other sort orders.
- Add a "Favorites" filter toggle in the query bar to show only starred meetings.
Implementation notes:
- Store as an array of
event_idstrings in$idaa_loc.recovery_meetings.favorites. - Update the
visible_event_obj_liderived in+page.svelteto prioritize these IDs in the sort logic.
Implemented: Star toggle on the [event_id] detail page
(src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte).
"My Meetings" filter chip is first in the filter chip row on the list page.
Implementation differs from proposal: favorites stored server-side in a
data_store record (code: idaa_meetings_favorites) as a UUID-keyed JSON map
rather than in $idaa_loc — this means favorites persist across browsers and
devices without Novi write capability. Pinning favorites to the top of the list
was not implemented; the filter chip shows only favorites instead.
10. "Add to Calendar" (iCal / Google)
Current behavior: Members must manually create calendar events if they want reminders for recurring meetings.
Proposed change: Add an "Add to Calendar" dropdown button on the meeting detail page (and optionally the card).
- Generates a
.icsfile or a Google Calendar URL with the recurring rule (e.g., "Every Wednesday at 7pm"). - Includes the meeting name, description, and the Zoom/Jitsi link in the location field.
Implementation notes:
- Use a helper to generate RFC 5545
RRULEstrings from theweekday_*andrecurring_patternfields. - Include the
attend_urlin the calendar event description for one-tap join from phone lock screens.
11. Geographic Search for In-Person Meetings
Current behavior: The only location filter is a binary "Physical" checkbox. Members must use fulltext search (e.g., "Chicago") to find local meetings.
Proposed change: Add a "City/State" search input or a map view.
- When
Physicalis checked, show a "Near [City, State]" input. - Map view (optional): A toggle to switch from "List" to "Map" view, plotting
meetings on a map using their
location_address_jsoncoordinates.
Implementation notes:
- The
eventtable haslocation_address_jsonwhich often contains city/state. - Simple implementation: a city-picker dropdown populated from the distinct
location_address_json->>'$.city'values in the current result set.
12. Prominent "Join" button for virtual meetings
Current behavior: On each meeting card, the Zoom or Jitsi join link is rendered
as a small btn-sm inside the content area, visually equivalent to other label/value
rows. The "Meeting Details" button at the top of the card is rendered larger than the
join link — meaning the primary action for a member who wants to attend a meeting right
now is visually subordinate to a navigation link.
The copy-to-clipboard button for the join link is gated behind $ae_loc.manager_access,
so regular members have no easy way to share the link with a sponsee.
Proposed change: For virtual meetings, elevate the join button to a full-width prominent CTA inside the card header area, directly below the meeting name and badges:
┌──────────────────────────────────────────────────┐
│ 📅 Monday Night IDAA Discussion 🖥 Virtual │
│ │
│ [ 🎥 Join Zoom Meeting ] ← full-width │
│ [ 📋 Meeting Details ] │
└──────────────────────────────────────────────────┘
- The Join button uses
preset-filled(solid) styling; Meeting Details usespreset-outlined(hollow). This makes the action hierarchy visually clear. - On mobile especially, a full-width join button is much easier to tap than a small inline link buried inside label rows.
- Replace manager-only clipboard with a Web Share API button for all members on
virtual meetings:
navigator.share({ title, url })on mobile triggers the native OS share sheet. Fall back to clipboard copy on desktop (wherenavigator.shareis often unavailable). This lets members easily send a meeting link to a sponsee. - Passcode, if present, moves to the Meeting Details page — exposing it in the list view is unnecessary and clutters the card.
Implementation notes:
- In
ae_idaa_comp__event_obj_li.svelte, move the Zoom/Jitsi attend block from theevent__contentsection up into theae_optionsdiv (line ~200), rendered only whenidaa_event_obj?.virtualis true and an attend URL exists. - Keep the existing small label/link in
event__contentas a fallback for when the prominent button is not shown (non-virtual meetings may still have a URL). - Web Share:
{#if navigator?.share}guard; wrap in a try/catch (user cancels the share sheet throwsAbortError). - The Live Now / Starting Soon badges from item #7, when implemented, should also interact with this button — e.g., a pulsing green border when the meeting is live.
13. "Today's Meetings" section at the top of the list
Current behavior: The meeting list shows all results sorted by the selected sort order. To find a meeting happening today, a member must scan every card's "When" line and mentally compare it to the current day and time. There is no at-a-glance view of what's available right now or later today.
This is distinct from the "Live Now" badge in item #7 (which marks individual cards after they're already displayed in a long list). This is a dedicated section pinned above the main results.
Proposed change: Add a collapsible "Today" section at the very top of the results list that shows only meetings scheduled on the current day of the week, sorted by start time:
▼ Today — Sunday, May 17 3 meetings
┌─────────────────────────────────────────────────┐
│ Sunday Serenity Discussion 7:00 AM ET 🔴Live │
│ IDAA Sunday Big Book 2:00 PM CT │
│ Sunday Night IDAA 8:00 PM ET │
└─────────────────────────────────────────────────┘
All Meetings (140)
...
- The section is collapsed by default if it's empty (no meetings today).
- Meetings in the "Today" section also appear in the main list below — this is a quick-access shortcut, not a filter.
- Past meetings (start time has already passed today) are dimmed but still shown; a meeting may still be in progress.
Implementation notes:
- Compute current day-of-week in the browser:
new Date().getDay()→ 0=Sunday, 6=Saturday. Map to theweekday_*boolean fields on the event object (e.g., day 0 →weekday_sunday). - Filter
visible_event_obj_li(already computed in the list wrapper) for items where the matchingweekday_*field is truthy. Sort byrecurring_start_time. - Store collapse state in
$idaa_sess.recovery_meetings.today_section_expanded(session only; default true so it's visible on first load). - Renders correctly in the Novi iframe since it's just a filtered sub-list of existing data — no additional API calls needed.
- If item #7 (Live Now) is implemented, the "Today" section naturally becomes the host for the live/starting-soon badges, since that's where members will look first.
14. Data freshness indicator (low priority — deprioritized)
Note: Meeting records change infrequently — once established, a meeting's schedule, type, and contact info are typically stable for months or years. The occasional update is usually minor wording. Surfacing a freshness indicator for data this static would add visual noise with very little member benefit. The existing error state (item #4 in the bug fix, distinct "Unable to load meetings") and the escape-hatch cache-reset button already handle the reliability-concern case. This idea is recorded for completeness but is not recommended for implementation.
15. "Confirmed meetings only" default filter
Current behavior: All meetings are shown by default, including those with
status === 'unknown' (not yet confirmed by IDAA Central Office). The "Not Confirmed
by IDAA" warning badge on those cards is alarming-looking but does nothing to prevent
unverified meetings from dominating the list.
There is currently no filter to hide unconfirmed meetings. Members have no choice but to see them all.
Business rationale: IDAA staff want meeting chairs to submit their meeting info for verification. Defaulting to "confirmed only" creates a natural incentive: unconfirmed meetings disappear from the default member view, which encourages chairs to contact IDAA staff and get their meeting verified. It also gives members a cleaner, higher- confidence list by default — they're not seeing meetings that may be outdated or inactive.
Proposed change: Add a qry__confirmed filter field with three states:
| Value | Behavior |
|---|---|
'confirmed_only' (default) |
Hide meetings where status === 'unknown' |
'all' |
Show all meetings, confirmed and unconfirmed |
'unconfirmed_only' |
Show only unconfirmed (admin/staff use) |
The filter UI shows a simple toggle in the query bar:
[✓ Confirmed Only] ← default, shown as active chip or checkbox
When the member switches to "All", the unconfirmed meetings appear with their warning badge (see item #15 for making that badge useful on mobile).
For trusted/admin users, the default should remain 'all' so staff can see the full
picture without having to change a setting.
Implementation notes:
- Add
qry__confirmed: 'confirmed_only' | 'all' | 'unconfirmed_only'to$idaa_loc.recovery_meetingsdefaults, defaulting to'confirmed_only'. Trusted users default to'all'. - Apply the filter in both the IDB fast path (
db_events.event.filter()) and the API revalidation secondary filter inhandle_search_refresh. IDB: checkev.status !== 'unknown'whenqry__confirmed === 'confirmed_only'. API: same post-fetch client-side filter. - Pass
qry__confirmedtoevents_func.search__eventif the API supports astatusfilter param; otherwise handle it client-side only. - The
no_results_no_filtersderived (used for the escape-hatch button) should NOT treatqry__confirmed === 'confirmed_only'as an active filter — it's the default state, not a narrowing choice the member made. Only count it as a filter if the member explicitly switched it to'all'or'unconfirmed_only'. - Add a count badge to the toggle: "Confirmed Only (132 of 140)" so members can see how many unconfirmed meetings exist without having to switch the filter.
16. "Not Confirmed" status — inline explanation on mobile
Current behavior: Meetings with status === 'unknown' show a warning badge:
⚠ Not Confirmed by IDAA ⚠. The badge has a title attribute with a full explanation
(~2 sentences). Title tooltips are invisible on mobile — tapping the badge does nothing.
Members on phones see a alarming-looking warning with no explanation of what it means
or what they should do.
Proposed change: Make the badge tappable. On tap (or hover on desktop), show an inline explanation panel directly below the badge:
⚠ Not Confirmed by IDAA [?]
↓ (on tap)
This meeting has not been confirmed by IDAA Central Office.
Please reach out to the chair for current information.
If this meeting is active, email info@idaa.org to confirm it.
[✕ Close]
Implementation notes:
- Add a
$state show_unconfirmed_info = falseper card (scoped to the{#each}block). - Replace the
titleattribute with anonclicktoggle that setsshow_unconfirmed_info. - The explanation renders in a
{#if show_unconfirmed_info}block directly below the badge row — a simple div with a rounded border and the existing tooltip text. - The
mailto:info@idaa.orglink in the explanation is already in the tooltip text; making it a real clickable link here rather than plain text in a tooltip is a direct improvement for mobile members who want to report a confirmed meeting. - This pattern also applies to other
title-only tooltips on the page if they appear. - Note: with item #15 defaulting to "confirmed only", most members will never encounter this badge unless they switch to the "All" view. The inline explanation is still worth implementing for that audience, but the default filter reduces how often it's seen.
17. Cycling sort button — ✅ Implemented 2026-05-18
Problem: Three separate sort chip buttons (Last Updated / Name A→Z / Name Z→A) took too much horizontal space and caused layout bounce as the selected chip changed width.
Implemented: Single cycling button in ae_idaa_comp__event_obj_qry.svelte.
Clicking advances through sort_modes array (Last Updated → Name A→Z → Name Z→A → repeat)
using $derived index + cycle_sort() function. Button has min-w-36 to prevent bounce.
Icon changes per mode (fa-clock / fa-sort-alpha-down / fa-sort-alpha-up-alt). A small
fa-redo icon indicates it's a cycling control.
18. Collapsible "Meeting Info" data store panel — ✅ Implemented 2026-05-18
Problem: The Element_data_store panel (code: recovery_meetings_info) displays
between the filter bar and the meeting results list. Once a member has read it, it
consumes vertical space on every page load and pushes results below the fold, especially
in the Novi iframe on mobile.
Implemented: Toggle button wrapping the <Element_data_store> in
src/routes/idaa/(idaa)/recovery_meetings/+page.svelte. Button shows
"Meeting Info" with a chevron (up = expanded, down = collapsed). Collapse state
persisted in $idaa_loc.recovery_meetings.ds_info_collapsed (localStorage) so the
user's preference survives page reloads. New field added to idaa_local_data_struct
in ae_idaa_stores.ts — no version bump needed (existing users without the field
get undefined which is falsy = expanded, the correct default).
Notes
- The fulltext search (
qry__fulltext_str) searches against thedefault_qry_strfield, which is a server-side composite that already includes contact info, day-of-week text, meeting type, location, and other metadata. The placeholder text in the search input is accurate — it genuinely searches contacts and schedule information despite those fields being stored in separate columns. - All changes must render correctly inside the Novi iframe context (Bootstrap v3.4.1
CSS conflicts — see
CLIENT__IDAA_and_customized_mods.mdfor known issues). - Mobile testing should cover Android Chrome specifically — the original "no meetings found" bug disproportionately affected mobile users with intermittent connections.