Files
OSIT-AE-App-Svelte/documentation/CLIENT__IDAA_and_customized_mods.md
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

46 KiB
Raw Blame History

CLIENT: IDAA — International Doctors in Alcoholics Anonymous

Client: International Doctors in Alcoholics Anonymous (IDAA) Module Path: src/routes/idaa/ State Stores: src/lib/stores/ae_idaa_stores.ts Last Updated: 2026-05-18 (Default limit and stepper update)


⚠️ CRITICAL PRIVACY REQUIREMENT

ALL IDAA content is PRIVATE. Authentication is required for ALL modules.

IDAA serves a sensitive population — physicians in addiction recovery. Content exposure to the public is a severe security failure and a violation of member trust.

  • A previous AI agent accidentally exposed IDAA Bulletin Board content publicly. This must never happen again.
  • Every route, component, and API call in this module must enforce authentication.
  • When in doubt: it's private.

Required access level: trusted_access or higher for all IDAA content.


What IDAA Is

IDAA is a private membership organization for physicians in recovery. They use the Aether platform for:

  • A private document archive (historical materials, meeting records)
  • A members-only bulletin board (community posts and discussion)
  • A searchable directory of in-person and virtual recovery meetings
  • Video conferencing (Jitsi-based)

IDAA's Aether instance is embedded as an iframe inside their existing Novi-powered website (idaa.org). Novi is their external Association Management System (AMS) — it handles membership records and authentication. Aether receives the member context via URL parameters on iframe load.

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

IDAA is not a standalone module — it is a composition of three existing Aether modules, access-gated and branded for the IDAA client.

IDAA Feature Aether Module Used Library
Archives Archives module src/lib/ae_archives/
Bulletin Board (BB) Posts module src/lib/ae_posts/
Recovery Meetings Events module (repurposed) src/lib/ae_events/
Video Conferences Jitsi (external embed) External

There is no src/lib/ae_idaa/ library directory. IDAA-specific state and logic lives in ae_idaa_stores.ts and the route components only.

This design allows the IDAA module to be removed or updated without touching core modules.


Route Structure

src/routes/idaa/
├── +layout.svelte              # Root layout: Novi UUID extraction, iframe height sync
├── (idaa)/
│   ├── +layout.svelte          # Access gate: blocks render if unauthorized; permission upgrade
│   ├── +page.svelte            # IDAA dashboard — 3-module selector
│   ├── archives/               # Archives submodule
│   │   ├── +page.svelte        # Archive list (LiveQuery)
│   │   └── [archive_id]/
│   │       ├── +page.svelte    # Archive detail + content viewer
│   │       ├── ae_idaa_comp__archive_obj_id_view.svelte
│   │       ├── ae_idaa_comp__archive_obj_id_edit.svelte
│   │       ├── ae_idaa_comp__archive_content_obj_id_edit.svelte
│   │       └── ae_idaa_comp__modal_media_player.svelte
│   ├── bb/                     # Bulletin Board (Posts) submodule
│   │   ├── +page.svelte        # Post list (LiveQuery, archive-filtered)
│   │   └── [post_id]/
│   │       ├── +page.svelte    # Post detail + comments
│   │       ├── ae_idaa_comp__post_obj_id_view.svelte
│   │       ├── ae_idaa_comp__post_obj_id_edit.svelte
│   │       └── ae_idaa_comp__post_comment_obj_id_edit.svelte
│   ├── recovery_meetings/      # Recovery Meetings (Events repurposed)
│   │   ├── +layout.ts          # Layout loader (auth, stores)
│   │   ├── +layout.svelte      # Layout wrapper
│   │   ├── +page.svelte        # Meeting list + search filters
│   │   ├── ae_idaa_comp__event_obj_li_wrapper.svelte   # List container/modal host
│   │   ├── ae_idaa_comp__event_obj_li.svelte           # Individual list item card
│   │   ├── ae_idaa_comp__event_obj_qry.svelte          # Query/filter bar
│   │   ├── ae_idaa_comp__event_obj_id_view.svelte      # Meeting detail (read-only)
│   │   ├── ae_idaa_comp__event_obj_id_edit.svelte   # Meeting edit form (active)
│   │   └── [event_id]/
│   │       ├── +page.svelte    # Meeting detail page — renders view OR edit based on session flag
│   │       └── +page.ts
│   ├── video_conferences/      # Jitsi video conference integration
│   └── jitsi_reports/          # Jitsi meeting activity log report (trusted_access only)

Note: Recovery Meetings has two UI entry points:

  1. Modal pattern (primary list flow) — list, view, and edit components live at recovery_meetings/ level, toggled via $idaa_sess.recovery_meetings session flags (show__modal_view, show__modal_edit).
  2. Direct page ([event_id]/+page.svelte) — navigating to /idaa/recovery_meetings/<id> renders the same view/edit components gated by $idaa_sess.recovery_meetings.edit__event_obj.

Both patterns use ae_idaa_comp__event_obj_id_edit.svelte. The edit form clears both show__modal_edit and edit__event_obj on save/cancel so it works correctly from either entry point.


Authentication: Novi UUID System

IDAA members do not log in through Aether — they log in through Novi (idaa.org), and Novi passes their identity to the Aether iframe via URL parameters.

URL Parameters (on iframe load)

?uuid=<36-char-uuid>
&iframe=true
&key=<site-access-key>

Security note (2026-03-09): The iframe HTML files previously also passed email and full_name via URL params. These were unverifiable claims that could be spoofed via URL. They have been removed. The SvelteKit layout now verifies identity via the Aether server-side Novi proxy — the Novi API call originates from the server, not the member's browser. See "Iframe Integration" → "Novi UUID Verification Flow" below.

Verification Flow ((idaa)/+layout.svelte)

When a uuid param is present in the URL, the layout performs an async call to the Aether server-side endpoint (GET /v3/action/idaa/novi_member/{uuid}), which proxies to Novi server-to-server:

  1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
  2. Gets verified name and email — these can't be forged via URL
  3. Sets $idaa_loc.novi_uuid, $idaa_loc.novi_email, $idaa_loc.novi_full_name
  4. Sets $idaa_loc.novi_verified = true on success

A novi_verifying UI state prevents the "Access Denied" screen from flashing during the API round-trip.

All or nothing: If the Novi API key is not configured on the site, or the verification call fails, access is denied. There is no URL-param fallback.

Required site_cfg_json fields:

{
  "novi_idaa_api_key": "Base64-encoded-key-from-Novi",
  "novi_api_root_url": "https://www.idaa.org/api",  // optional, this is the default
  "novi_admin_li": ["uuid-1", "uuid-2"],
  "novi_trusted_li": ["uuid-3", "uuid-4"],
  "novi_idaa_group_guid_li": ["group-uuid"]          // Jitsi moderators only
}

Novi API Integration — How We Use It

This section documents the exact way Aether uses the Novi API for the IDAA integration so future maintainers can recreate the flow.

  • Purpose: Verify a Novi-provided uuid received via iframe URL parameters, obtain a verified name/email from Novi, and upgrade Aether permissions for that session when appropriate.

  • All-or-nothing policy: If the Novi API key is not configured or the verification call fails, the Novi-based access path is denied. The layout explicitly prevents child routes from rendering while verification is in-flight to avoid flashing "Access Denied".

  • Rate limits (Novi API): 20 calls/second · 600 calls/minute · 100,000 calls/day. The Aether backend handles 429 responses; the frontend receives a 429 and retries once after 10 seconds. The 12-hour TTL cache on successful verification (Redis server-side + $idaa_loc client-side) prevents repeated calls during normal use. A 503 (Novi unreachable) is auto-retried once after 3 seconds before surfacing an error to the user.

Verification Flow (implementation)

  1. The IDAA iframe loads Aether pages with a ?uuid=<uuid>&iframe=true param.
  2. When the uuid param is present the IDAA layout calls the Aether server-side proxy:
// simplified
fetch(`${aether_api_url}/v3/action/idaa/novi_member/${uuid}`, {
  method: 'GET',
  headers: {
    'x-aether-api-key': api_key,
    'x-account-id': account_id
  }
})
// Aether calls Novi server-to-server; member's browser IP is never in the Novi call path.
  1. On success (200), the layout reads data.full_name and data.email from the response and writes them to the IDAA store, marking verification success.

  2. The layout then determines a target Novi permission level (authenticated, trusted, administrator) by checking configured UUID lists (novi_trusted_li, novi_admin_li) and upgrades the Aether session only if the Novi-derived level is higher than the current global level.

  3. The layout also resets a few IDAA-specific query defaults (BB filters, etc.) to safe values after verification.

Key site_cfg_json fields and where they are used

  • novi_idaa_api_key: Base64-encoded Basic auth token provided by Novi. Used by the Aether server to authenticate against Novi — the frontend never touches the key itself. The frontend checks only for its presence in site_cfg_json as a guard meaning "IDAA is configured for this site". If missing, Novi-based access is denied.

  • novi_api_root_url: Optional Novi API root (defaults to https://www.idaa.org/api). Read by the Aether server, not the frontend.

  • novi_admin_li: Array of UUIDs treated as administrators for IDAA. Merged into $idaa_loc.novi_admin_li during layout initialization and used to set administrator level.

  • novi_trusted_li: Array of UUIDs treated as trusted members. Merged into $idaa_loc.novi_trusted_li and used to set trusted level.

  • novi_jitsi_mod_li / novi_idaa_group_guid_li: Lists used to map Jitsi moderator privileges and group GUIDs (where applicable).

  • novi_bb_base_url: (optional) Base URL used to build links for Bulletin Board notification emails.

  • jitsi_exclude_uuids: (optional) Array of Novi UUIDs to exclude from Jitsi Reports. This is the canonical staff/test filter. UUIDs are matched case-insensitively against final_participants[].novi_uuid when present. Example: ["uuid-1", "uuid-2"].

  • jitsi_known_meetings: (optional) Array of meeting names / room names to keep in the report. When this list is non-empty, only matching room_name values are shown. Matching is case-insensitive.

  • Legacy fallback: jitsi_exclude_names is still honored for older configs, but it should be migrated to UUIDs.

  • Email config values (noreply_email, noreply_name, admin_email, admin_name): used by functions that send notification emails (BB posts, comments, recovery meetings).

Stores / runtime fields set by verification

  • $idaa_loc.novi_uuid — the verified UUID
  • $idaa_loc.novi_email — verified email (normalized)
  • $idaa_loc.novi_full_name — display name built from Novi fields
  • $idaa_loc.novi_verified — boolean flag indicating successful verification
  • $idaa_loc.novi_admin_li, $idaa_loc.novi_trusted_li — merged lists from site config

These fields are read elsewhere in the IDAA UI to enable flows for verified users (for example: creating meetings, posting comments, or auto-populating contact info in notifications).

Where in the codebase this runs (examples)

Security notes and operational guidance

  • The previous implementation leaked email and full_name via URL params — this was removed because those values are unauthenticated and can be spoofed.
  • The API key is sensitive — keep it only in site cfg_json and do not expose it in client-side code or public repositories. The key is read and used exclusively by the Aether backend; it is never sent to the browser.
  • If Novi changes their customer API shape, update app/methods/idaa_novi_verify_methods.py in the backend (display name/email normalization) and this documentation.

If you need a compact checklist for re-creating this flow in another integration, ask and I will add a small runbook with exact request/response field mappings.

Planned: Server-Side Novi Verification Implemented (2026-05-19)

Problem solved: The previous client-side Novi API call originated from the member's browser. Hotel/conference WiFi, VPNs, corporate/hospital networks, and Cloudflare IP reputation filtering could block these calls and produce false "Access Denied" for legitimate members.

Solution implemented: A FastAPI endpoint proxies the Novi call server-to-server (Aether → Novi), with Redis caching. Members' browser IPs are no longer in the call path.

Endpoint: GET /v3/action/idaa/novi_member/{uuid}

  • Standard Aether auth headers (x-aether-api-key, x-account-id)
  • Server reads novi_idaa_api_key / novi_api_root_url from site cfg_json
  • Redis cache: idaa:novi_member:{uuid} — 4-hour TTL, only 200s cached
  • 404 results never cached (recently-joined members not incorrectly denied)

Frontend: verify_novi_uuid() in (idaa)/+layout.svelte now calls this endpoint with standard Aether headers. The novi_idaa_api_key is still checked for presence in site_cfg_json as a proxy for "is IDAA configured for this site" (server holds the key itself).

Full API spec: GUIDE__AE_API_V3_for_Frontend.md §12.

Permission Levels (Ascending)

Level Condition Access
Anonymous No UUID, unrecognized UUID, or verification failure No access
Authenticated UUID verified against Novi API View own content, limited actions
Trusted Verified UUID in novi_trusted_li Full member access to all IDAA content
Administrator Verified UUID in novi_admin_li Full access + edit/manage

novi_trusted_li and novi_admin_li are managed in Aether site config (not in Novi directly).

Identity Linkage: The Novi UUID Rule (Triple Linkage)

CRITICAL ARCHITECTURAL STANDARD: All member-generated content in the IDAA module MUST be explicitly linked to the member's Novi UUID via the external_person_id field. This linkage is the primary mechanism for ownership, edit permissions, and auditing.

1. Mandatory at Creation

Linkage MUST happen at the moment of initial object creation (POST). Shell records created without an external_person_id are considered orphaned and may be inaccessible to the creator.

2. Triple Linkage Scope

The following objects require mandatory external_person_id linkage:

  • Recovery Meetings (ae_Event)
  • Bulletin Board Posts (ae_Post)
  • Post Comments (ae_PostComment)

3. Implementation Patterns

  • Buttons: Creation buttons (e.g., "Create New Meeting") must include external_person_id: $idaa_loc.novi_uuid in their initial create_ae_obj payload.
  • Edit Forms: Edit components must provide robust fallbacks to $idaa_loc.novi_uuid for new or incomplete records, ensuring identity is captured even if the initial creation call was narrow.
  • Identity Sync: Along with the UUID, full_name and email should also be synced from $idaa_loc to provide human-readable context in notifications and admin views.
  • Race Condition Defense: $idaa_loc may be briefly null on mount before the store hydrates from localStorage. Creation buttons and edit submit handlers must scavenge identity directly from localStorage.getItem('ae_idaa_loc') as a fallback when the store value is missing.

4. Staff Editing Rules (IDAA Trusted/Admin Staff)

IDAA staff have their own Novi UUID. When they edit member content, their identity must not overwrite the member's external_person_id, full_name, or email.

Content Type external_person_id for staff full_name / email for staff
BB Post Readonly (unless administrator_access) — member's UUID preserved Same — rendered from existing record, not staff identity
Post Comment Preserved — form state initializes from existing record first Same
Recovery Meeting Intentionally editable for trusted staff — staff can reassign meeting ownership Contact 1 renders from existing contact_li_json[0] first; staff identity only fills if blank

The fallback to $idaa_loc.novi_uuid (the current user's UUID) only fires when the record has no existing external_person_id. For any record properly created after the 2026-04-07 triple-linkage enforcement, this fallback should never be reached.

5. Recovery Meetings — Contact 1 Convention

In 99% of cases, Contact 1 should be the same person linked via external_person_id — the IDAA member who owns and runs the meeting. These are two separate fields:

  • external_person_id — the ownership/identity link (Novi UUID). Determines who may edit the meeting.
  • contact_li_json[0] — the displayed contact info (name, email, phone). Shown to members searching for meetings.

They are expected to match but are set independently. Members unlock Contact 1 via confirm dialog if they need to list a different contact. Staff can edit both fields directly.

Permission Upgrade Rule

// RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade.
// If a user has a higher global Aether role (site manager, super),
// their global role is preserved and not overwritten by Novi auth.

This ensures that OSIT staff with super or manager roles retain full access regardless of Novi UUID status.

Non-Novi Sign-in Paths (unaffected)

  • User/Pass or Auth Link: No uuid in URL → layout Novi block does not run
  • Shared Passcode: No uuid in URL → layout Novi block does not run

Access Gate ((idaa)/+layout.svelte)

The inner layout blocks ALL rendering if the user is not authorized:

  • novi_verifying = true → "Verifying identity..." spinner (message updates during retry)
  • verify_error_type === 'rate_limited' → yellow "Identity Verification Unavailable" panel with:
    • Try Again — calls handle_verify_retry() (respects retry_count, waits 10 s before re-calling Novi)
    • Clear Cache & Reload — clears IDB + localStorage + sessionStorage, then reloads
    • Full Reset — same clear but also navigates to / with invalidateAll
  • verify_error_type === 'api_error' → same yellow panel (API returned non-2xx, not a rate limit)
  • Verification failed or no UUID → "Access Denied" error page
  • Access check runs before any child routes render

Module 1: Archives

Route: /idaa/archives/ Library: src/lib/ae_archives/ Types: ae_Archive, ae_ArchiveContent

The Archives module stores IDAA historical content — meeting records, conference proceedings, historical documents, and media.

Object Types

Archive (Container)

  • Represents a collection (e.g., "2019 Conference Proceedings")
  • Key fields: name, description, original_datetime, original_location, archive_on
  • archive_on — date when this archive collection is auto-hidden (scheduled visibility control)

ArchiveContent (Items)

  • Individual items within an archive
  • Supports multiple content types: 'text', 'file', 'url', 'video'
  • Key fields: archive_content_type, content_html, url, hosted_file_id, duration
  • Video/audio content has a dedicated media player component

Database (Dexie)

db_archives.archive    — Archive containers
db_archives.content    — Archive content items (linked by archive_id)

Demo / Test IDs

  • Archive: nAA2bHLv8RK (id: 1) "One Sky Test Archive"
  • Archive Content: UjKzrk-GKu5 (id: 1) "Hosted File Test"

Module 2: Bulletin Board (BB)

Route: /idaa/bb/ Library: src/lib/ae_posts/ Types: ae_Post, ae_PostComment

The BB is the IDAA members-only community discussion board. It is the most sensitive module — public exposure must never occur.

Object Types

Post (Thread)

  • Key fields: title, content, anonymous, full_name, email
  • archive_on — date after which the post is hidden from all views
  • archive — boolean flag for immediate archival
  • enable_comments — controls whether replies are allowed
  • post_comment_count — cached count of replies

PostComment (Reply)

  • Key fields: post_id, content, anonymous, full_name, email
  • Replies inherit the parent post's visibility rules

Post Visibility / Archival Filter

Posts with archive_on set to a past date are automatically hidden from all queries. This is enforced at the component level via a LiveQuery filter:

// This filter is REQUIRED — do not remove it
filter((x) => !x.archive_on || archiveDate > now)

Archived posts are soft-deleted — they remain in the database for audit purposes but are not shown to members.

Most recent first (sorted updated_on DESC).

Database (Dexie)

db_posts.post      — Posts (threads)
db_posts.comment   — Post comments (linked by post_id)

Module 3: Recovery Meetings

Route: /idaa/recovery_meetings/ Library: src/lib/ae_events/ (standard Events module, repurposed) Types: ae_Event (standard event type, filtered for meeting context)

Recovery Meetings reuses the Aether Events object to represent AA recovery meetings. These are NOT conferences — they are regular ongoing meetings (weekly, monthly, etc.) available to IDAA members.

Search Filters

Members can filter meetings by:

  • Fulltext search — name, location, day of week, contacts (debounced 250ms; uses SWR pattern)
  • Virtual — online meetings (Zoom, Jitsi, other)
  • In-person — physical location meetings
  • Meeting type — IDAA / Caduceus / Family Recovery
  • My Meetings — star toggle; shows only meetings the member has starred (favorites)

Sort options: Last Updated (default), Meeting Name AZ, Meeting Name ZA.

Empty state behavior:

  • Zero results with active filters → "No meetings found for these filters" + "Clear all filters" button
  • Zero results with no filters → bare message shown, then after 8s a "Refresh Meeting Cache" escape hatch appears (clears IDB and re-fetches from API — indicates a stale-cache problem, not a real empty set)

Search uses the standard Aether SWR pattern (IDB cache returned immediately, then API refreshes in background).

Search Architecture — What Is and Isn't Searched

The fulltext search runs against the default_qry_str field (backend-computed STORED GENERATED column, contains: id_random, type, name, description, timezone, recurring pattern/text, location text, contact name and email).

Contact names and emails ARE searchable via the API path. default_qry_str includes contact data, so the API lk_qry LIKE search on that field covers contacts automatically.

IDB fast-path gap: The local cache (Dexie) fast-path returns all cached meetings without text filtering — users see the unfiltered list immediately, then the API result (with contacts filtered) replaces it after the background refresh completes. The IDB path does not parse contact_li_json for instant local text matching.

Known history (2026-05-19): Contact search appeared broken due to two issues now resolved:

  1. The backend STORED GENERATED columns (default_qry_str, contact_li_json_ext) had stale values; forced a rebuild via fake updates on each event record.
  2. The recovery meetings page secondary filter was re-running text matching against response fields — silently dropping results that matched only via default_qry_str (e.g. by contact name, since that field may not appear in the response body). Fix: removed text re-filtering from the secondary filter (type / physical / virtual OR-logic only).

Remaining enhancement (tracked in TODO__Agents.md):

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

My Meetings (Favorites)

Members can star meetings to build a personal "My Meetings" list. The star toggle appears:

  • On each card in the meeting list (ae_idaa_comp__event_obj_li.svelte)
  • On the meeting detail page nav bar ([event_id]/+page.svelte)

Favorites are stored in the data_store table (code: idaa_meetings_favorites, scoped to the IDAA account). The record's json field holds { [novi_uuid]: [event_id, ...] } — one shared record per account containing all members' favorites. This means:

  • Favorites persist across browsers and devices (server-side)
  • Does not write to ae_event rows (avoiding the ON UPDATE current_timestamp() side effect)
  • Known last-write-wins race condition if two members toggle simultaneously — acceptable for ~1000 members
  • Pre-created DB records: ID 150 (gaTKSVPagFj, account_id=1, dev/demo), ID 151 (knJh8zhyKT0, account_id=13, live IDAA)

The star button uses inline styles (not .btn) to avoid Bootstrap v3 box-model overrides in the iframe.

Edit Form — Sections and Key Fields

The edit form (ae_idaa_comp__event_obj_id_edit.svelte) is organized into these sections. All fields map directly to the ae_Event object; none are IDAA-specific custom fields.

Section Key Fields
General Information name (required), description (TipTap rich text), type (IDAA / Caduceus / Family Recovery)
How to Attend physical (bool), virtual (bool) toggles; conditionally shows:
→ Physical location_address_json (name, line_13, city, state, postal, country), location_text (TipTap)
→ Virtual Platform toggle: Zoom (attend_url_code meeting ID, attend_url_passcode, attend_json.zoom.passcode_enc, attend_json.zoom.domain, attend_json.zoom.full_url), Jitsi (attend_json.jitsi.*), Other (attend_url, attend_url_passcode, attend_phone, attend_phone_passcode)
→ Both attend_text (TipTap — additional attendance instructions)
Schedule recurring_pattern (weekly/every other week/monthly/other), weekday_* (SunSat booleans), timezone, recurring_start_time, recurring_end_time, recurring_text (optional TipTap, auto-generated with *gen* prefix if blank)
Contacts external_person_id (Novi UUID link), contact_li_json[0] (Contact 1: name, email, phone_mobile, phone_home, phone_office — name/email locked to Novi user by default), contact_li_json[1] (Contact 2: same fields, optional)
Admin Options status, hide, priority, sort, group, enable, notes (TipTap) — trusted_access only

Rich text fields all use AE_Comp_Editor_TipTap with separate *_new_html state variables (not bound to $idaa_slct.event_obj directly) to track change state for the save-button logic.

Zoom URL auto-generation: Triggered by $idaa_trig = 'update_zoom_full_url'. An $effect reconstructs attend_json.zoom.full_url from domain + meeting_id + passcode_enc whenever the Meeting ID, Passcode, Encrypted Passcode, or Domain fields change.

Recurring text auto-generation: If recurring_text is blank or contains the *gen* prefix, the submit handler generates a human-readable string (e.g., *gen* weekly: Monday, Wednesday at 7:00 PM America/Chicago). Members can opt into a custom text via "Add More Details?" (admin/trusted only).

Contact 1 lock: Contact 1 name and email default to the logged-in Novi member's identity ($idaa_loc.novi_full_name, $idaa_loc.novi_email). They are readonly unless the user explicitly unlocks them via confirm dialog (or has administrator access).

Jitsi Integration

Some virtual meetings are hosted via Jitsi. Members with a Jitsi moderator UUID (novi_jitsi_mod_li) have elevated permissions in video sessions.

Edit Form — Implementation Notes (v2)

  • The v2 edit form uses a <style> block with @apply. Tailwind v4 requires @reference "../../../../app.css"; at the top of any component <style> block that uses @apply.
  • The country subdivision lookup list (lu_country_subdivision_list) contains duplicate entries — specifically Puerto Rico (PR) has two rows with code = '-'. The {#each} key must use the array index (i) rather than sub.code to avoid a Svelte each_key_duplicate error. The duplicate entries are a backend data quality issue that should be cleaned up in the DB.

Demo / Test IDs

No dedicated IDAA recovery meeting demo records — uses the standard Event demo record for dev:

  • Event: pjrcghqwert (id: 1) "Demo One Sky IT Conference"

Module 4: Video Conferences (Jitsi)

Route: /idaa/video_conferences/

Embeds Jitsi video conferences directly in the IDAA module. Separate from Recovery Meetings — this is for IDAA board meetings or special sessions, not regular AA meetings.

Moderation permissions are controlled by novi_jitsi_mod_li in the IDAA store.


Module 5: Jitsi Reports

Route: /idaa/jitsi_reports/ Access: trusted_access or novi_verified — same gate as the rest of (idaa)/ Data source: activity_log table — jitsi_meeting_event and jitsi_meeting_stats log types Library function: qry__jitsi_report() in src/lib/ae_reports/reports_functions.ts

An admin/staff reporting tool that aggregates raw Jitsi activity logs into human-readable meeting sessions. It is not a member-facing page — IDAA members do not see it.

Reminder: this page now filters staff by Novi UUID and can whitelist known meeting names from site config.

View Modes

Two display modes, toggled via a button in the page header:

Mode Description
Grouped by Room (default) One collapsible section per room_name. Each section contains a compact table: Date / Time / Duration / Attendees / Participant List. Mirrors the output of the offline Python script (create_jitsi_report.py).
Flat List Original card-per-session accordion layout. Better for drilling into event timelines and raw participant lists.

Both modes use the same filtered data set — switching views does not reset filters.

Dark Mode / Surface Safety

The page now uses explicit page and row surfaces so dark mode does not collapse into white-on-white text in either the regular app or the Novi iframe.

Filters

Filter Default Logic
Min. Participants 2 Minimum real_participant_count to display a session. Used as the only size filter.
Room Name edit mode only Case-insensitive substring match against room_name. Hidden unless AE global edit mode is on.
From / To last 60 days / today Date range applied to start_time. "To" date includes the full end of day.

A "Reset Filters" button appears whenever any filter is non-default.

In edit mode, two extra toggles appear:

  • Show excluded IDs — temporarily include the UUIDs listed in jitsi_exclude_uuids
  • Show all meetings — temporarily ignore jitsi_known_meetings

An "Active Exclusions" panel below the filter bar shows the currently applied Novi UUID exclusions and known meeting-name whitelist values. Each list is collapsible so the page stays compact.

Staff / Meeting Filtering

Problem: Staff/test accounts and one-off test rooms distort the reports.

Site config keys:

{
  "jitsi_exclude_uuids": ["uuid-1", "uuid-2"],
  "jitsi_known_meetings": ["IDAA-BIPOC-Meeting", "IDAA-Sunday-Meeting"]
}

How it works:

  1. The page reads $ae_loc.site_cfg_json?.jitsi_exclude_uuids and excludes matching participants by Novi UUID. The UUID comes from the Jitsi log url_params.uuid field. g_uuid is the meeting/group UUID and is not used here.
  2. If a participant record does not include a UUID in the activity log, it is left visible; UUIDs are used whenever available.
  3. real_participant_count = real_participants.length drives filters, exports, and the per-meeting attendee count.
  4. Room-level unique participant counts are computed from Novi UUIDs when present, with display-name fallback only for UUID-less records.
  5. If $ae_loc.site_cfg_json?.jitsi_known_meetings is non-empty, only meetings whose room_name matches one of the listed names are shown.
  6. The Room Name filter is only shown when global edit mode is enabled.

Temporary stopgap: the report also hides these staff display names through the same UUID-exclusion toggle until the long-term logging fix lands: Scott I., Brie P., Michelle V.

Note: matching is case-insensitive on the stored room_name / meeting name.

Summary Stats

Shown above the meeting list when data is loaded. Stats reflect the filtered + exclusion-applied view:

  • Meetings Shown — count of sessions passing all filters
  • Total Participants — sum of real_participant_count across all shown sessions
  • Avg Duration — mean session duration (HH:MM:SS)
  • Total Duration — sum of all session durations (HH:MM:SS)

In grouped view, each room header also shows its own subtotals (meeting count, unique participants by Novi UUID when available). Each meeting instance keeps the full participant list visible; the Copy names button is edit-mode only so staff can grab the list for follow-up reports without exposing extra controls to normal viewers.

Caching / Load Behavior

The page now reads cached activity_log rows from IndexedDB first, renders that result immediately, then refreshes from the API in the background. That keeps the report usable even when the network round-trip is slow.

Both the cache path and the API refresh now page through the matching activity-log set in created_on DESC order with a 1000-row page size before building the report. That avoids the old "first 500 rows" behavior that could hide newer sessions if the log table grew large.

The report page keeps the newest session first in both the flat list and the grouped-by-room view; grouped room rows are also sorted newest session first within each room.

Jitsi URL Builder

Collapsible panel, visible to trusted_access users only. Generates properly-formatted Jitsi meeting URLs for IDAA rooms. Component: ae_idaa_comp__jitsi_url_builder.svelte.

Trusted Access users now get a footer link on the Video Conferences page that jumps back to the Jitsi Reports page. It preserves the current iframe context so the staff workflow stays inside the Novi embed.

Future idea: make that link include a room= query param for the current meeting so Jitsi Reports can auto-filter to that meeting instance, and have Reset clear that param again.

Export

CSV and JSON export buttons in the page header export the currently filtered + exclusion-applied data set.

Room Name Fragmentation

The same logical meeting can appear as multiple rooms (e.g. IDAA-BIPOC-Meeting, IDAA-BIPOC-Meeting-2026, IDAA-BIPOC-Meeting-March-31) because the Jitsi URL builder appends a date suffix to generate unique per-session room names. In grouped view, these appear as separate groups. A future normalization pass (strip trailing date suffixes) could optionally merge them — not implemented yet.

Data Flow

activity_log table
  └── qry__jitsi_report()           # reports_functions.ts — fetches + aggregates by meeting_id
        └── MeetingReport[]         # { meeting_id, room_name, start_time, final_duration,
                                    #   final_participants, final_participant_count, events }
              └── jitsi_reports/+page.svelte
                    ├── apply exclusion list → real_participants / real_participant_count
                    ├── apply filters        → meetings_filtered
                    ├── derive grouped view  → Map<room_name, MeetingReport[]>
                    └── render flat or grouped

State Management (ae_idaa_stores.ts)

Four stores manage all IDAA state:

idaa_loc (localStorage — persistent across sessions)

Stores Novi auth context and per-submodule query settings:

{
  novi_uuid: string | null          // Member UUID (set on verification success)
  novi_email: string | null         // Verified email from Novi API
  novi_full_name: string | null     // Verified name from Novi API
  novi_verified: boolean            // true after successful Novi API verification
  novi_admin_li: string[]           // Admin UUID list (from site config)
  novi_trusted_li: string[]         // Trusted member UUID list
  novi_jitsi_mod_li: string[]       // Jitsi moderator UUIDs

  archives: { enabled, hidden, limit, offset, edit__archive_obj, edit__archive_content_obj }
  bb: { enabled, hidden, limit, offset, edit__post_obj, edit__post_comment_obj,
        qry__enabled, qry__hidden, qry__limit, qry__offset, qry__order_by, qry__order_by_li }
  recovery_meetings: {
    qry__enabled, qry__hidden, qry__limit, qry__offset,
    qry__fulltext_str, qry__physical, qry__virtual, qry__type,
    qry__order_by, qry__order_by_li,
    qry__favorites_only,  // true = show only starred meetings (My Meetings filter)
    edit__event_obj       // null or event_id string when edit form is open
  }
}

idaa_sess (in-memory only — resets on page load)

UI state per submodule:

{
  archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id,
              show__modal_edit__archive_content_id, show__modal_view__archive_content_id, obj_changed }
  bb: { qry__status, edit__post_obj, show__inline_edit__post_obj, show__modal_edit__post_id,
        show__modal_view__post_id, obj_changed }
  recovery_meetings: {
    qry__status,        // null | 'loading' | 'done' | 'error'
    qry__fulltext_str,  // session-only copy (separate from persisted loc copy)
    search_version,     // incremented to trigger a new search cycle
    edit__event_obj,    // null | event_id — controls edit form visibility
    show__modal_edit, show__modal_view,
    show__modal_edit__event_id, show__modal_view__event_id,
    attend_platform,    // 'Zoom' | 'Jitsi' | null — platform selected in virtual attend section
    obj_changed
  }
}

idaa_slct (sessionStorage — selection tracking)

{
  event_id: string | null
  archive_id: string | null
  archive_content_id: string | null
  post_id: string | null
  post_comment_id: string | null
}

idaa_trig / idaa_prom

Trigger flags and promise tracking for async operations (standard Aether pattern).


Iframe Integration

The IDAA module is embedded in idaa.org via iframe. This requires:

  1. Height sync — The root layout posts message events to the parent frame for dynamic height adjustment (content length varies)
  2. URL parameter auth — Novi passes member context via query string on load
  3. No standard navigation — Members navigate within the iframe; Aether's nav chrome is hidden or minimal in this context

Novi UUID Verification Flow

Iframe HTML files (in static/): Pass only uuid to the iframe src — no Novi API calls in the browser:

idaa_novi_iframe_archives.html
idaa_novi_iframe_bulletin_board.html
idaa_novi_iframe_recovery_meetings.html
idaa_novi_iframe_jitsi_meeting.html   ← reference pattern (unchanged)

SvelteKit layout ((idaa)/+layout.svelte): Calls GET /customers/{uuid} on the Novi API using the novi_idaa_api_key from site_cfg_json. Sets verified name/email in $idaa_loc and grants permissions. Shows a "Verifying identity..." spinner during the async call.

Jitsi page (video_conferences/+page.svelte): Checks $idaa_loc.novi_verified in fetch_novi_data(). If the layout already verified the user, it reuses $idaa_loc.novi_email / $idaa_loc.novi_full_name and skips the duplicate member details API call. The group moderator check (get_novi_group_moderators) always runs — it is Jitsi-specific.

⚠️ Iframe CSS Conflicts (Bootstrap v3)

When $ae_loc.iframe = true, the root layout (+layout.svelte) injects two external stylesheets from Novi's CDN:

https://assets-staging.noviams.com/novi-core-assets/css/fontawesome.css  — safe, icon-only
https://assets-staging.noviams.com/novi-core-assets/css/c/idaa/idaa.css  — Bootstrap v3.4.1 ⚠️

idaa.css is a full Bootstrap v3.4.1 bundle. It applies global styles to bare HTML elements (input, select, textarea, h1h6) and commonly named classes (.btn, .badge, .active, .text-*, .bg-*). These will compete with Tailwind v4 + Skeleton UI.

Known consequences:

  • Bare form elements (<input>, <select>) receive Bootstrap's height/padding resets on top of Tailwind
  • .btn class gets Bootstrap button colors, potentially overriding preset-* Skeleton classes
  • <section> and heading elements may get unexpected margins/padding from Bootstrap's typography reset
  • Class names .field-input and .field-label (used in the v2 edit form's scoped <style> block) also exist in idaa.css's date picker — Svelte's scoped attribute selector wins, but be aware
  • In iframe widths near Tailwind sm, avoid hiding critical button labels behind breakpoint classes and do not depend on color-only active states; Bootstrap's .active/button styling can make the selected state nearly invisible unless the control uses an obvious fill/ring change plus aria-pressed

Mitigation: The iframe CSS conflicts existed before v2 and are not new. The v2 form uses the same Skeleton/Tailwind component classes as the rest of the app. Avoid using bare <section>, <article>, or block-level HTML5 elements as style hooks; use <div> with explicit classes instead.


Testing Requirements

Auth Gate Tests Come First

For every IDAA submodule, the first test written must be an authentication enforcement test.

// ✅ Required test pattern for each IDAA module
test('Archives - unauthenticated user cannot access content', async ({ page }) => {
    // Inject localStorage WITHOUT trusted_access
    // Navigate to /idaa/archives/
    // Assert: access denied message shown, no archive content visible
});

test('Archives - trusted member can access content', async ({ page }) => {
    // Inject localStorage WITH trusted_access + novi_uuid
    // Navigate to /idaa/archives/
    // Assert: archive list renders
});

Privacy in Test Data

  • Never use real member data in test fixtures
  • Use canonical demo IDs from tests/_helpers/env.ts only
  • Test names should document the privacy rule being enforced, not just the behavior

Trusted Access State Injection

Tests that need authenticated IDAA access must set trusted_access: true and novi_uuid in the injected ae_loc localStorage:

// In addInitScript or env helper
ae_loc.trusted_access = true;
ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };

Current Test Coverage (as of 2026-04-07)

Module State Notes
Archives ⚠️ Smoke only archive_content.test.ts — no auth gate test
Bulletin Board None Priority — most sensitive module
Recovery Meetings Substantial tests/idaa_recovery_meeting_edit.test.ts — form render, field interactions, PATCH payload verification (all sections), real backend save, creation linkage (Novi UUID in POST body)
Video Conferences None Jitsi complexity, lower priority
Jitsi Reports None Admin-only tool; lower privacy risk than member modules

Pending: BB Post and Post Comment creation linkage tests (pattern established in Recovery Meetings test).


  • Archives: https://www.idaa.org/idaa-archives
  • Bulletin Board: https://www.idaa.org/idaa-bulletin-board
  • Meetings: https://www.idaa.org/idaa-meetings


IDAA Novi Groups and Moderators

"IDAA Association Admins Group" = "409e91dc-f5a3-486c-a964-71b7d19e6841"

  • Scott
  • Michelle
  • Brie

"IDAA Couples Meeting" = "e9e162f0-3d03-4241-9682-340135ec3fb8"

  • "Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
  • "Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
  • "Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
  • "Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
  • "Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
  • "Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"

"Student/Resident Meeting Moderators" "d76d2c00-962d-40f6-a2e8-ed9c85594d96"

  • "Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
  • "Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"

"IDAA BIPOC Meeting" "873d3ad0-2605-4ccf-824c-638c16b2b9cf"

  • "Paula Lynn Bailey-Walton" "68383ba2-0989-4860-9ea6-073f9698df67"
  • "Tasha Hudson" "03d5408c-3c13-4c3a-a93f-49871f9050b1"

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