♻️ refactor: centralize global error handler in shared-ui

Extract setupGlobalErrorHandler() utility from contacts app and add to
@manacore/shared-ui. Migrate 7 apps to use the shared implementation:
calendar, chat, clock, contacts, matrix, picture, storage.

Features:
- Catches unhandled promise rejections with error classification
- Handles offline/online network status changes
- Built-in i18n (DE + EN) with customizable translations
- Optional onAuthError callback for redirect handling
- Returns cleanup function for proper unmounting
This commit is contained in:
Till-JS 2026-01-29 15:17:17 +01:00
parent aca66b2014
commit cdac341882
11 changed files with 307 additions and 119 deletions

View file

@ -3,7 +3,7 @@
import '$lib/i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { ToastContainer } from '@manacore/shared-ui';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
import { isLoading as i18nLoading } from 'svelte-i18n';
import { onMount } from 'svelte';
@ -13,10 +13,16 @@
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
onMount(() => {
// Setup global error handling
const cleanupErrorHandler = setupGlobalErrorHandler();
theme.initialize();
await authStore.initialize();
loading = false;
authStore.initialize().then(() => {
loading = false;
});
return cleanupErrorHandler;
});
</script>

View file

@ -2,13 +2,18 @@
import '../app.css';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import { ToastContainer } from '@manacore/shared-ui';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
let { children } = $props();
onMount(() => {
const cleanup = theme.initialize();
return cleanup;
const cleanupErrorHandler = setupGlobalErrorHandler();
const cleanupTheme = theme.initialize();
return () => {
cleanupErrorHandler();
cleanupTheme();
};
});
</script>

View file

@ -5,24 +5,34 @@
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { waitLocale } from '$lib/i18n';
import { ToastContainer } from '@manacore/shared-ui';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
let loading = $state(true);
onMount(async () => {
// Wait for locale to be loaded
await waitLocale();
onMount(() => {
// Setup global error handling
const cleanupErrorHandler = setupGlobalErrorHandler();
// Initialize theme
theme.initialize();
// Initialize async operations
const init = async () => {
// Wait for locale to be loaded
await waitLocale();
// Initialize auth
await authStore.initialize();
// Initialize theme
theme.initialize();
loading = false;
// Initialize auth
await authStore.initialize();
loading = false;
};
init();
return cleanupErrorHandler;
});
</script>

View file

@ -2,11 +2,10 @@
import '../app.css';
import '$lib/i18n'; // Initialize i18n early
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { isLoading as i18nLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { toastStore, ToastContainer } from '@manacore/shared-ui';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
@ -16,73 +15,19 @@
// Derived state: app is ready when auth is initialized AND i18n is loaded
let appReady = $derived(!loading && !$i18nLoading);
/**
* Global error handler for unhandled promise rejections and API errors
*/
function setupGlobalErrorHandling() {
if (!browser) return;
// Handle unhandled promise rejections (e.g., failed API calls)
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason;
// Extract error message
let message = 'Ein unerwarteter Fehler ist aufgetreten';
if (error instanceof Error) {
// Network errors
if (error.message === 'Failed to fetch' || error.name === 'TypeError') {
message = 'Netzwerkfehler: Server nicht erreichbar';
}
// Auth errors
else if (
error.message.includes('401') ||
error.message.toLowerCase().includes('unauthorized')
) {
message = 'Sitzung abgelaufen. Bitte erneut anmelden.';
}
// Other API errors
else if (error.message) {
message = error.message;
}
}
// Show toast notification
toastStore.error(message);
// Prevent default browser error handling
event.preventDefault();
});
// Handle general JavaScript errors
window.addEventListener('error', (event) => {
// Only handle non-script errors (network failures for resources, etc.)
if (event.message && !event.filename) {
toastStore.error('Ein Fehler ist aufgetreten');
}
});
// Handle offline/online status
window.addEventListener('offline', () => {
toastStore.warning('Keine Internetverbindung', 10000);
});
window.addEventListener('online', () => {
toastStore.success('Verbindung wiederhergestellt');
});
}
onMount(async () => {
// Setup global error handling
setupGlobalErrorHandling();
onMount(() => {
// Setup global error handling (German translations by default)
const cleanupErrorHandler = setupGlobalErrorHandler();
// Initialize theme
theme.initialize();
// Initialize auth
await authStore.initialize();
authStore.initialize().then(() => {
loading = false;
});
loading = false;
return cleanupErrorHandler;
});
</script>

View file

@ -5,7 +5,7 @@
import type { Snippet } from 'svelte';
import { isLoading as i18nLoading, _ as t } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { ToastContainer } from '@manacore/shared-ui';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
interface Props {
children: Snippet;
@ -14,8 +14,13 @@
let { children }: Props = $props();
onMount(() => {
const cleanup = theme.initialize();
return cleanup;
const cleanupErrorHandler = setupGlobalErrorHandler();
const cleanupTheme = theme.initialize();
return () => {
cleanupErrorHandler();
cleanupTheme();
};
});
</script>

View file

@ -2,7 +2,7 @@
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { authStore } from '$lib/stores/auth.svelte';
import { ToastContainer } from '@manacore/shared-ui';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { onMount } from 'svelte';
// Import and initialize theme
@ -14,6 +14,9 @@
let { children, data } = $props();
onMount(() => {
// Setup global error handling
const cleanupErrorHandler = setupGlobalErrorHandler();
// Initialize theme (applies CSS variables and loads from localStorage)
const cleanupTheme = theme.initialize();
@ -21,6 +24,7 @@
authStore.initialize();
return () => {
cleanupErrorHandler();
cleanupTheme();
};
});

View file

@ -3,7 +3,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { locale } from 'svelte-i18n';
import { PillNavigation } from '@manacore/shared-ui';
import { PillNavigation, setupGlobalErrorHandler } from '@manacore/shared-ui';
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
@ -140,31 +140,41 @@
goto('/login');
}
onMount(async () => {
// Initialize theme
theme.initialize();
onMount(() => {
// Setup global error handling
const cleanupErrorHandler = setupGlobalErrorHandler();
// Initialize auth
await authStore.initialize();
// Initialize async operations
const init = async () => {
// Initialize theme
theme.initialize();
// Load user settings
await userSettings.load();
// Initialize auth
await authStore.initialize();
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('storage-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
sidebarModeStore.set(true);
}
// Load user settings
await userSettings.load();
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('storage-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
// Initialize sidebar mode from localStorage
const savedSidebar = localStorage.getItem('storage-nav-sidebar');
if (savedSidebar === 'true') {
isSidebarMode = true;
sidebarModeStore.set(true);
}
loading = false;
// Initialize collapsed state from localStorage
const savedCollapsed = localStorage.getItem('storage-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
loading = false;
};
init();
return cleanupErrorHandler;
});
</script>