feat(idaa): add /idaa/clear-caches page for Novi iframe cache reset

Clears all IDB databases, localStorage, and sessionStorage for the
prod-idaa.oneskyit.com origin when loaded as an iframe inside www.idaa.org.
Targets the partitioned storage bucket used by IDAA Novi iframes — direct
navigation to the site clears a different partition and has no effect.

Uses Novi-safe styling (explicit bg/text surfaces, no bare h1 elements,
inline styles on links) to survive Bootstrap v3 CSS injected by Novi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-11 13:01:34 -04:00
parent a5beff4aa8
commit 05841350fe

View File

@@ -0,0 +1,138 @@
<script lang="ts">
import { browser } from '$app/environment';
import { TriangleAlert, CircleCheck, Loader } from '@lucide/svelte';
type StepStatus = 'pending' | 'running' | 'done' | 'error';
interface ClearStep {
label: string;
status: StepStatus;
detail: string;
}
let steps: ClearStep[] = $state([
{ label: 'IndexedDB databases', status: 'pending', detail: '' },
{ label: 'Local storage', status: 'pending', detail: '' },
{ label: 'Session storage', status: 'pending', detail: '' }
]);
let overall_done = $state(false);
let had_error = $state(false);
async function clear_all_caches() {
// IDB — enumerate and delete every database on this origin
steps[0].status = 'running';
try {
const db_list = await indexedDB.databases();
for (const db of db_list) {
if (db.name) indexedDB.deleteDatabase(db.name);
}
steps[0].status = 'done';
steps[0].detail = `${db_list.length} database${db_list.length !== 1 ? 's' : ''} cleared`;
} catch (e) {
steps[0].status = 'error';
steps[0].detail = String(e);
had_error = true;
}
// localStorage
steps[1].status = 'running';
try {
localStorage.clear();
steps[1].status = 'done';
steps[1].detail = 'Cleared';
} catch (e) {
steps[1].status = 'error';
steps[1].detail = String(e);
had_error = true;
}
// sessionStorage
steps[2].status = 'running';
try {
sessionStorage.clear();
steps[2].status = 'done';
steps[2].detail = 'Cleared';
} catch (e) {
steps[2].status = 'error';
steps[2].detail = String(e);
had_error = true;
}
overall_done = true;
// Notify parent window (Novi page) that the clear is complete.
// The Novi page can optionally listen for this to show a confirmation to the member.
try {
window.parent.postMessage({ ae_cache_cleared: true, had_error }, '*');
} catch (_) {
// not in an iframe — ignore
}
}
// Run once on mount. Reads no reactive state so $effect never re-fires.
$effect(() => {
if (!browser) return;
clear_all_caches();
});
</script>
<!-- Explicit bg+text surfaces so Bootstrap v3 (injected by Novi in iframe context) cannot
cause white-on-white. h1/h2 are avoided — Bootstrap targets bare heading elements. -->
<div class="mx-auto mt-8 flex max-w-sm flex-col items-center gap-6 rounded-lg bg-white py-10 text-center text-gray-900 dark:bg-gray-900 dark:text-gray-100">
{#if !overall_done}
<Loader size={40} class="animate-spin text-blue-500" />
<div class="text-xl font-semibold">
Clearing saved data&hellip;
</div>
{:else if had_error}
<TriangleAlert size={40} class="text-warning-500" />
<div class="text-xl font-semibold">
Cleared with some errors
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Most caches were cleared. Close this tab and reload the IDAA pages to
get the latest version.
</p>
{:else}
<CircleCheck size={40} class="text-success-500" />
<div class="text-xl font-semibold">
All caches cleared
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Close this tab and reload the IDAA pages to get the latest version.
</p>
{/if}
<ul class="w-full rounded border border-gray-200 text-left text-sm dark:border-gray-700">
{#each steps as step (step.label)}
<li
class="flex items-center justify-between gap-2 border-b border-gray-100 px-3 py-2 last:border-b-0 dark:border-gray-700">
<span class="text-gray-700 dark:text-gray-300">{step.label}</span>
<span class="flex items-center gap-1 text-xs">
{#if step.status === 'pending'}
<span class="text-gray-400"></span>
{:else if step.status === 'running'}
<Loader size="0.85em" class="animate-spin text-blue-400" />
{:else if step.status === 'done'}
<CircleCheck size="0.85em" class="text-success-500" />
<span class="text-gray-600 dark:text-gray-400">{step.detail}</span>
{:else if step.status === 'error'}
<TriangleAlert size="0.85em" class="text-error-500" />
<span class="text-error-500">{step.detail}</span>
{/if}
</span>
</li>
{/each}
</ul>
{#if overall_done}
<p class="text-sm text-gray-700 dark:text-gray-300">
If you were having trouble with the IDAA
<a href="https://www.idaa.org/idaa-meetings" style="color:#2563eb;text-decoration:underline;" class="text-blue-600 underline">Meeting List</a>,
<a href="https://www.idaa.org/idaa-archives" style="color:#2563eb;text-decoration:underline;" class="text-blue-600 underline">Archives</a>, or
<a href="https://www.idaa.org/idaa-bulletin-board" style="color:#2563eb;text-decoration:underline;" class="text-blue-600 underline">Bulletin Board</a>,
please try once more. We apologize for the inconvenience, and thank you for your patience while we work to improve the IDAA experience!
</p>
{/if}
</div>