From 128944c7aba65ca6725b8cf052e313527a414148 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 19 May 2026 10:46:28 -0400 Subject: [PATCH] feat(idaa): improve error handling for Novi verification failures and network errors Distinguish transient API failures (rate limits, server errors, network drops) from real membership denials, so members see actionable messages instead of 'Access Denied.' Layout: new verify_error_type state ('rate_limited' | 'api_error') surfaces a yellow 'Identity Verification Unavailable' banner with three recovery options -- Try Again (no reload, clears latch), Clear Cache and Reload, and Full Reset. Spinner now shows live status messages (e.g. 'High traffic - retrying in 10 seconds...'). Recovery meetings page: qry_error_detail distinguishes network drops (TypeError / ERR_NETWORK_CHANGED) from server errors, showing specific guidance in the error UI. Co-Authored-By: Claude Sonnet 4.6 --- src/routes/idaa/(idaa)/+layout.svelte | 103 +++++++++++++++++- .../(idaa)/recovery_meetings/+page.svelte | 18 ++- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/routes/idaa/(idaa)/+layout.svelte b/src/routes/idaa/(idaa)/+layout.svelte index 78a7b161..c83f47ff 100644 --- a/src/routes/idaa/(idaa)/+layout.svelte +++ b/src/routes/idaa/(idaa)/+layout.svelte @@ -63,6 +63,14 @@ let verify_in_flight = false; // UUID must be verified fresh. A plain boolean would wrongly block verification of the new UUID. // Storing the failed UUID means only that exact UUID is skipped; any other UUID is a clean slate. let verify_failed_for_uuid: string | null = null; +// Tracks the type of Novi API failure for the UI — 'rate_limited' or 'api_error'. +// A transient API error is NOT the same as a real membership denial; this lets us +// show a distinct "Verification Unavailable" state instead of "Access Denied". +let verify_error_type: 'rate_limited' | 'api_error' | null = $state(null); +// Shown inside the spinner — updated during rate-limit retry waits. +let verifying_status_msg: string = $state('Verifying identity...'); +// Incremented by handle_verify_retry() to re-run Effect 2 without a full page reload. +let retry_count: number = $state(0); // Clear stale db_events.event IDB data on IDAA session start. // @@ -140,6 +148,7 @@ $effect(() => { // Read url_uuid here (outside untrack) to create a reactive dependency. // Effect 2 re-runs whenever the UUID in the URL changes. const current_uuid = url_uuid; + void retry_count; // Reactive dep — re-run Effect 2 when user clicks "Try Again". untrack(() => { if (!current_uuid) { @@ -268,6 +277,8 @@ async function verify_novi_uuid( verify_in_flight = false; return; } + verifying_status_msg = 'Verifying identity...'; + verify_error_type = null; console.log(`IDAA Layout: Starting Novi UUID verification for ${uuid}...`); try { @@ -280,9 +291,11 @@ async function verify_novi_uuid( if (response.status === 429) { if (is_retry) { + verify_error_type = 'rate_limited'; throw new Error(`Novi API rate limited for UUID ${uuid} (retry also failed)`); } console.warn(`IDAA Layout: Novi API rate limited (429) for ${uuid}. Retrying in 10s...`); + verifying_status_msg = 'High traffic — retrying in 10 seconds...'; await new Promise((resolve) => setTimeout(resolve, 10_000)); await verify_novi_uuid(uuid, api_key, api_root_url, true); return; @@ -293,6 +306,10 @@ async function verify_novi_uuid( } const result = await response.json(); + log_lvl = 2; + if (log_lvl > 1) { + console.log(`IDAA Layout: Novi API response for ${uuid}:`, result); + } // Build display name: prefer "First L." format, fall back to full Name field. const first_name = result?.FirstName ?? null; @@ -359,6 +376,8 @@ async function verify_novi_uuid( error ); verify_failed_for_uuid = uuid; // Latch — stop retry loop for this UUID. See declaration comment. + // 'rate_limited' is set before throw for 429; everything else is an unexpected API error. + if (!verify_error_type) verify_error_type = 'api_error'; $idaa_loc.novi_uuid = null; $idaa_loc.novi_email = null; $idaa_loc.novi_full_name = null; @@ -375,6 +394,17 @@ async function verify_novi_uuid( novi_verifying = false; } } +/** + * Clears the verification failure latch and forces Effect 2 to re-run without a full page reload. + * Called by the "Try Again" button in the verification-error UI state. + */ +function handle_verify_retry() { + verify_error_type = null; + verify_failed_for_uuid = null; + verifying_timed_out = false; + novi_verifying = true; + retry_count++; +} {#if !browser} @@ -389,7 +419,7 @@ async function verify_novi_uuid( class="container m-8 flex w-full flex-col items-center justify-center gap-2 p-8">

- Verifying identity... + {verifying_status_msg}

{#if verifying_timed_out} +
+

+ + + Identity Verification Unavailable + +

+ {#if verify_error_type === 'rate_limited'} +

+ The membership directory is temporarily busy (rate limited). Please wait a moment and try again. +

+ {:else} +

+ We were unable to connect to the membership directory. This is likely a temporary issue — it may not be a problem with your membership or access. +

+ {/if} +

+ If this keeps happening, try "Clear Cache & Reload" or contact technical support. +

+
+ + + + + + +
+
{:else if $ae_loc.trusted_access || ($ae_loc.authenticated_access && $idaa_loc.novi_uuid && $idaa_loc.novi_verified)}