Compare commits
14 Commits
bbab9e7c8c
...
71e79f032d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71e79f032d | ||
|
|
53fd5e7de4 | ||
|
|
14e84884cd | ||
|
|
e921ca973f | ||
|
|
2855e091f7 | ||
|
|
f37c64c68b | ||
|
|
615af58a11 | ||
|
|
76e21b08ff | ||
|
|
4ada5c4a8f | ||
|
|
8850db89c6 | ||
|
|
ccacdc3f4b | ||
|
|
128944c7ab | ||
|
|
c0386f27bc | ||
|
|
ee506832e7 |
@@ -286,7 +286,12 @@ This ensures that OSIT staff with `super` or `manager` roles retain full access
|
||||
|
||||
### Access Gate (`(idaa)/+layout.svelte`)
|
||||
The inner layout blocks ALL rendering if the user is not authorized:
|
||||
- `novi_verifying = true` → "Verifying identity..." spinner
|
||||
- `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
|
||||
|
||||
@@ -392,18 +397,29 @@ Search uses the standard Aether SWR pattern (IDB cache returned immediately, the
|
||||
|
||||
### 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).
|
||||
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 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.
|
||||
**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.
|
||||
|
||||
**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
|
||||
**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.
|
||||
|
||||
### My Meetings (Favorites)
|
||||
|
||||
@@ -821,4 +837,4 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Current
|
||||
**Last Verified:** 2026-05-18 — Recovery Meetings: updated default limit to 100 and added 75 to limit stepper; added My Meetings / favorites (data_store-backed, star button on list + detail page), guided empty state for filtered zero-results, corrected filter/sort descriptions; updated `idaa_loc` and `idaa_sess` store schemas to match actual fields
|
||||
**Last Verified:** 2026-05-19 — Access Gate: documented new `verify_error_type` error-handling states and retry/reset UI; Search Architecture: corrected contact-search status (now works via `default_qry_str` in API path — two root causes fixed 2026-05-18/19); noted IDB fast-path gap as remaining enhancement
|
||||
|
||||
@@ -77,6 +77,40 @@ guessing defaults.
|
||||
|
||||
## 🚧 Upcoming High Priority
|
||||
|
||||
### ~~[IDAA] Random "Access Denied" — Root Cause Review & Fixes~~ ✅ Resolved (2026-05-19)
|
||||
|
||||
All known root causes fixed across 10+ commits to `src/routes/idaa/(idaa)/+layout.svelte`.
|
||||
Deploying as of 2026-05-19. Monitor for further member reports.
|
||||
|
||||
#### All fixes applied
|
||||
- [x] Access Denied on iframe reload (sessionStorage URL preservation) — `2855e091f`
|
||||
- [x] TTL cache bypassed when `$ae_loc` auth flags reset — `2855e091f`
|
||||
- [x] "Verification Unavailable" screen distinct from "Access Denied" — `2855e091f`
|
||||
- [x] "Try Again" without page reload (`retry_count` pattern) — `2855e091f`
|
||||
- [x] Novi TTL extended to 45 minutes (from 5) — `2855e091f` + manual edit
|
||||
- [x] 12 s AbortController hard timeout on Novi fetch — `e921ca973`
|
||||
- [x] Network/AbortError gets 3 s grace + one retry — `e921ca973`
|
||||
- [x] Clear Cache & Reload added to Access Denied state (iframe mode) — `2855e091f`
|
||||
- [x] `VERIFY_TIMEOUT_MS` 8 s → 35 s (was firing mid-retry, causing premature Reset clicks) — `53fd5e7de`
|
||||
- [x] `sessionStorage` try-catch (iOS Safari Private Browsing throws on access) — `53fd5e7de`
|
||||
- [x] Appshell stores guarded behind `account_id` — `8850db89c`
|
||||
- [x] Recovery meetings over-filtering bug (API `default_qry_str`) — `76e21b08f`
|
||||
- [x] A→Z sort in recovery meetings API revalidation path — `c0386f27b`
|
||||
- [x] `events.event` IDB content version bump (stale cache) — previous commit
|
||||
|
||||
#### Root layout SWR verified safe:
|
||||
The root `+layout.ts` builds `ae_loc_init` as a plain site-config object (no `authenticated_access`,
|
||||
`trusted_access`, or `access_type` fields). The root layout sync effect
|
||||
`$ae_loc = { ...current_loc, ...ae_acct.loc }` therefore cannot overwrite Novi-set auth flags.
|
||||
Confirmed safe — this is NOT a cause of Access Denied.
|
||||
|
||||
#### Remaining architectural note:
|
||||
The long-term fix for the coarse `$ae_loc` reactivity (Svelte 4 store) causing Effect 2 to
|
||||
re-run on unrelated writes is tracked under **[Stores] Svelte 4 → Svelte 5 State Migration**
|
||||
below. The TTL + `verify_in_flight` guards are the current mitigation.
|
||||
|
||||
---
|
||||
|
||||
### [Stores] Svelte 4 → Svelte 5 State Migration (prerequisite for Phase 2c)
|
||||
The app uses `svelte-persisted-store` (Svelte 4 store contract) for all core persisted state
|
||||
(`ae_loc`, `idaa_loc`, `ae_api`, `ae_sess`, etc.). In Svelte 5 `$effect`, reading **any field**
|
||||
@@ -217,20 +251,20 @@ suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a pac
|
||||
- `$effect` auth guards in IDAA page components are reactivity guards (prevent spurious SWR calls on coarse `$ae_loc` writes), NOT auth-bypass guards. SvelteKit layout hierarchy already prevents child components from mounting when `(idaa)/+layout.svelte` blocks rendering.
|
||||
- Doc: SvelteKit layout hierarchy security model captured in `GUIDE__SvelteKit2_Svelte5_DexieJS.md` and `BOOTSTRAP__AI_Agent_Quickstart.md` (Mistake #7).
|
||||
|
||||
- [ ] **[IDAA] Make `contact_li_json_ext` searchable — Recovery Meeting contact search (2026-04-08)**
|
||||
Members cannot search for meetings by contact name or email. `contact_li_json` data is not
|
||||
included in `default_qry_str` and MariaDB cannot substring-search a JSON longtext directly.
|
||||
The `event` table already has `contact_li_json_ext` (STORED GENERATED, indexed) to work around this.
|
||||
- [ ] **[IDAA] IDB fast-path contact search — Recovery Meetings (2026-04-08, updated 2026-05-19)**
|
||||
**API path is now working** — `default_qry_str` already includes contact name/email.
|
||||
Two bugs were fixed 2026-05-19: (1) STORED GENERATED columns had stale values; forced
|
||||
rebuild via fake updates. (2) Frontend secondary filter was re-checking text against
|
||||
response fields, silently dropping API results that matched only via `default_qry_str`.
|
||||
|
||||
**Backend (blocked on this first):** Add `contact_li_json_ext` to the searchable fields
|
||||
whitelist for the `event` object type — likely a one-line change in `ae_obj_types_def.py`
|
||||
or the event object definition. Message sent to backend agent 2026-04-08.
|
||||
|
||||
**Frontend (after backend ships):**
|
||||
- `src/lib/ae_events/ae_events__event.ts` → `search__event()`: add `contact_li_json_ext`
|
||||
as an OR condition alongside `default_qry_str` when `qry_str` is present.
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path IDB filter: parse
|
||||
`contact_li_json` and include contact names/emails in the local text match check.
|
||||
**Remaining gap — IDB fast-path only:** The local cache fast-path returns all cached meetings
|
||||
without text filtering; users see the unfiltered list first, then the API-filtered result
|
||||
replaces it. To make contact matches appear instantly from cache:
|
||||
- `src/lib/ae_events/ae_events__event.ts` → fast-path filter in `search__event()`: parse
|
||||
`contact_li_json` and include contact names/emails in the local text match.
|
||||
- `src/routes/idaa/(idaa)/recovery_meetings/+page.svelte` fast-path display: same filter.
|
||||
Backend enhancement (`contact_li_json_ext` whitelist) is not required for this — the IDB
|
||||
records already store `contact_li_json` raw JSON which can be parsed client-side.
|
||||
|
||||
- [ ] **[IDAA] Optimize Recovery Meetings SQL VIEW and indexes.**
|
||||
The current search query can be taxing on the server. With ~150 active meetings, the view
|
||||
|
||||
18
src/app.css
18
src/app.css
@@ -233,12 +233,14 @@ html.trusted_access #appShell {
|
||||
font-display: swap;
|
||||
} */
|
||||
|
||||
/* modern theme */
|
||||
@font-face {
|
||||
/* modern theme — @font-face commented out 2026-05-19: Quicksand is declared but no
|
||||
CSS rule applies font-family:'Quicksand' to any element, so the browser never
|
||||
fetches this file. Re-enable if a theme or component starts using it. */
|
||||
/* @font-face {
|
||||
font-family: 'Quicksand';
|
||||
src: url('/fonts/Quicksand.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
} */
|
||||
|
||||
/* :root [data-theme='modern'] { */
|
||||
/* --theme-rounded-base: 20px;
|
||||
@@ -916,8 +918,14 @@ img.qr_code:focus {
|
||||
/* BEGIN: Overrides and fixes specific to Novi and IDAA */
|
||||
.iframe .novi_btn {
|
||||
border-radius: 60px;
|
||||
/* border-color: hsla(0, 0%, 50%, .5); */
|
||||
/* border-color: hsla(0, 0%, 0%, .15); */
|
||||
/* Bootstrap v3 (.btn) sets border:1px solid transparent and wins over
|
||||
Skeleton/Tailwind preset-outlined classes when loaded last. Use box-shadow
|
||||
instead — Bootstrap does not set box-shadow on .btn so it cannot strip it. */
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.iframe .novi_btn:hover {
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.5);
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.iframe .novi_m0 {
|
||||
|
||||
31
src/app.html
31
src/app.html
@@ -53,6 +53,8 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Google Fonts: commented out 2026-05-19 — no theme or component applies these families;
|
||||
all themes use system-ui/sans-serif. Re-enable if a theme is added that references them.
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
@@ -64,14 +66,43 @@
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
|
||||
rel="stylesheet" />
|
||||
-->
|
||||
|
||||
<!-- <link href="app.css" rel="stylesheet"> -->
|
||||
|
||||
<!-- Pre-JS loading indicator. Removed by root +layout.svelte onMount once Svelte
|
||||
bootstraps and the existing is_hydrating overlay takes over. Pointer-events:none
|
||||
so it never blocks interaction if something goes wrong with the remove call. -->
|
||||
<style>
|
||||
#ae_loader {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
#ae_loader::after {
|
||||
content: '';
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(120, 120, 120, 0.15);
|
||||
border-top-color: rgba(120, 120, 120, 0.45);
|
||||
animation: ae_loader_spin 0.75s linear infinite;
|
||||
}
|
||||
@keyframes ae_loader_spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<!-- h-full w-full overflow-auto -->
|
||||
<!-- overflow-x-scroll -->
|
||||
<body data-sveltekit-preload-data="hover" class="h-full w-full">
|
||||
<div id="ae_loader" aria-hidden="true"></div>
|
||||
<div style="display: contents" class="">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
let log_lvl: number = 0;
|
||||
|
||||
// *** Import Svelte specific
|
||||
import { untrack } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
|
||||
import '../app.css';
|
||||
@@ -53,6 +53,12 @@ interface Props {
|
||||
|
||||
let { data, children }: Props = $props();
|
||||
|
||||
// Remove the pre-JS loader from app.html as soon as Svelte mounts.
|
||||
// The existing is_hydrating overlay takes over from here.
|
||||
onMount(() => {
|
||||
document.getElementById('ae_loader')?.remove();
|
||||
});
|
||||
|
||||
// STABLE DERIVATION: Using prop data directly to avoid store loops
|
||||
let ae_acct = $derived(data[data.account_id]);
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ function clear_sess() {
|
||||
class="mx-1 inline-block text-gray-500 dark:text-gray-400" />
|
||||
<abbr title="Aether - Events Module" class="text-gray-500 dark:text-gray-400 font-semibold"> Æ Events </abbr>
|
||||
</span>
|
||||
{#if !$ae_sess?.disable_sys_header}
|
||||
{#if !$ae_sess?.disable_sys_header && $ae_loc.account_id}
|
||||
<Element_data_store
|
||||
ds_code="hub__site__appshell_header"
|
||||
ds_type="html" />
|
||||
@@ -364,7 +364,7 @@ function clear_sess() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if !$ae_sess?.disable_sys_footer}
|
||||
{#if !$ae_sess?.disable_sys_footer && $ae_loc.account_id}
|
||||
<footer
|
||||
class:hidden={yTop > 300}
|
||||
class:opacity-80={yTop < 250}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
let log_lvl: number = 0;
|
||||
|
||||
// *** Import Svelte specific
|
||||
@@ -63,6 +63,50 @@ 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);
|
||||
|
||||
// In-iframe reload helper.
|
||||
// After internal SvelteKit navigation the UUID is stripped from the URL — a bare
|
||||
// location.reload() would reload without it, so verification can't run and the user
|
||||
// sees "Access Denied" again. We save the initial load URL (which contains the UUID)
|
||||
// to sessionStorage on first mount and use it for all reload buttons.
|
||||
// sessionStorage is per-tab and cross-origin-isolated, so each Novi iframe instance
|
||||
// gets its own slot; it clears naturally when Novi closes/reopens the iframe.
|
||||
const IDAA_IFRAME_RELOAD_URL_KEY = 'idaa_iframe_reload_url';
|
||||
|
||||
onMount(() => {
|
||||
const uuid_in_url = new URLSearchParams(window.location.search).get('uuid');
|
||||
if (uuid_in_url) {
|
||||
// Guard: iOS Safari Private Browsing and some iframe sandbox configs throw on
|
||||
// sessionStorage access. Graceful fallback: skip the save; reload_with_uuid()
|
||||
// will fall back to location.reload() (loses UUID-preservation but doesn't crash).
|
||||
try {
|
||||
if (!sessionStorage.getItem(IDAA_IFRAME_RELOAD_URL_KEY)) {
|
||||
sessionStorage.setItem(IDAA_IFRAME_RELOAD_URL_KEY, window.location.href);
|
||||
}
|
||||
} catch {
|
||||
console.warn('IDAA Layout: sessionStorage unavailable — reload buttons will use location.reload() fallback.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function reload_with_uuid() {
|
||||
try {
|
||||
const initial_url = sessionStorage.getItem(IDAA_IFRAME_RELOAD_URL_KEY);
|
||||
if (initial_url && initial_url !== location.href) {
|
||||
location.href = initial_url;
|
||||
return;
|
||||
}
|
||||
} catch { /* sessionStorage unavailable — fall through to location.reload() */ }
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// Clear stale db_events.event IDB data on IDAA session start.
|
||||
//
|
||||
@@ -83,18 +127,14 @@ if (browser) {
|
||||
// Show a manual reset button if the spinner is still visible after this many ms.
|
||||
// Handles the case where site_cfg_json loads without novi_idaa_api_key (stale cache)
|
||||
// or the Novi API call hangs — the user would otherwise be stuck with no escape.
|
||||
const VERIFY_TIMEOUT_MS = 8000;
|
||||
//
|
||||
// WHY 35s: worst-case auto-retry cycle is 27s (12s first timeout + 3s wait + 12s retry).
|
||||
// If the auto-retries succeed or fail within that window, the spinner is already gone
|
||||
// (content shown or error panel shown) before this fires. The escape hatch only appears
|
||||
// for the rare case where the Novi API is slow-but-not-timing-out, or site_cfg_json
|
||||
// never loads. Previously 8s — fired mid-flight and caused premature Reset & Retry clicks.
|
||||
const VERIFY_TIMEOUT_MS = 35_000;
|
||||
|
||||
// One-time technical notice banner — persisted in localStorage so it only shows once.
|
||||
// TEMPORARY (2026-04-01): Remove this block after a few days.
|
||||
const TECH_NOTICE_KEY = 'idaa_tech_notice_2026_04_01_dismissed';
|
||||
let show_tech_notice: boolean = $state(
|
||||
browser ? localStorage.getItem(TECH_NOTICE_KEY) !== 'true' : false
|
||||
);
|
||||
function dismiss_tech_notice() {
|
||||
show_tech_notice = false;
|
||||
try { localStorage.setItem(TECH_NOTICE_KEY, 'true'); } catch { /* storage unavailable */ }
|
||||
}
|
||||
let verifying_timed_out: boolean = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
@@ -108,7 +148,7 @@ $effect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const VERIFIED_TTL_MS_DEFAULT = 5 * 60 * 1000; // 5 minutes
|
||||
const VERIFIED_TTL_MS_DEFAULT = 45 * 60 * 1000; // 45 minutes
|
||||
|
||||
// Effect 1: Set URL origin and params
|
||||
$effect(() => {
|
||||
@@ -140,6 +180,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) {
|
||||
@@ -202,14 +243,17 @@ $effect(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// TTL cache: skip if this UUID was recently verified.
|
||||
// Prevents duplicate API calls when site_cfg_json updates multiple times (SWR pattern).
|
||||
// TTL cache: skip if this UUID was recently verified AND $ae_loc still has permissions.
|
||||
// Without the permission check: if $ae_loc resets (e.g. browser restart while
|
||||
// $idaa_loc TTL is still valid), verification is skipped and the user hits Access Denied
|
||||
// because $ae_loc.authenticated_access is false. Re-running verify fixes both.
|
||||
const now = Date.now();
|
||||
if (
|
||||
$idaa_loc.novi_verified &&
|
||||
$idaa_loc.novi_uuid === current_uuid &&
|
||||
$idaa_loc.novi_verified_ts &&
|
||||
now - $idaa_loc.novi_verified_ts < ttl_ms
|
||||
now - $idaa_loc.novi_verified_ts < ttl_ms &&
|
||||
($ae_loc.trusted_access || $ae_loc.authenticated_access)
|
||||
) {
|
||||
if (log_lvl) console.log(`IDAA Layout: cached verification valid for ${current_uuid}`);
|
||||
novi_verifying = false;
|
||||
@@ -244,7 +288,14 @@ $effect(() => {
|
||||
* Verifies a Novi UUID against the Novi API and sets permissions accordingly.
|
||||
* "All or nothing" — if no API key is configured or the call fails, access is denied.
|
||||
* Called from within untrack(), so store writes here will not trigger reactive loops.
|
||||
* On a 429 rate-limit response, waits 10 seconds and retries once before failing.
|
||||
*
|
||||
* Retry policy:
|
||||
* 429 (rate limited) → wait 10s, retry once.
|
||||
* Network error / timeout → wait 3s, retry once.
|
||||
* Anything else (4xx, 5xx, empty 200) → fail immediately.
|
||||
*
|
||||
* The Novi fetch is wrapped in an AbortController with a 12s hard timeout.
|
||||
* Without it, a hung Novi server leaves verify_in_flight=true indefinitely.
|
||||
*/
|
||||
async function verify_novi_uuid(
|
||||
uuid: string,
|
||||
@@ -268,21 +319,36 @@ 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 {
|
||||
const headers = new Headers();
|
||||
headers.append('Authorization', `Basic ${api_key}`);
|
||||
const response = await fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
// Hard timeout: abort if Novi doesn't respond within 12 seconds.
|
||||
// Prevents verify_in_flight from getting stuck on a hung server.
|
||||
const abort_ctrl = new AbortController();
|
||||
const abort_timeout_id = setTimeout(() => abort_ctrl.abort(), 12_000);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${api_root_url}/customers/${uuid}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: abort_ctrl.signal
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(abort_timeout_id);
|
||||
}
|
||||
|
||||
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<void>((resolve) => setTimeout(resolve, 10_000));
|
||||
await verify_novi_uuid(uuid, api_key, api_root_url, true);
|
||||
return;
|
||||
@@ -293,6 +359,12 @@ 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);
|
||||
}
|
||||
|
||||
// NOTE: We are currently trusting that the pages in the Novi CMS are handling the actual current active member checks. It (Novi CMS page config) denies access to the page as a whole if they are not a member. 2026-05-19
|
||||
|
||||
// Build display name: prefer "First L." format, fall back to full Name field.
|
||||
const first_name = result?.FirstName ?? null;
|
||||
@@ -319,6 +391,9 @@ async function verify_novi_uuid(
|
||||
);
|
||||
}
|
||||
|
||||
// Other result fields for future use:
|
||||
// Role ("Regular User"), MembershipExpires, MemberStatus ("current"), CreatedDate, LastUpdatedDate, Groups ("GroupUniqueID", "GroupName", "InheritingMember", "JoinDate")
|
||||
|
||||
$idaa_loc.novi_uuid = uuid;
|
||||
$idaa_loc.novi_email = verified_email;
|
||||
$idaa_loc.novi_full_name = verified_name;
|
||||
@@ -353,12 +428,31 @@ async function verify_novi_uuid(
|
||||
$idaa_loc.bb.qry__hidden = 'not_hidden';
|
||||
$idaa_loc.bb.qry__enabled = 'enabled';
|
||||
} catch (error) {
|
||||
// Network errors (TypeError: Failed to fetch) and AbortError (our 12s timeout) get one
|
||||
// automatic retry after a short wait — they are almost always transient (cellular drop,
|
||||
// hotel WiFi hiccup, momentary Novi server lag).
|
||||
const is_network_or_timeout =
|
||||
error instanceof TypeError ||
|
||||
(error instanceof Error && error.name === 'AbortError');
|
||||
if (is_network_or_timeout && !is_retry) {
|
||||
const reason = error instanceof Error && error.name === 'AbortError'
|
||||
? 'timed out (no response in 12s)'
|
||||
: 'network error';
|
||||
console.warn(`IDAA Layout: Novi verification ${reason} for ${uuid}. Retrying in 3s...`);
|
||||
verifying_status_msg = 'Connection issue — retrying...';
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 3_000));
|
||||
await verify_novi_uuid(uuid, api_key, api_root_url, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verification failed — all-or-nothing means deny access.
|
||||
console.error(
|
||||
`IDAA Layout: Novi UUID verification failed for ${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 +469,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++;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !browser}
|
||||
@@ -389,7 +494,7 @@ async function verify_novi_uuid(
|
||||
class="container m-8 flex w-full flex-col items-center justify-center gap-2 p-8">
|
||||
<p class="text-center text-sm text-gray-500">
|
||||
<span class="fas fa-spinner fa-spin"></span>
|
||||
Verifying identity...
|
||||
{verifying_status_msg}
|
||||
</p>
|
||||
{#if verifying_timed_out}
|
||||
<!-- Escape hatch: shown after VERIFY_TIMEOUT_MS if still spinning.
|
||||
@@ -410,13 +515,85 @@ async function verify_novi_uuid(
|
||||
db_archives.archive.clear().catch(() => {});
|
||||
db_archives.content.clear().catch(() => {});
|
||||
db_events.event.clear().catch(() => {});
|
||||
location.reload();
|
||||
reload_with_uuid();
|
||||
}}>
|
||||
<span class="fas fa-redo m-1"></span>
|
||||
Reset & Retry
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if verify_error_type}
|
||||
<!-- Verification error — distinct from real access denial.
|
||||
A transient Novi API failure (network, 5xx, rate limit) should NOT look identical
|
||||
to a real "your UUID is not a member" denial. Provides Try Again without a reload,
|
||||
plus aggressive Clear Cache and Full Reset paths for persistent issues. -->
|
||||
<div class="container m-8 flex w-full flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<h1 class="font-bold">
|
||||
<span class="text-yellow-500 dark:text-yellow-400">
|
||||
<span class="fas fa-exclamation-circle mr-1"></span>
|
||||
Identity Verification Unavailable
|
||||
</span>
|
||||
</h1>
|
||||
{#if verify_error_type === 'rate_limited'}
|
||||
<p class="text-sm">
|
||||
The membership directory is temporarily busy (rate limited). Please wait a moment and try again.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm">
|
||||
We were unable to connect to the membership directory. This is usually a temporary network issue — it is not a problem with your membership or access.
|
||||
</p>
|
||||
{/if}
|
||||
<p class="text-xs italic text-gray-500">
|
||||
Try "Try Again" first. If the problem persists, use "Clear Cache & Reload".
|
||||
If nothing works, close this page and reopen IDAA from the menu on idaa.org.
|
||||
</p>
|
||||
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
|
||||
<!-- Try Again: unlatches verify_failed_for_uuid and re-runs Effect 2 — no reload needed -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handle_verify_retry}
|
||||
class="btn btn-sm preset-tonal-primary border-primary-500 border">
|
||||
<span class="fas fa-redo m-1"></span>
|
||||
Try Again
|
||||
</button>
|
||||
<!-- Clear Cache & Reload: wipes IDAA-specific localStorage + IDB, then reloads -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
localStorage.removeItem('ae_idaa_loc');
|
||||
console.log('IDAA Layout: Clear Cache & Reload — purging IDAA IDB tables.');
|
||||
db_posts.post.clear().catch(() => {});
|
||||
db_posts.comment.clear().catch(() => {});
|
||||
db_archives.archive.clear().catch(() => {});
|
||||
db_archives.content.clear().catch(() => {});
|
||||
db_events.event.clear().catch(() => {});
|
||||
reload_with_uuid();
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-surface preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all">
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Clear Cache & Reload
|
||||
</button>
|
||||
<!-- Full Reset: enumerates and deletes all IDB + clears all storage + reloads -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
if (!confirm('FULL RESET: Delete all caches and local storage, then reload?')) return;
|
||||
const db_list = await indexedDB.databases();
|
||||
for (const db of db_list) {
|
||||
if (db.name) indexedDB.deleteDatabase(db.name);
|
||||
}
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
// sessionStorage was just cleared, so reload_with_uuid() falls back to
|
||||
// location.reload() — that's correct since this is a full wipe.
|
||||
location.reload();
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-surface preset-outlined-error-100-900 hover:preset-filled-error-200-800 transition-all">
|
||||
<span class="fas fa-trash m-1"></span>
|
||||
Full Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $ae_loc.trusted_access || ($ae_loc.authenticated_access && $idaa_loc.novi_uuid && $idaa_loc.novi_verified)}
|
||||
<!-- TEMPORARY (2026-04-01): One-time notice about last night's technical issues. Remove after a few days. -->
|
||||
<!-- {#if show_tech_notice && 1 == 3}
|
||||
@@ -442,58 +619,78 @@ async function verify_novi_uuid(
|
||||
{/if} -->
|
||||
{@render children?.()}
|
||||
{#if $idaa_loc.novi_uuid}
|
||||
<span class="text-sm text-gray-500">
|
||||
Novi: <span class="fas fa-user m-1"></span>
|
||||
{$idaa_loc.novi_uuid}
|
||||
{$idaa_loc.novi_full_name ?? 'name not set'}
|
||||
{$idaa_loc.novi_email ?? 'email not set'}
|
||||
</span>
|
||||
{:else}
|
||||
<p class="text-center text-sm text-gray-500">
|
||||
IDAA Novi UUID not found in URL param!
|
||||
<p class="text-center text-xs text-gray-500">
|
||||
Novi: {$idaa_loc.novi_uuid} · {$idaa_loc.novi_full_name ?? 'name not set'} · {$idaa_loc.novi_email ?? 'email not set'}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Access Denied — shown only when verification is not in flight (novi_verifying=false),
|
||||
no API error (verify_error_type=null), and $ae_loc has no auth. Most common causes:
|
||||
(1) No UUID in URL and no cached session — genuine denial.
|
||||
(2) Timing race on first load — UUID arrives but $ae_loc not yet populated.
|
||||
(3) $ae_loc reset while $idaa_loc TTL cache was still valid (fixed via TTL+perms check).
|
||||
In iframe context the UUID is only on the initial Novi-provided URL, not on
|
||||
subsequent SvelteKit client-side navigations — reload_with_uuid() restores it. -->
|
||||
<div
|
||||
class="container m-8 flex w-full flex-col items-center justify-center gap-1 p-8 font-bold">
|
||||
<h1>
|
||||
class="container m-8 flex w-full flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<h1 class="font-bold">
|
||||
<span class="text-red-500">
|
||||
<span class="fas fa-exclamation-triangle"></span>
|
||||
Access Denied
|
||||
<span class="fas fa-exclamation-triangle"></span>
|
||||
</span>
|
||||
</h1>
|
||||
<p>You do not have access to these IDAA page.</p>
|
||||
<p class="text-sm">You do not have access to this IDAA page.</p>
|
||||
|
||||
{#if $ae_loc.iframe}
|
||||
In iframe mode
|
||||
{/if}
|
||||
|
||||
{#if $idaa_loc.novi_uuid}
|
||||
<span class="text-sm text-gray-500">
|
||||
Novi: <span class="fas fa-user m-1"></span>
|
||||
{$idaa_loc.novi_uuid}
|
||||
{$idaa_loc.novi_full_name ?? 'name not set'}
|
||||
{$idaa_loc.novi_email ?? 'email not set'}
|
||||
</span>
|
||||
{:else}
|
||||
<p>IDAA Novi UUID not found!</p>
|
||||
{/if}
|
||||
|
||||
{#if $ae_loc.iframe}
|
||||
<!-- WHY: In iframe mode the Novi UUID is passed via URL param on first load.
|
||||
If verification hasn't completed yet (timing race on Novi API), the user
|
||||
lands on Access Denied. Reloading the iframe re-triggers verification. -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-primary border-primary-500 mt-4 border"
|
||||
onclick={() => location.reload()}>
|
||||
<span class="fas fa-redo m-1"></span>
|
||||
Reload / Retry
|
||||
</button>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
If your session just started, try reloading.
|
||||
<p class="text-xs italic text-gray-500">
|
||||
If you just opened this page, try reloading. If the problem persists, try
|
||||
"Clear Cache & Reload", or close this page and reopen IDAA from the menu on idaa.org.
|
||||
</p>
|
||||
<div class="flex flex-row flex-wrap items-center justify-center gap-2">
|
||||
<!-- WHY: In iframe mode the Novi UUID is passed via URL param on first load.
|
||||
If verification was a timing race the user lands here. reload_with_uuid()
|
||||
restores the original URL (with UUID) — plain location.reload() would
|
||||
reload the current SvelteKit path which may not have the UUID. -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-primary border-primary-500 border"
|
||||
onclick={reload_with_uuid}>
|
||||
<span class="fas fa-redo m-1"></span>
|
||||
Reload / Retry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
localStorage.removeItem('ae_idaa_loc');
|
||||
db_posts.post.clear().catch(() => {});
|
||||
db_posts.comment.clear().catch(() => {});
|
||||
db_archives.archive.clear().catch(() => {});
|
||||
db_archives.content.clear().catch(() => {});
|
||||
db_events.event.clear().catch(() => {});
|
||||
reload_with_uuid();
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-surface preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all">
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Clear Cache & Reload
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<div class="mt-2 rounded border border-gray-300 bg-gray-50 p-2 text-left text-xs text-gray-500 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="font-mono">Debug (edit mode only)</p>
|
||||
{#if $idaa_loc.novi_uuid}
|
||||
<p>UUID: {$idaa_loc.novi_uuid}</p>
|
||||
<p>Name: {$idaa_loc.novi_full_name ?? 'not set'}</p>
|
||||
<p>Email: {$idaa_loc.novi_email ?? 'not set'}</p>
|
||||
{:else}
|
||||
<p>No Novi UUID in store</p>
|
||||
{/if}
|
||||
<p>iframe: {$ae_loc.iframe}</p>
|
||||
<p>authenticated_access: {$ae_loc.authenticated_access}</p>
|
||||
<p>trusted_access: {$ae_loc.trusted_access}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -47,6 +47,9 @@ let last_executed_key = '';
|
||||
// with a visible retry button — NOT the same "No meetings found" message a real empty
|
||||
// result would show. (Conflating the two was a key reason the bug went unsolved so long.)
|
||||
let auto_retry_count = 0;
|
||||
// Tracks the category of the last search error for a more informative user message.
|
||||
// ERR_NETWORK_CHANGED and other fetch failures surface as TypeError in JS.
|
||||
let qry_error_detail: 'network' | 'server' | null = $state(null);
|
||||
|
||||
// ── Escape-hatch: cache-reset button after suspicious zero-result delay ──────────────
|
||||
//
|
||||
@@ -84,7 +87,7 @@ let has_active_filters = $derived(
|
||||
let show_cache_reset_btn = $state(false);
|
||||
let cache_reset_timer: any = null;
|
||||
|
||||
// Starts/cancels the 8-second countdown based on no_results_no_filters.
|
||||
// Starts/cancels the 7-second countdown based on no_results_no_filters.
|
||||
// $effect only re-runs when the derived VALUE changes (true↔false), not on every
|
||||
// store write — so the timer counts down cleanly without spurious resets.
|
||||
$effect(() => {
|
||||
@@ -97,7 +100,7 @@ $effect(() => {
|
||||
show_cache_reset_btn = false;
|
||||
cache_reset_timer = setTimeout(() => {
|
||||
show_cache_reset_btn = true;
|
||||
}, 8000);
|
||||
}, 5000);
|
||||
} else {
|
||||
show_cache_reset_btn = false;
|
||||
}
|
||||
@@ -188,6 +191,7 @@ async function handle_search_refresh(qry_key: string) {
|
||||
);
|
||||
|
||||
$idaa_sess.recovery_meetings.qry__status = 'loading';
|
||||
qry_error_detail = null;
|
||||
|
||||
// If 'Remote First' is toggled (Admin only), we clear results immediately to show fresh state.
|
||||
if (remote_first) {
|
||||
@@ -297,8 +301,16 @@ async function handle_search_refresh(qry_key: string) {
|
||||
if (current_search_id === last_search_id) {
|
||||
let api_results = results || [];
|
||||
|
||||
// SECONDARY FILTER: Ensure API results respect exact UI filters (handles backend broadness)
|
||||
api_results = api_results.filter((ev: any) => {
|
||||
// SECONDARY FILTER: Re-apply structured filters the API may handle loosely.
|
||||
// WHY type/physical/virtual: the backend uses AND logic for body filters, so
|
||||
// physical+virtual together would incorrectly exclude either-only meetings —
|
||||
// we pass only one at a time and handle OR logic here.
|
||||
// WHY NOT qry_str: the API already applied exact LIKE search on default_qry_str
|
||||
// (a backend-generated combined index that includes contact name/email). Re-running
|
||||
// text filtering client-side against the response fields silently drops meetings
|
||||
// that matched only via default_qry_str (e.g., by contact name) because that
|
||||
// field may not be present in the response body or may not duplicate the match.
|
||||
api_results = api_results.filter((ev) => {
|
||||
if (qry_type && ev.type !== qry_type) return false;
|
||||
if (qry_physical || qry_virtual) {
|
||||
let match = false;
|
||||
@@ -306,27 +318,22 @@ async function handle_search_refresh(qry_key: string) {
|
||||
if (qry_virtual && ev.virtual == true) match = true;
|
||||
if (!match) return false;
|
||||
}
|
||||
if (qry_str && qry_str.length >= 3) {
|
||||
const name = (ev.name ?? '').toLowerCase();
|
||||
const desc = (ev.description ?? '').toLowerCase();
|
||||
const loc = (ev.location_text ?? '').toLowerCase();
|
||||
const dqs = (ev.default_qry_str ?? '').toLowerCase();
|
||||
if (
|
||||
!name.includes(qry_str) &&
|
||||
!desc.includes(qry_str) &&
|
||||
!loc.includes(qry_str) &&
|
||||
!dqs.includes(qry_str)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// RE-SORT: Ensure perfect chronological order even if API puts NULLs last (Refactored 2026-02-16)
|
||||
if ($idaa_loc.recovery_meetings.qry__order_by === 'name') {
|
||||
// RE-SORT: Match the fast-path IDB sort so stale/NULL API ordering is corrected.
|
||||
// WHY: The RE-SORT previously only checked 'name' (a legacy mode that no longer
|
||||
// exists in sort_modes). 'name_asc' and 'name_desc' fell through to the else
|
||||
// branch and were silently re-sorted chronologically, ignoring the user's selection.
|
||||
const sort_order = $idaa_loc.recovery_meetings.qry__order_by;
|
||||
if (sort_order === 'name_asc' || sort_order === 'name') {
|
||||
api_results.sort((a, b) =>
|
||||
(a.name ?? '').localeCompare(b.name ?? '')
|
||||
);
|
||||
} else if (sort_order === 'name_desc') {
|
||||
api_results.sort((a, b) =>
|
||||
(b.name ?? '').localeCompare(a.name ?? '')
|
||||
);
|
||||
} else {
|
||||
api_results.sort((a, b) =>
|
||||
(b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? '')
|
||||
@@ -380,7 +387,10 @@ async function handle_search_refresh(qry_key: string) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (current_search_id === last_search_id) {
|
||||
console.error('Revalidation failed:', error);
|
||||
// TypeError = network-level failure (ERR_NETWORK_CHANGED, offline, DNS failure).
|
||||
// These are transient by nature; show a different message than a server error.
|
||||
const is_network_err = error instanceof TypeError;
|
||||
console.error('Revalidation failed:', is_network_err ? `Network error: ${error}` : error);
|
||||
|
||||
if (auto_retry_count < 1) {
|
||||
// First failure — schedule one silent retry before surfacing the error.
|
||||
@@ -398,6 +408,7 @@ async function handle_search_refresh(qry_key: string) {
|
||||
// is not the same as a genuinely empty result. Conflating the two was why
|
||||
// the "no meetings found" bug went undiagnosed for ~1 year — staff and users
|
||||
// saw what looked like an empty list and assumed no data, not a failure.
|
||||
qry_error_detail = is_network_err ? 'network' : 'server';
|
||||
$idaa_sess.recovery_meetings.qry__status = 'error';
|
||||
event_id_li = [];
|
||||
}
|
||||
@@ -466,7 +477,13 @@ if (browser) {
|
||||
{:else if $idaa_sess.recovery_meetings.qry__status === 'error'}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p>Unable to load meetings. Please try again.</p>
|
||||
<p>
|
||||
{#if qry_error_detail === 'network'}
|
||||
Network connection interrupted — please check your connection and try again.
|
||||
{:else}
|
||||
Unable to load meetings — server error. Please try again.
|
||||
{/if}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-primary m-auto"
|
||||
@@ -509,7 +526,7 @@ if (browser) {
|
||||
class="btn btn-sm preset-tonal-warning m-auto"
|
||||
onclick={handle_cache_reset}>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Refresh Meeting Cache
|
||||
Refresh Meeting List Cache
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -377,14 +377,20 @@ $effect(() => {
|
||||
</section>
|
||||
|
||||
<!-- The footer for the IDAA section of the site -->
|
||||
<section
|
||||
class="module_footer footer_content lg:text-md xl:text-md flex min-h-7 px-1 py-0.5 text-sm text-slate-400 transition hover:text-slate-800 sm:text-sm 2xl:text-lg"
|
||||
class:ae_debug={$ae_loc?.debug}>
|
||||
<Element_data_store
|
||||
ds_code="hub__site__appshell_footer"
|
||||
ds_type="html"
|
||||
class_li="grow flex flex-row justify-between" />
|
||||
</section>
|
||||
<!-- WHY the account_id guard: the outer IDAA layout renders before the inner (idaa) layout
|
||||
completes Novi UUID verification. Without it, element_data_store fires immediately with
|
||||
no account_id in the headers → 403. The footer is IDAA-specific content so it should
|
||||
only load once the member's identity is established. -->
|
||||
{#if $ae_loc.account_id}
|
||||
<section
|
||||
class="module_footer footer_content lg:text-md xl:text-md flex min-h-7 px-1 py-0.5 text-sm text-slate-400 transition hover:text-slate-800 sm:text-sm 2xl:text-lg"
|
||||
class:ae_debug={$ae_loc?.debug}>
|
||||
<Element_data_store
|
||||
ds_code="hub__site__appshell_footer"
|
||||
ds_type="html"
|
||||
class_li="grow flex flex-row justify-between" />
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- </AppShell> -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user