# 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. ### Breakout Links and Iframe Persistence Members often need to open Jitsi meetings outside the Novi iframe (e.g., for full-screen features or on mobile). These are referred to as **Breakout Links**. - **The Problem:** SvelteKit client-side navigation within the iframe often drops "bootstrap" query parameters like `?key=...` (site access key) and `?uuid=...` (Novi identity token). - **The Requirement:** When a member breaks out of the iframe into a new browser tab, these keys **must** be present in the URL. Without them, the member will hit the site-domain gate or the IDAA auth gate and see "Access Denied." - **The Solution:** The Video Conferences page uses a derived `breakout_url` that proactively re-injects the missing `key` (from `$ae_loc.allow_access`) and `uuid` (from `$idaa_loc.novi_uuid`) before generating the external link. **Example Breakout URL:** `https://client.oneskyit.com/idaa/video_conferences?uuid=...&key=...&room=...` --- ## Architecture: Composite Module 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/` 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= ``` > **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:** ```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 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=&iframe=true` param. 2. When the `uuid` param is present the IDAA layout calls the Aether server-side proxy: ```js // 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. ``` 3. 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. 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. 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) - 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 `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: ```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, 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 A–Z, Meeting Name Z–A. **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. ### Sort Encoding — Events Use Legacy (Not `build_tmp_sort`) `ae_events__event.ts` builds `tmp_sort_1` with the **legacy encoding**: `priority ? 1 : 0` (priority=true → `'1'`). This is the **opposite** of `build_tmp_sort` (priority=true → `'0'`). | Module | Encoding | Correct comparator | | --- | --- | --- | | `ae_events__event.ts` (Recovery Meetings) | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` | | `ae_events__event_session.ts` | Legacy: `priority=true→'1'` | **Descending** `b.localeCompare(a)` | | `ae_events__event_presentation.ts` | `build_tmp_sort` (overrides legacy in `specific_processor`) | **Ascending** `a.localeCompare(b)` | | Journals, Posts, Archives | `build_tmp_sort` | **Ascending** `a.localeCompare(b)` | **Do not apply the `build_tmp_sort` ascending rule to raw event or session sorts** until `ae_events__event.ts` is migrated (tracked in TODO__Agents.md under IDB Sort rollout). ### Search Trigger — Use `$slct.account_id`, Not `$ae_loc.account_id` The recovery meetings search `$effect` gates on `$slct.account_id` (set only by the bootstrap Sync Effect, non-persisted). Do NOT change this back to `$ae_loc.account_id`. **Why:** `$ae_loc` is a persisted store that hydrates from localStorage on page load. Its `account_id` may be stale from a previous session (e.g., a dev/demo account_id left behind). Using it as the gate fires the API call with the wrong account before bootstrap has run, producing either a 403 or wrong-account data. `$slct.account_id` is null until bootstrap sets it — a reliable gate. See mistake #14 in `BOOTSTRAP__AI_Agent_Quickstart.md`. ### My Meetings (Favorites) 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_1–3, 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_*` (Sun–Sat 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 `