Implemented offline-first fast-paths and hardened API/Layout resilience. Added reactive offline banner, root error page, and ghost site fallbacks to handle server downtime and connection loss without crashing.
This commit is contained in:
63
src/routes/+error.svelte
Normal file
63
src/routes/+error.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { RefreshCw, Home, AlertTriangle } from '@lucide/svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let status = $derived(page.status);
|
||||
let message = $derived(page.error?.message || 'An unexpected error occurred');
|
||||
|
||||
// Check if it looks like a connection/API failure
|
||||
let is_connection_error = $derived(
|
||||
message.toLowerCase().includes('site lookup failed') ||
|
||||
message.toLowerCase().includes('fetch') ||
|
||||
status === 500
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center min-h-screen p-4 bg-surface-50 dark:bg-surface-900 text-center">
|
||||
<div class="max-w-md p-8 rounded-2xl shadow-xl bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
|
||||
|
||||
<div class="mb-6 flex justify-center">
|
||||
<div class="p-4 rounded-full bg-error-100 dark:bg-error-900 text-error-600 dark:text-error-400">
|
||||
<AlertTriangle size={48} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-6xl font-black mb-2 text-surface-900 dark:text-white">{status}</h1>
|
||||
<h2 class="text-2xl font-bold mb-4 text-surface-700 dark:text-surface-200">
|
||||
{#if is_connection_error}
|
||||
Connection Failure
|
||||
{:else}
|
||||
Something went wrong
|
||||
{/if}
|
||||
</h2>
|
||||
|
||||
<p class="text-surface-600 dark:text-surface-400 mb-8 leading-relaxed">
|
||||
{message}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
onclick={() => browser && window.location.reload()}
|
||||
class="btn btn-lg preset-filled-primary-500 hover:preset-filled-primary-600 w-full font-bold"
|
||||
>
|
||||
<RefreshCw class="mr-2 size-5" />
|
||||
Retry Connection
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
class="btn btn-lg preset-outlined-surface-500 w-full font-bold"
|
||||
>
|
||||
<Home class="mr-2 size-5" />
|
||||
Return Home
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if is_connection_error}
|
||||
<div class="mt-8 pt-6 border-t border-surface-100 dark:border-surface-700 text-sm text-surface-500">
|
||||
<p>If you are onsite at an event, please check your network connection or contact the registration desk.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,6 +31,7 @@
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { online } from 'svelte/reactivity/window';
|
||||
import xml from 'highlight.js/lib/languages/xml'; // for HTML
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
@@ -110,6 +111,10 @@
|
||||
let flag_denied: boolean = $state(false); // Access Denied
|
||||
// let flag_reason: string = $state(''); // Reason: New version, Expired Cache, Access Denied
|
||||
|
||||
// Connection Status
|
||||
let is_offline = $derived(browser && online.current === false);
|
||||
let api_unreachable = $derived($ae_loc?.account_id === 'ghost');
|
||||
|
||||
// BEGIN: Sanity Checks:
|
||||
// Added 2025-07-15
|
||||
if (!$ae_loc?.sys_menu) {
|
||||
@@ -773,6 +778,30 @@
|
||||
<!-- <link rel="manifest" href="/manifest.json"> -->
|
||||
</svelte:head>
|
||||
|
||||
{#if browser && (is_offline || api_unreachable)}
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 z-[100] p-4 bg-orange-600 text-white text-center shadow-2xl flex flex-row items-center justify-center gap-4"
|
||||
>
|
||||
<span class="text-xl font-bold">
|
||||
{#if is_offline}
|
||||
<span class="fas fa-wifi-slash mr-2"></span>
|
||||
Connection Offline
|
||||
{:else}
|
||||
<span class="fas fa-server mr-2"></span>
|
||||
API Server Unreachable
|
||||
{/if}
|
||||
</span>
|
||||
<span class="hidden md:inline">Viewing cached data. Changes may not be saved.</span>
|
||||
<button
|
||||
class="btn btn-sm variant-filled-white text-orange-600 font-bold"
|
||||
onclick={() => window.location.reload()}
|
||||
>
|
||||
<RefreshCw class="inline-block mr-1 size-4" />
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $ae_loc?.site_google_tracking_id && $ae_loc?.site_google_tracking_id.length > 0}
|
||||
<Analytics bind:site_google_tracking_id={$ae_loc.site_google_tracking_id} />
|
||||
{/if}
|
||||
|
||||
@@ -6,6 +6,9 @@ import { lookup_site_domain } from '$lib/ae_core/ae_core__site';
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
import type { ae_SiteDomain } from '$lib/types/ae_types';
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
|
||||
import {
|
||||
PUBLIC_AE_API_PROTOCOL,
|
||||
PUBLIC_AE_API_SERVER,
|
||||
@@ -104,25 +107,41 @@ export async function load({ fetch, params, parent, route, url }) {
|
||||
|
||||
const fqdn = url.host;
|
||||
|
||||
const result = await lookup_site_domain({
|
||||
api_cfg: ae_api_init,
|
||||
fqdn,
|
||||
view: 'base',
|
||||
log_lvl
|
||||
}).catch((err) => {
|
||||
console.log('Site lookup failed in root layout.', err);
|
||||
error(500, {
|
||||
message: 'Site lookup aborted or failed! Check network and API.'
|
||||
});
|
||||
});
|
||||
|
||||
if (result === null) {
|
||||
error(403, {
|
||||
message: 'The site lookup failed! Check that the domain name is configured and enabled.'
|
||||
let result: ae_SiteDomain | null = null;
|
||||
try {
|
||||
result = await lookup_site_domain({
|
||||
api_cfg: ae_api_init,
|
||||
fqdn,
|
||||
view: 'base',
|
||||
log_lvl
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Site lookup critical failure in root layout.', err);
|
||||
}
|
||||
|
||||
const json_data = result;
|
||||
if (result === null) {
|
||||
console.warn('Site lookup returned null. Attempting emergency ghost fallback.');
|
||||
// This is a last resort if the internal library fallback also failed
|
||||
result = {
|
||||
id: 'ghost',
|
||||
id_random: 'ghost',
|
||||
account_id_random: 'ghost',
|
||||
account_code: 'ghost',
|
||||
account_name: 'Ghost Account',
|
||||
site_id_random: 'ghost',
|
||||
site_domain_id_random: 'ghost',
|
||||
enable: '1',
|
||||
header_image_path: '',
|
||||
style_href: '',
|
||||
google_tracking_id: '',
|
||||
access_code_kv_json: {},
|
||||
cfg_json: {},
|
||||
access_key: '',
|
||||
site_domain_access_key: ''
|
||||
} as any;
|
||||
}
|
||||
|
||||
const json_data = result as any;
|
||||
account_id = json_data.account_id_random;
|
||||
data_struct.account_id = json_data.account_id_random;
|
||||
ae_acct.account_id = json_data.account_id_random;
|
||||
|
||||
@@ -14,7 +14,17 @@ export async function load({ params, parent, url }) {
|
||||
data.log_lvl = log_lvl;
|
||||
|
||||
const account_id = data.account_id;
|
||||
const ae_acct = data[account_id];
|
||||
let ae_acct = data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Events - [event_id] launcher +layout.ts: Account ${account_id} not found in data. Initializing ghost acct.`);
|
||||
ae_acct = {
|
||||
api: data.ae_api || {},
|
||||
slct: {
|
||||
account_id: account_id
|
||||
}
|
||||
};
|
||||
}
|
||||
// console.log(`ae_acct = `, ae_acct);
|
||||
|
||||
const event_id = params.event_id;
|
||||
|
||||
@@ -14,19 +14,29 @@ export async function load({ params, parent, url }) {
|
||||
data.log_lvl = log_lvl;
|
||||
|
||||
const account_id = data.account_id;
|
||||
const ae_acct = data[account_id];
|
||||
let ae_acct = data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Events - [event_id] launcher [event_location_id] +page.ts: Account ${account_id} not found in data. Initializing ghost acct.`);
|
||||
ae_acct = {
|
||||
api: data.ae_api || {},
|
||||
slct: {
|
||||
account_id: account_id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const event_location_id = params.event_location_id;
|
||||
if (!event_location_id) {
|
||||
console.log(
|
||||
console.warn(
|
||||
`ae Events - [event_id] launcher [event_location_id] +page.ts: The event_location_id was not found in the params.event_location_id!!!`
|
||||
);
|
||||
error(404, {
|
||||
message: 'Events Pres Mgmt - Event Location ID not found'
|
||||
});
|
||||
// error(404, {
|
||||
// message: 'Events Pres Mgmt - Event Location ID not found'
|
||||
// });
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
if (browser && event_location_id) {
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`ae_events launcher [event_location_id] +page.ts: event_location_id = `,
|
||||
@@ -85,7 +95,7 @@ export async function load({ params, parent, url }) {
|
||||
// ae_acct.slct.event_session_obj = load_event_session_obj;
|
||||
// }
|
||||
} else {
|
||||
console.log(`ae pres_mgmt launcher [slug] +page.ts: browser = false`);
|
||||
console.log(`ae pres_mgmt launcher [slug] +page.ts: browser = false or location_id missing`);
|
||||
}
|
||||
|
||||
// WARNING: Precaution against shared data between sites and sessions.
|
||||
|
||||
@@ -584,13 +584,17 @@
|
||||
)} mx-0.5"
|
||||
></span>
|
||||
{event_file_obj.extension}
|
||||
{#if result === null}
|
||||
<span>
|
||||
{#if result === null || result === false}
|
||||
<span class="text-error-500">
|
||||
<span class="fas fa-exclamation-triangle mx-1"></span>
|
||||
Download failed!
|
||||
Failed!
|
||||
</span>
|
||||
{/if}
|
||||
<!-- </span> -->
|
||||
{:catch error}
|
||||
<span class="text-error-500" title={error?.message}>
|
||||
<span class="fas fa-exclamation-circle mx-0.5"></span>
|
||||
Error!
|
||||
</span>
|
||||
{/await}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -15,7 +15,17 @@ export async function load({ params, parent, url }) {
|
||||
data.log_lvl = log_lvl;
|
||||
|
||||
const account_id = data.account_id;
|
||||
const ae_acct = data[account_id];
|
||||
let ae_acct = data[account_id];
|
||||
|
||||
if (!ae_acct) {
|
||||
console.warn(`ae Events - [event_id] +layout.ts: Account ${account_id} not found in data. Initializing ghost acct.`);
|
||||
ae_acct = {
|
||||
api: data.ae_api || {},
|
||||
slct: {
|
||||
account_id: account_id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const event_id = params.event_id;
|
||||
if (!event_id) {
|
||||
@@ -54,16 +64,18 @@ export async function load({ params, parent, url }) {
|
||||
// }
|
||||
});
|
||||
if (!load_event_obj) {
|
||||
error(404, {
|
||||
message: 'Events - Event not found'
|
||||
});
|
||||
console.warn(`Events - [event_id] +layout.ts: Event ${event_id} not found via API or Cache.`);
|
||||
// error(404, {
|
||||
// message: 'Events - Event not found'
|
||||
// });
|
||||
} else {
|
||||
console.log(`load_event_obj = `, load_event_obj);
|
||||
ae_acct.slct.event_obj = load_event_obj;
|
||||
// ae_acct.slct.event_device_obj_li = load_event_obj.event_device_obj_li;
|
||||
ae_acct.slct.event_location_obj_li = load_event_obj.event_location_obj_li;
|
||||
ae_acct.slct.event_session_obj_li = load_event_obj.event_session_obj_li;
|
||||
ae_acct.slct.badge_template_obj_li = load_event_obj.event_badge_template_obj_li;
|
||||
}
|
||||
console.log(`load_event_obj = `, load_event_obj);
|
||||
ae_acct.slct.event_obj = load_event_obj;
|
||||
// ae_acct.slct.event_device_obj_li = load_event_obj.event_device_obj_li;
|
||||
ae_acct.slct.event_location_obj_li = load_event_obj.event_location_obj_li;
|
||||
ae_acct.slct.event_session_obj_li = load_event_obj.event_session_obj_li;
|
||||
ae_acct.slct.badge_template_obj_li = load_event_obj.event_badge_template_obj_li;
|
||||
}
|
||||
|
||||
// WARNING: Precaution against shared data between sites and sessions.
|
||||
|
||||
Reference in New Issue
Block a user