mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
refactor(admin): drop nav tabs + overview duplication from layout
Now that every /admin/* page is a thin wrapper over a workbench card, the layout's nav tabs are redundant with the workbench's own scene navigation. The heading + tab strip were also duplicating chrome that each card now owns. - Layout shrinks to an auth guard: redirect non-admins, gate-screen if the session is not yet initialized. - /admin/+page.svelte now wraps the existing admin module ListView instead of duplicating its stats/security/quick-links grid. Smoketested: all 11 /admin/* and settings routes respond 200 with clean SSR output; type-check clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7611d109be
commit
5bf3ea8cbd
2 changed files with 50 additions and 194 deletions
|
|
@ -1,83 +1,63 @@
|
|||
<!--
|
||||
/admin/* layout — auth guard only.
|
||||
|
||||
The nav-tabs and dashboard chrome moved into individual workbench
|
||||
cards (admin, admin-users, admin-system, admin-user-data,
|
||||
complexity). Each card owns its own header and renders equally well
|
||||
inside the workbench or under its own /admin/* route wrapper.
|
||||
|
||||
This layout's only remaining job is to keep non-admin users out of
|
||||
the /admin/* URL space. The cards themselves also re-check the role
|
||||
inline because they can be rendered in workbench scenes outside this
|
||||
layout's scope.
|
||||
-->
|
||||
<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';
|
||||
import { ShieldCheck } from '@mana/shared-icons';
|
||||
|
||||
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('/');
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ href: '/admin', label: 'Overview', icon: 'home' },
|
||||
{ href: '/admin/users', label: 'Users', icon: 'users' },
|
||||
{ href: '/admin/user-data', label: 'User Data', icon: 'database' },
|
||||
{ href: '/admin/system', label: 'System', icon: 'server' },
|
||||
{ href: '/admin/complexity', label: 'Complexity', icon: 'chart' },
|
||||
];
|
||||
|
||||
const icons: Record<string, string> = {
|
||||
home: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />`,
|
||||
users: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />`,
|
||||
database: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />`,
|
||||
server: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />`,
|
||||
chart: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3v18h18M7 14l4-4 4 4 5-5" />`,
|
||||
};
|
||||
|
||||
function isActive(href: string, pathname: string): boolean {
|
||||
if (href === '/admin') {
|
||||
return pathname === '/admin';
|
||||
}
|
||||
return pathname.startsWith(href);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#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 class="gate">
|
||||
<div class="icon" aria-hidden="true">🔒</div>
|
||||
<h3>Zugriff verweigert</h3>
|
||||
<p>Du hast keine Admin-Berechtigung.</p>
|
||||
</div>
|
||||
{: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">
|
||||
<ShieldCheck size={20} class="text-red-600 dark:text-red-400" />
|
||||
<span class="text-sm font-medium text-red-600 dark:text-red-400">Admin</span>
|
||||
</div>
|
||||
</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]}
|
||||
</svg>
|
||||
{tab.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.gate {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 1rem;
|
||||
text-align: center;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
.icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.gate h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
margin: 0 0 0.5rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.gate p {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,134 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import StatCard from '$lib/components/admin/StatCard.svelte';
|
||||
import QuickLinks from '$lib/components/admin/QuickLinks.svelte';
|
||||
import { adminService, type AdminStats } from '$lib/api/services/admin';
|
||||
/**
|
||||
* /admin — renders the workbench-card ListView.
|
||||
* Admin-role guard lives in the parent layout and inside the ListView.
|
||||
*/
|
||||
|
||||
let stats = $state<AdminStats | null>(null);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
name: 'Grafana Dashboard',
|
||||
url: 'https://grafana.mana.how',
|
||||
description: 'System & Backend Metrics',
|
||||
icon: 'grafana' as const,
|
||||
},
|
||||
{
|
||||
name: 'Umami Analytics',
|
||||
url: 'https://stats.mana.how',
|
||||
description: 'Web Analytics',
|
||||
icon: 'analytics' as const,
|
||||
},
|
||||
{
|
||||
name: 'Docker Dashboard',
|
||||
url: 'https://grafana.mana.how/d/backends-docker',
|
||||
description: 'Container Metrics',
|
||||
icon: 'docker' as const,
|
||||
},
|
||||
{
|
||||
name: 'System Overview',
|
||||
url: 'https://grafana.mana.how/d/system-overview',
|
||||
description: 'CPU, Memory, Disk',
|
||||
icon: 'grafana' as const,
|
||||
},
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const result = await adminService.getStats();
|
||||
if (result.error) {
|
||||
error = result.error;
|
||||
} else {
|
||||
stats = result.data;
|
||||
}
|
||||
loading = false;
|
||||
});
|
||||
|
||||
let userGrowthPercent = $derived(
|
||||
stats
|
||||
? Math.round((stats.newUsers7d / Math.max(stats.totalUsers - stats.newUsers7d, 1)) * 100)
|
||||
: 0
|
||||
);
|
||||
import ListView from '$lib/modules/admin/ListView.svelte';
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard title="Total Users" value={stats?.totalUsers ?? '-'} icon="users" {loading} />
|
||||
<StatCard
|
||||
title="New Users (7d)"
|
||||
value={stats?.newUsers7d ?? '-'}
|
||||
change={userGrowthPercent}
|
||||
changeLabel="vs previous"
|
||||
icon="users"
|
||||
{loading}
|
||||
/>
|
||||
<StatCard
|
||||
title="Active Sessions"
|
||||
value={stats?.activeSessions ?? '-'}
|
||||
icon="activity"
|
||||
{loading}
|
||||
/>
|
||||
<StatCard
|
||||
title="Unique Users (24h)"
|
||||
value={stats?.uniqueUsers24h ?? '-'}
|
||||
icon="clock"
|
||||
{loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Security & Quick Links -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Security Overview -->
|
||||
<div class="rounded-lg border bg-card p-6 shadow-sm">
|
||||
<h3 class="text-lg font-semibold mb-4">Security (Last 7 Days)</h3>
|
||||
{#if loading}
|
||||
<div class="space-y-4 animate-pulse">
|
||||
<div class="h-4 bg-muted rounded w-full"></div>
|
||||
<div class="h-4 bg-muted rounded w-3/4"></div>
|
||||
</div>
|
||||
{:else if stats}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<span class="text-sm">Successful Logins</span>
|
||||
</div>
|
||||
<span class="font-mono text-sm">{stats.loginSuccess7d}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full bg-red-500"></div>
|
||||
<span class="text-sm">Failed Logins</span>
|
||||
</div>
|
||||
<span class="font-mono text-sm">{stats.loginFailed7d}</span>
|
||||
</div>
|
||||
<div class="pt-2 border-t">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Success Rate</span>
|
||||
<span class="font-medium text-green-600">
|
||||
{stats.loginSuccess7d + stats.loginFailed7d > 0
|
||||
? Math.round(
|
||||
(stats.loginSuccess7d / (stats.loginSuccess7d + stats.loginFailed7d)) * 100
|
||||
)
|
||||
: '—'}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<QuickLinks links={quickLinks} />
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div
|
||||
class="rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20 p-4"
|
||||
>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ListView />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue