fix(manacore): address critical production readiness issues

1. Admin role gate: Nav link only shows for admin role users, admin
   layout redirects non-admins to /home with access denied message
2. Profile update: Replace stubbed setTimeout with real API call to
   profileService.updateProfile(), add empty name validation
3. Error boundaries: Wrap each dashboard widget in svelte:boundary
   with error UI showing widget name, error message, retry button
4. Payment page: Replace alert() with toast notification for
   unfinished payment integration (no more browser alerts)
5. Form validation: Add name validation in profile update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-25 09:25:47 +01:00
parent d2264f5360
commit 23261aab51
5 changed files with 112 additions and 57 deletions

View file

@ -46,7 +46,29 @@
>
{#each items as widget (widget.id)}
<div class={WIDGET_SIZE_CLASSES[widget.size]} animate:flip={{ duration: flipDurationMs }}>
<WidgetContainer {widget} />
<svelte:boundary>
<WidgetContainer {widget} />
{#snippet failed(error, reset)}
<div
class="flex flex-col items-center justify-center rounded-xl border border-red-200 bg-red-50 p-6 text-center dark:border-red-900/30 dark:bg-red-950/20"
>
<div class="mb-2 text-2xl">⚠️</div>
<p class="mb-1 text-sm font-medium text-red-700 dark:text-red-400">
{widget.id} fehlgeschlagen
</p>
<p class="mb-3 text-xs text-red-500 dark:text-red-500/70">
{error?.message || 'Unbekannter Fehler'}
</p>
<button
type="button"
onclick={reset}
class="rounded-md bg-red-100 px-3 py-1 text-xs font-medium text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
>
Erneut versuchen
</button>
</div>
{/snippet}
</svelte:boundary>
</div>
{/each}
</div>

View file

@ -80,8 +80,7 @@
let userEmail = $derived(authStore.user?.email);
// Navigation items for ManaCore
// Admin link is conditionally added based on user role
let baseNavItems: PillNavItem[] = [
const baseNavItems: PillNavItem[] = [
{ href: '/home', label: 'Home', icon: 'home' },
{ href: '/dashboard', label: 'Dashboard', icon: 'grid' },
{ href: '/observatory', label: 'Observatory', icon: 'eye' },
@ -92,12 +91,11 @@
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
// TODO: Check user role from authStore and add admin link if admin
// For now, always show admin link for testing
const navItems: PillNavItem[] = [
...baseNavItems,
{ href: '/admin', label: 'Admin', icon: 'shield' },
];
// Only show admin link if user has admin role
let isAdmin = $derived(authStore.user?.role === 'admin');
let navItems = $derived<PillNavItem[]>(
isAdmin ? [...baseNavItems, { href: '/admin', label: 'Admin', icon: 'shield' }] : baseNavItems
);
// Navigation shortcuts (Ctrl+1-5)
const navRoutes = navItems.map((item) => item.href);

View file

@ -1,9 +1,19 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import type { Snippet } from 'svelte';
import { authStore } from '$lib/stores/auth.svelte';
let { children }: { children: Snippet } = $props();
// Guard: redirect non-admin users
let isAdmin = $derived(authStore.user?.role === 'admin');
$effect(() => {
if (authStore.initialized && !authStore.loading && !isAdmin) {
goto('/home');
}
});
const tabs = [
{ href: '/admin', label: 'Overview', icon: 'home' },
{ href: '/admin/users', label: 'Users', icon: 'users' },
@ -26,49 +36,57 @@
}
</script>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Admin Dashboard</h1>
<p class="text-muted-foreground">System monitoring and management</p>
</div>
<div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-red-100 dark:bg-red-900/30">
<svg
class="h-4 w-4 text-red-600 dark:text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<span class="text-sm font-medium text-red-600 dark:text-red-400">Admin</span>
</div>
{#if !isAdmin}
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="mb-4 text-5xl">🔒</div>
<h3 class="mb-2 text-lg font-medium">Zugriff verweigert</h3>
<p class="text-muted-foreground">Du hast keine Admin-Berechtigung.</p>
</div>
<nav class="flex gap-1 border-b pb-px">
{#each tabs as tab}
{@const active = isActive(tab.href, $page.url.pathname)}
<a
href={tab.href}
class="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-t-lg transition-colors
{active
? 'text-primary border-b-2 border-primary -mb-px bg-primary/5'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{@html icons[tab.icon]}
{:else}
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Admin Dashboard</h1>
<p class="text-muted-foreground">System monitoring and management</p>
</div>
<div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-red-100 dark:bg-red-900/30">
<svg
class="h-4 w-4 text-red-600 dark:text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
{tab.label}
</a>
{/each}
</nav>
<span class="text-sm font-medium text-red-600 dark:text-red-400">Admin</span>
</div>
</div>
<div>
{@render children()}
<nav class="flex gap-1 border-b pb-px">
{#each tabs as tab}
{@const active = isActive(tab.href, $page.url.pathname)}
<a
href={tab.href}
class="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-t-lg transition-colors
{active
? 'text-primary border-b-2 border-primary -mb-px bg-primary/5'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'}"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{@html icons[tab.icon]}
</svg>
{tab.label}
</a>
{/each}
</nav>
<div>
{@render children()}
</div>
</div>
</div>
{/if}

View file

@ -1,14 +1,16 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
let toastMessage = $state<string | null>(null);
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);
alert(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
toastMessage = `Abo "${planId}" ausgewahlt. Bezahlsystem wird in Kurze integriert.`;
setTimeout(() => (toastMessage = null), 4000);
}
function handleBuyPackage(packageId: string) {
console.log('Buy package:', packageId);
alert(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
toastMessage = `Paket "${packageId}" ausgewahlt. Bezahlsystem wird in Kurze integriert.`;
setTimeout(() => (toastMessage = null), 4000);
}
</script>
@ -16,6 +18,15 @@
<title>Mana - ManaCore</title>
</svelte:head>
<!-- Toast notification -->
{#if toastMessage}
<div
class="fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-amber-600 px-5 py-3 text-sm font-medium text-white shadow-lg"
>
{toastMessage}
</div>
{/if}
<div class="mana-page">
<SubscriptionPage
appName="ManaCore"

View file

@ -4,6 +4,7 @@
import { authStore } from '$lib/stores/auth.svelte';
import { creditsService } from '$lib/api/credits';
import type { CreditBalance } from '$lib/api/credits';
import { profileService } from '$lib/api/profile';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { APP_VERSION } from '$lib/version';
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
@ -38,13 +39,18 @@
}
async function handleUpdateProfile() {
const name = `${firstName} ${lastName}`.trim();
if (!name) {
profileError = 'Bitte gib einen Namen ein';
return;
}
savingProfile = true;
profileSuccess = false;
profileError = null;
try {
// TODO: Implement profile update API when available
await new Promise((resolve) => setTimeout(resolve, 500));
await profileService.updateProfile({ name });
profileSuccess = true;
ManaCoreEvents.profileUpdated();
} catch (e) {