Files
OSIT-AE-App-Svelte/documentation/CLIENT__IDAA_and_customized_mods.md
Scott Idem 372d79df2b docs(idaa): track contact_li_json_ext search gap + message sent to backend
- TODO__Agents.md: added task for contact search — backend to whitelist
  contact_li_json_ext in event search, frontend to add OR condition in
  search__event() and update local IDB fast-path filter. Blocked on backend.

- CLIENT__IDAA_and_customized_mods.md: documented the search architecture
  gap under Recovery Meetings — what default_qry_str contains, why
  contact_li_json is unsearchable as raw JSON, what contact_li_json_ext is
  and what needs to happen to enable contact name/email search.

Backend agent notified via ae_send_message 2026-04-08.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 00:20:59 -04:00

638 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-03-09 (Novi UUID verification upgrade)
---
## ⚠️ 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.
---
## 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/ # External Jitsi reporting
```
> **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 fetches verified identity directly from the Novi API.
> 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 Novi API call** to verify:
1. The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
2. Gets verified name and email directly from Novi — 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, or the verification call fails, access is denied. There is no URL-param fallback.
**Required `site_cfg_json` fields:**
```json
{
"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 layout handles 429 responses with a 10-second flat backoff and one retry. If the retry also returns 429, access is denied and a "Reload / Retry" button is shown. The 5-minute TTL cache on successful verification prevents repeated calls during normal use.
### 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 performs an authenticated GET against the Novi customers endpoint:
```js
// simplified
fetch(`${api_root_url}/customers/${uuid}`, {
method: 'GET',
headers: { 'Authorization': `Basic ${api_key}` }
})
```
3. On success the layout uses the returned JSON to build a display name and normalized email, then writes these values to the IDAA store and marks verification success.
4. The layout then determines a target Novi permission level (`authenticated`, `trusted`, `administrator`) by checking configured UUID lists (`novi_trusted_li`, `novi_admin_li`) and upgrades the Aether session only if the Novi-derived level is higher than the current global level.
5. The layout also resets a few IDAA-specific query defaults (BB filters, etc.) to safe values after verification.
### Key `site_cfg_json` fields and where they are used
- **`novi_idaa_api_key`**: Base64-encoded Basic auth token provided by Novi. Required for the verification request. Accessed in code as `$ae_loc.site_cfg_json.novi_idaa_api_key` and passed in the `Authorization: Basic <key>` header. If missing, Novi-based access is denied.
- **`novi_api_root_url`**: Optional API root (defaults to `https://www.idaa.org/api`). Used to form the verification URL.
- **`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.
- **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)
- The Novak UUID verification and permission-upgrade logic is implemented in the IDAA layout: [src/routes/idaa/(idaa)/+layout.svelte](src/routes/idaa/(idaa)/+layout.svelte).
- UI elements that permit actions for verified Novi users or trusted members check these values. Example: the "Create New Meeting" button allows creation when either the session has `trusted_access` or a `novi_uuid` is present — see [src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte](src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte).
### Security notes and operational guidance
- The previous implementation leaked `email` and `full_name` via URL params — this was removed because those values are unauthenticated and can be spoofed.
- The API key is sensitive — keep it only in site config and do not expose it in client-side code or public repositories.
- The verification request uses Basic auth with the provided `novi_idaa_api_key` (already Base64-encoded by Novi) — treat the token like a password.
- If Novi changes their customer API shape, update the layout parsing (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.
### 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
- 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:
```typescript
// This filter is REQUIRED — do not remove it
filter((x) => !x.archive_on || archiveDate > now)
```
Archived posts are soft-deleted — they remain in the database for audit purposes but are not shown to members.
Most recent first (sorted `updated_on DESC`).
### Database (Dexie)
```
db_posts.post — Posts (threads)
db_posts.comment — Post comments (linked by post_id)
```
---
## 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
- **Physical** — in-person meetings
- **Virtual** — online meetings (Zoom, Google Meet, etc.)
- **Meeting type** — specific meeting format categories
Search is debounced (250ms) and uses the standard Aether SWR pattern.
### Search Architecture — What Is and Isn't Searched
The fulltext search runs against the `default_qry_str` field (backend-computed, contains:
`id_random`, type, name, description, timezone, recurring pattern/text, location text).
**Contact names and emails are NOT currently searchable.** The `contact_li_json` field is a
JSON longtext — MariaDB cannot efficiently substring-search it directly. The backend already
has a `contact_li_json_ext` (STORED GENERATED, indexed) column to work around this, but it
has not yet been added to the searchable fields whitelist in the API.
**Pending fix (tracked in TODO__Agents.md, 2026-04-08):**
- Backend: add `contact_li_json_ext` to the event object searchable fields whitelist
- Frontend: add `contact_li_json_ext` as an OR condition in `search__event()`, and update
the local IDB fast-path filter to parse `contact_li_json` for instant cache results
### 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.
---
## 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:
```typescript
{
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 }
recovery_meetings: { qry__fulltext_str, qry__physical, qry__virtual, qry__type, qry__limit, edit__event_obj }
}
```
### `idaa_sess` (sessionStorage — cleared on tab close)
UI state per submodule:
```typescript
{
archives: { qry__status, show__modal_edit__archive_id, show__modal_view__archive_id, obj_changed }
bb: { qry__status, show__modal_edit__post_id, show__modal_view__post_id, obj_changed }
recovery_meetings: { qry__status, show__modal_edit, show__modal_view, attend_platform, obj_changed }
}
```
### `idaa_slct` (sessionStorage — selection tracking)
```typescript
{
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:
```text
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:
```text
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
**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.**
```typescript
// ✅ 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:
```typescript
// 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 |
**Pending:** BB Post and Post Comment creation linkage tests (pattern established in Recovery Meetings test).
---
## External Links (idaa.org)
- Archives: `https://www.idaa.org/idaa-archives`
- Bulletin Board: `https://www.idaa.org/idaa-bulletin-board`
- Meetings: `https://www.idaa.org/idaa-meetings`
---
## Related Documentation
- [AE API V3 for Frontend](./GUIDE__AE_API_V3_for_Frontend.md)
- [Development Guide](./GUIDE__Development.md)
- [Naming Conventions](./AE__Naming_Conventions.md)
- [Playwright Test README](../tests/README.md)
---
## 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-04-07 — updated for Novi UUID triple-linkage enforcement, staff editing rules, Contact 1 convention, test coverage