diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte index fcbf8de56..72bb7bc61 100644 --- a/apps/calendar/apps/web/src/routes/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/+layout.svelte @@ -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; }); diff --git a/apps/chat/apps/web/src/routes/+layout.svelte b/apps/chat/apps/web/src/routes/+layout.svelte index 1e6853364..2c89e2faf 100644 --- a/apps/chat/apps/web/src/routes/+layout.svelte +++ b/apps/chat/apps/web/src/routes/+layout.svelte @@ -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(); + }; }); diff --git a/apps/clock/apps/web/src/routes/+layout.svelte b/apps/clock/apps/web/src/routes/+layout.svelte index ceaec48ce..489b4af31 100644 --- a/apps/clock/apps/web/src/routes/+layout.svelte +++ b/apps/clock/apps/web/src/routes/+layout.svelte @@ -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; }); diff --git a/apps/contacts/apps/web/src/routes/+layout.svelte b/apps/contacts/apps/web/src/routes/+layout.svelte index 35cbef0d2..ef2718de6 100644 --- a/apps/contacts/apps/web/src/routes/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/+layout.svelte @@ -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; }); diff --git a/apps/matrix/apps/web/src/routes/+layout.svelte b/apps/matrix/apps/web/src/routes/+layout.svelte index 44996b54e..fd716edc6 100644 --- a/apps/matrix/apps/web/src/routes/+layout.svelte +++ b/apps/matrix/apps/web/src/routes/+layout.svelte @@ -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(); + }; }); diff --git a/apps/picture/apps/web/src/routes/+layout.svelte b/apps/picture/apps/web/src/routes/+layout.svelte index 10de2e8ec..eece69687 100644 --- a/apps/picture/apps/web/src/routes/+layout.svelte +++ b/apps/picture/apps/web/src/routes/+layout.svelte @@ -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(); }; }); diff --git a/apps/storage/apps/web/src/routes/+layout.svelte b/apps/storage/apps/web/src/routes/+layout.svelte index 96addf775..41d8e1439 100644 --- a/apps/storage/apps/web/src/routes/+layout.svelte +++ b/apps/storage/apps/web/src/routes/+layout.svelte @@ -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; }); diff --git a/docs/CONSISTENCY_REPORT.md b/docs/CONSISTENCY_REPORT.md index ca2bf53d0..cbb8b9adc 100644 --- a/docs/CONSISTENCY_REPORT.md +++ b/docs/CONSISTENCY_REPORT.md @@ -26,6 +26,7 @@ Nach eingehender Analyse aller Web-Apps im Monorepo wurden folgende Bereiche auf 5. ✅ **@manacore/shared-api-client Package erstellt** - 10 Apps migriert (clock, todo, contacts, storage, calendar, picture, nutriphi, planta, questions, skilltree) 6. ✅ **i18n zu 6 Apps hinzugefügt** - todo, skilltree, nutriphi, planta, questions, matrix (jeweils DE + EN) 7. ✅ **AuthGateModal zentralisiert** - `@manacore/shared-auth-ui` für 4 Apps (chat, todo, contacts, calendar) +8. ✅ **Global Error Handler zentralisiert** - `@manacore/shared-ui` für 7 Apps (calendar, chat, clock, contacts, matrix, picture, storage) --- @@ -220,18 +221,24 @@ Alle Apps nutzen **Mana Core Auth** mit `@manacore/shared-auth`. - Jede App hat eigene Version - Könnte mit `@manacore/shared-ui` Skeletons vereinheitlicht werden -#### Global Error Handler +#### Global Error Handler ✅ -- Nur in Contacts App vollständig implementiert -- Sollte extrahiert werden +> **Status: Erledigt (29.01.2026)** + +- ✅ Zentraler Global Error Handler in `@manacore/shared-ui` +- ✅ Migrierte Apps: calendar, chat, clock, contacts, matrix, picture, storage +- Funktion: `setupGlobalErrorHandler(options?)` +- Behandelt: Unhandled Promise Rejections, JS Errors, Offline/Online Status +- i18n: DE + EN eingebaut, erweiterbar +- Optional: `onAuthError` Callback für Redirect ### Empfehlungen (nach Priorität) #### Hoch -1. **Toast Store & Component vereinheitlichen** - Svelte 5 Runes Standard -2. **AuthGateModal nach shared-auth-ui** verschieben -3. **Global Error Handler** als Composable extrahieren +1. ~~**Toast Store & Component vereinheitlichen**~~ ✅ Erledigt +2. ~~**AuthGateModal nach shared-auth-ui**~~ ✅ Erledigt +3. ~~**Global Error Handler**~~ ✅ Erledigt #### Mittel @@ -254,6 +261,7 @@ Alle Apps nutzen **Mana Core Auth** mit `@manacore/shared-auth`. | ~~API Client Package erstellen~~ | ✅ Erledigt (10 Apps migriert) | | ~~i18n zu 6 Apps hinzufügen~~ | ✅ Erledigt | | ~~AuthGateModal zentralisieren~~ | ✅ Erledigt (4 Apps migriert) | +| ~~Global Error Handler extrahieren~~ | ✅ Erledigt (7 Apps migriert) | ### 🔴 Hohe Priorität @@ -261,10 +269,7 @@ _(Keine offenen Aufgaben mit hoher Priorität)_ ### 🟡 Mittlere Priorität -| Aufgabe | Aufwand | Impact | -|---------|---------|--------| -| ~~AuthGateModal in Shared Package~~ | ~~Niedrig~~ | ✅ Erledigt | -| Global Error Handler extrahieren | Niedrig | Error UX | +_(Keine offenen Aufgaben mit mittlerer Priorität)_ ### 🟢 Niedrige Priorität @@ -280,8 +285,9 @@ _(Keine offenen Aufgaben mit hoher Priorität)_ 1. ~~**API Client Package** als nächstes angehen (höchster Impact)~~ ✅ Erledigt 2. ~~**i18n** zu fehlenden Apps hinzufügen~~ ✅ Erledigt (6 Apps) 3. ~~**AuthGateModal** in Shared Package extrahieren~~ ✅ Erledigt (4 Apps) -4. **Global Error Handler** extrahieren -5. Schrittweise weitere Punkte abarbeiten +4. ~~**Global Error Handler** extrahieren~~ ✅ Erledigt (7 Apps) +5. **App-Skeletons vereinheitlichen** (niedrige Priorität) +6. **Auth Store Pattern dokumentieren** (niedrige Priorität) --- diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index 319814da4..8ba5222d4 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -196,6 +196,18 @@ export type { // Immersive Mode export { default as ImmersiveModeToggle } from './components/ImmersiveModeToggle.svelte'; -// Toast -export { toastStore, toast, handleApiError, ToastContainer } from './toast'; -export type { Toast, ToastType } from './toast'; +// Toast & Global Error Handling +export { + toastStore, + toast, + handleApiError, + ToastContainer, + setupGlobalErrorHandler, + GLOBAL_ERROR_TRANSLATIONS, +} from './toast'; +export type { + Toast, + ToastType, + GlobalErrorHandlerOptions, + GlobalErrorHandlerTranslations, +} from './toast'; diff --git a/packages/shared-ui/src/toast/globalErrorHandler.ts b/packages/shared-ui/src/toast/globalErrorHandler.ts new file mode 100644 index 000000000..af8977ea2 --- /dev/null +++ b/packages/shared-ui/src/toast/globalErrorHandler.ts @@ -0,0 +1,180 @@ +/** + * Global Error Handler - Catches unhandled errors and shows toast notifications + * + * Usage: + * ```ts + * import { setupGlobalErrorHandler } from '@manacore/shared-ui'; + * import { onMount } from 'svelte'; + * + * onMount(() => { + * const cleanup = setupGlobalErrorHandler(); + * return cleanup; // Optional: cleanup on unmount + * }); + * + * // With custom translations: + * setupGlobalErrorHandler({ + * networkError: 'Network error: Server unreachable', + * sessionExpired: 'Session expired. Please log in again.', + * unexpectedError: 'An unexpected error occurred', + * genericError: 'An error occurred', + * offline: 'No internet connection', + * online: 'Connection restored', + * }); + * ``` + */ + +import { toastStore } from './toast.svelte'; + +export interface GlobalErrorHandlerTranslations { + /** Message for network/fetch errors */ + networkError: string; + /** Message for 401/unauthorized errors */ + sessionExpired: string; + /** Fallback message for unhandled promise rejections */ + unexpectedError: string; + /** Message for general JavaScript errors */ + genericError: string; + /** Message when going offline */ + offline: string; + /** Message when connection is restored */ + online: string; +} + +const DEFAULT_TRANSLATIONS_DE: GlobalErrorHandlerTranslations = { + networkError: 'Netzwerkfehler: Server nicht erreichbar', + sessionExpired: 'Sitzung abgelaufen. Bitte erneut anmelden.', + unexpectedError: 'Ein unerwarteter Fehler ist aufgetreten', + genericError: 'Ein Fehler ist aufgetreten', + offline: 'Keine Internetverbindung', + online: 'Verbindung wiederhergestellt', +}; + +const DEFAULT_TRANSLATIONS_EN: GlobalErrorHandlerTranslations = { + networkError: 'Network error: Server unreachable', + sessionExpired: 'Session expired. Please log in again.', + unexpectedError: 'An unexpected error occurred', + genericError: 'An error occurred', + offline: 'No internet connection', + online: 'Connection restored', +}; + +export const GLOBAL_ERROR_TRANSLATIONS = { + de: DEFAULT_TRANSLATIONS_DE, + en: DEFAULT_TRANSLATIONS_EN, +} as const; + +export interface GlobalErrorHandlerOptions { + /** Custom translations (default: German) */ + translations?: Partial; + /** Locale key to use built-in translations (overridden by translations if provided) */ + locale?: 'de' | 'en'; + /** Duration for offline warning toast in ms (default: 10000) */ + offlineDuration?: number; + /** Enable console logging for debugging (default: false) */ + debug?: boolean; + /** Custom handler for auth errors (e.g., redirect to login) */ + onAuthError?: () => void; +} + +/** + * Sets up global error handling for unhandled promise rejections, + * JavaScript errors, and network connectivity changes. + * + * @param options - Configuration options + * @returns Cleanup function to remove event listeners + */ +export function setupGlobalErrorHandler(options: GlobalErrorHandlerOptions = {}): () => void { + // Don't run on server + if (typeof window === 'undefined') { + return () => {}; + } + + const { + translations: customTranslations, + locale = 'de', + offlineDuration = 10000, + debug = false, + onAuthError, + } = options; + + // Merge built-in translations with custom overrides + const baseTranslations = GLOBAL_ERROR_TRANSLATIONS[locale] || DEFAULT_TRANSLATIONS_DE; + const translations: GlobalErrorHandlerTranslations = { + ...baseTranslations, + ...customTranslations, + }; + + const log = (msg: string, ...args: unknown[]) => { + if (debug) { + console.log(`[GlobalErrorHandler] ${msg}`, ...args); + } + }; + + // Handle unhandled promise rejections + const handleUnhandledRejection = (event: PromiseRejectionEvent) => { + const error = event.reason; + log('Unhandled rejection:', error); + + let message = translations.unexpectedError; + + if (error instanceof Error) { + // Network errors + if (error.message === 'Failed to fetch' || error.name === 'TypeError') { + message = translations.networkError; + } + // Auth errors + else if ( + error.message.includes('401') || + error.message.toLowerCase().includes('unauthorized') + ) { + message = translations.sessionExpired; + onAuthError?.(); + } + // Other errors with messages + else if (error.message) { + message = error.message; + } + } + + toastStore.error(message); + event.preventDefault(); + }; + + // Handle general JavaScript errors + const handleError = (event: ErrorEvent) => { + log('Error event:', event); + // Only handle non-script errors (network failures for resources, etc.) + if (event.message && !event.filename) { + toastStore.error(translations.genericError); + } + }; + + // Handle offline status + const handleOffline = () => { + log('Offline'); + toastStore.warning(translations.offline, offlineDuration); + }; + + // Handle online status + const handleOnline = () => { + log('Online'); + toastStore.success(translations.online); + }; + + // Add event listeners + window.addEventListener('unhandledrejection', handleUnhandledRejection); + window.addEventListener('error', handleError); + window.addEventListener('offline', handleOffline); + window.addEventListener('online', handleOnline); + + log('Initialized'); + + // Return cleanup function + return () => { + window.removeEventListener('unhandledrejection', handleUnhandledRejection); + window.removeEventListener('error', handleError); + window.removeEventListener('offline', handleOffline); + window.removeEventListener('online', handleOnline); + log('Cleaned up'); + }; +} diff --git a/packages/shared-ui/src/toast/index.ts b/packages/shared-ui/src/toast/index.ts index 67db8c244..6b262098f 100644 --- a/packages/shared-ui/src/toast/index.ts +++ b/packages/shared-ui/src/toast/index.ts @@ -1,3 +1,8 @@ export { toastStore, toast, handleApiError } from './toast.svelte'; export type { Toast, ToastType } from './toast.svelte'; export { default as ToastContainer } from './ToastContainer.svelte'; +export { setupGlobalErrorHandler, GLOBAL_ERROR_TRANSLATIONS } from './globalErrorHandler'; +export type { + GlobalErrorHandlerOptions, + GlobalErrorHandlerTranslations, +} from './globalErrorHandler';