Previously, IDAA iframe access relied on trusting URL params (uuid, email, full_name) passed from Novi — any 36-char string granted authenticated access with no actual verification. The (idaa)/+layout.svelte now performs an async Novi API call on every UUID load to verify the UUID exists, fetches name/email directly from Novi (cannot be spoofed via URL), and sets $idaa_loc.novi_verified on success. All-or-nothing: if novi_idaa_api_key is absent or the call fails, access denied. - ae_idaa_stores.ts: add novi_verified boolean field to idaa_loc - (idaa)/+layout.svelte: async UUID verification with spinner to prevent Access Denied flash; permission upgrade-only strategy preserved - video_conferences/+page.svelte: skip duplicate Novi member details call if layout already verified ($idaa_loc.novi_verified check) - iframe HTML files: remove browser-side Novi API fetch and email/full_name params; pass only uuid; add README/START/STOP/WARNING comments for client staff; fix iframe-before-script DOM ordering bug - documentation: CLIENT__IDAA_and_customized_mods.md updated with full verification flow, site_cfg_json fields, permission table, access gate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
22 KiB
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 (v1, legacy — do not touch)
│ │ ├── ae_idaa_comp__event_obj_id_edit_v2.svelte # Meeting edit form (v2, 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:
- Modal pattern (primary list flow) — list, view, and edit components live at
recovery_meetings/level, toggled via$idaa_sess.recovery_meetingssession flags (show__modal_view,show__modal_edit).- 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_v2.svelte. The edit form clears bothshow__modal_editandedit__event_objon 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
full_namevia 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:
- The UUID actually exists in Novi's system (prevents fake/crafted UUIDs)
- Gets verified name and email directly from Novi — these can't be forged via URL
- Sets
$idaa_loc.novi_uuid,$idaa_loc.novi_email,$idaa_loc.novi_full_name - Sets
$idaa_loc.novi_verified = trueon 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:
{
"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
}
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).
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
uuidin URL → layout Novi block does not run - Shared Passcode: No
uuidin 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 viewsarchive— boolean flag for immediate archivalenable_comments— controls whether replies are allowedpost_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
- 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.
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
<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 withcode = '-'. The{#each}key must use the array index (i) rather thansub.codeto avoid a Svelteeach_key_duplicateerror. 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:
{
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:
{
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)
{
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:
- Height sync — The root layout posts
messageevents to the parent frame for dynamic height adjustment (content length varies) - URL parameter auth — Novi passes member context via query string on load
- 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, h1–h6) 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 .btnclass gets Bootstrap button colors, potentially overridingpreset-*Skeleton classes<section>and heading elements may get unexpected margins/padding from Bootstrap's typography reset- Class names
.field-inputand.field-label(used in the v2 edit form's scoped<style>block) also exist inidaa.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.
// ✅ 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.tsonly - 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-02-26)
| Module | State | Notes |
|---|---|---|
| Archives | ⚠️ Smoke only | archive_content.test.ts — no auth gate test |
| Bulletin Board | ❌ None | Priority — most sensitive module |
| Recovery Meetings | ❌ None | — |
| Video Conferences | ❌ None | Jitsi complexity, lower priority |
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
Document Status: ✅ Current Last Verified: 2026-03-09 — updated for Novi UUID verification upgrade