diff --git a/documentation/CLIENT__IDAA_and_customized_mods.md b/documentation/CLIENT__IDAA_and_customized_mods.md index f3ebf510..e0e1a69e 100644 --- a/documentation/CLIENT__IDAA_and_customized_mods.md +++ b/documentation/CLIENT__IDAA_and_customized_mods.md @@ -219,6 +219,25 @@ If you need a compact checklist for re-creating this flow in another integration `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. + ### Permission Upgrade Rule ``` // RULE: Only UPGRADE to Novi-based permissions, NEVER downgrade. diff --git a/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte b/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte index 9f7abd04..21c2b505 100644 --- a/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte +++ b/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_comment_obj_id_edit.svelte @@ -43,9 +43,13 @@ let prom_api__post_comment_obj: any = $state(); let post_comment_form = $state({ content: $idaa_slct.post_comment_obj?.content ?? '', anonymous: $idaa_slct.post_comment_obj?.anonymous ?? false, - external_person_id: $idaa_slct.post_comment_obj?.external_person_id ?? '', - full_name: $idaa_slct.post_comment_obj?.full_name ?? '', - email: $idaa_slct.post_comment_obj?.email ?? '', + external_person_id: + $idaa_slct.post_comment_obj?.external_person_id || + $idaa_loc.novi_uuid || + '', + full_name: + $idaa_slct.post_comment_obj?.full_name || $idaa_loc.novi_full_name || '', + email: $idaa_slct.post_comment_obj?.email || $idaa_loc.novi_email || '', enable: $idaa_slct.post_comment_obj?.enable ?? true, hide: $idaa_slct.post_comment_obj?.hide ?? false, priority: $idaa_slct.post_comment_obj?.priority ?? false, @@ -94,10 +98,29 @@ async function handle_submit_form(event: any) { // SVELTE 5: Map values from local form state to payload post_comment_do['content'] = content_new_html; post_comment_do['anonymous'] = post_comment_form.anonymous; - post_comment_do['external_person_id'] = - post_comment_form.external_person_id; - post_comment_do['full_name'] = post_comment_form.full_name; - post_comment_do['email'] = post_comment_form.email; + + // Robust scavenging of identity from storage if form state is missing it + let novi_uuid = post_comment_form.external_person_id; + let novi_full_name = post_comment_form.full_name; + let novi_email = post_comment_form.email; + + if (!novi_uuid && typeof localStorage !== 'undefined') { + try { + const ls_val = localStorage.getItem('ae_idaa_loc'); + if (ls_val) { + const ls_json = JSON.parse(ls_val); + novi_uuid = ls_json.novi_uuid; + novi_full_name = ls_json.novi_full_name; + novi_email = ls_json.novi_email; + } + } catch (e) { + /* ignore */ + } + } + + post_comment_do['external_person_id'] = novi_uuid; + post_comment_do['full_name'] = novi_full_name; + post_comment_do['email'] = novi_email; post_comment_do['notify'] = post_comment_di.notify ?? true; // Default to true if not found post_comment_do['hide'] = post_comment_form.hide; diff --git a/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte b/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte index 323f528b..4717fc5b 100644 --- a/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte +++ b/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_obj_id_edit.svelte @@ -891,7 +891,7 @@ $effect(() => { name="external_person_id" value={$idaa_slct.post_obj?.external_person_id ? $idaa_slct.post_obj?.external_person_id - : ''} + : ($idaa_loc.novi_uuid ?? '')} readonly={!$ae_loc.administrator_access} class="input preset-tonal-surface hover:preset-filled-surface-100-900 form-control w-96" /> {/if} @@ -918,7 +918,7 @@ $effect(() => { name="full_name" value={$idaa_slct.post_obj?.full_name ? $idaa_slct?.post_obj.full_name - : $idaa_loc.novi_full_name} + : ($idaa_loc.novi_full_name ?? '')} readonly={!$ae_loc.trusted_access} class="input preset-tonal-surface hover:preset-filled-surface-100-900 form-control w-96" /> diff --git a/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte b/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte index 8f06dd8b..50036005 100644 --- a/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte +++ b/src/routes/idaa/(idaa)/bb/ae_idaa_comp__post_options.svelte @@ -183,11 +183,30 @@ let creating = $state(false); // true while create API call is in-flight creating = true; $idaa_slct.post_obj = {}; + // Robust scavenging of identity from storage if store is null + let novi_uuid = $idaa_loc.novi_uuid; + let novi_full_name = $idaa_loc.novi_full_name; + let novi_email = $idaa_loc.novi_email; + + if (!novi_uuid && typeof localStorage !== 'undefined') { + try { + const ls_val = localStorage.getItem('ae_idaa_loc'); + if (ls_val) { + const ls_json = JSON.parse(ls_val); + novi_uuid = ls_json.novi_uuid; + novi_full_name = ls_json.novi_full_name; + novi_email = ls_json.novi_email; + } + } catch (e) { + /* ignore */ + } + } + let data_kv = { - external_person_id: $idaa_loc.novi_uuid, + external_person_id: novi_uuid, title: '', - full_name: $idaa_loc.novi_full_name, - email: $idaa_loc.novi_email, + full_name: novi_full_name, + email: novi_email, enable: true }; if (log_lvl) { diff --git a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte index d2da26f1..29053432 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte @@ -446,8 +446,22 @@ function prevent_default(fn: (event: T) => void) { $idaa_slct.event_id = null; $idaa_slct.event_obj = {}; + let novi_uuid = $idaa_loc.novi_uuid; + if (!novi_uuid && typeof localStorage !== 'undefined') { + try { + const ls_val = localStorage.getItem('ae_idaa_loc'); + if (ls_val) { + const ls_json = JSON.parse(ls_val); + novi_uuid = ls_json.novi_uuid; + } + } catch (e) { + // Ignore storage errors + } + } + let data_kv = { - name: 'Change NEW Recovery Meeting Name' + name: 'Change NEW Recovery Meeting Name', + external_person_id: novi_uuid }; if (log_lvl) { console.log( diff --git a/tests/idaa_recovery_meeting_edit.test.ts b/tests/idaa_recovery_meeting_edit.test.ts index 3c4b21fe..d2869718 100644 --- a/tests/idaa_recovery_meeting_edit.test.ts +++ b/tests/idaa_recovery_meeting_edit.test.ts @@ -67,6 +67,7 @@ const mock_event = { */ function build_idaa_loc_defaults(opts: { edit_event_obj?: boolean } = {}) { return { + __version: 1, // MUST MATCH IDAA_LOC_VERSION in store_versions.ts ver: '2024-08-21_1646', ver_idb: '2024-08-21_1645', novi_uuid: TEST_NOVI_UUID, @@ -111,6 +112,7 @@ async function setup_idaa_auth(page: any) { trusted_access: true, administrator_access: false, access_type: 'trusted', + edit_mode: true, iframe: false, // Pre-seed the timezone list so the component renders . @@ -1033,3 +1035,48 @@ test.describe('IDAA Recovery Meetings — Field Save Payload Verification', () = }); }); + +// ============================================================================= +// Creation and Identity Linkage Tests +// Verifies that new meetings are correctly linked to the creating member's Novi ID. +// ============================================================================= + +test.describe('IDAA Recovery Meetings — Creation and Identity', () => { + + test.beforeEach(async ({ page }) => { + page.on('pageerror', (err: Error) => + console.error(`BROWSER ERROR: ${err.message}`) + ); + await setup_idaa_auth(page); + await setup_api_mocks(page, TEST_EVENT_ID); + }); + + test('creating a new meeting sends external_person_id in POST payload', async ({ page }) => { + // 1. Start at the meeting list page + await page.goto('/idaa/recovery_meetings'); + + // 2. Click "Create New Meeting" button + // The component uses window.confirm() — we must handle the dialog + page.on('dialog', dialog => dialog.accept()); + + // Track API calls to capture the POST request (Creation, NOT search) + const post_promise = page.waitForRequest( + (req: any) => + req.url().includes('/v3/crud/event') && + !req.url().includes('search') && + req.method() === 'POST', + { timeout: 5000 } + ); + + await page.getByRole('button', { name: /Create New Meeting/ }).click(); + + // 3. Capture and verify the POST body + const post_req = await post_promise; + const post_body = JSON.parse(post_req.postData() ?? '{}'); + + expect(post_body.external_person_id, 'external_person_id in POST body').toBe(TEST_NOVI_UUID); + expect(post_body.name, 'default meeting name').toContain('Recovery Meeting Name'); + }); + +}); +