feat(ui): logs page, mein konto, remove settings/api-docs from topbar, accent hamburger, api docs in help
This commit is contained in:
@@ -62,7 +62,7 @@ export function TopBar() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="p-2 rounded-md bg-secondary-900 text-secondary-300 hover:bg-secondary-800 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
className="p-2 rounded-md bg-primary-600 text-white hover:bg-primary-700 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||||
aria-label="Seitenleiste ein-/ausklappen"
|
aria-label="Seitenleiste ein-/ausklappen"
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
>
|
>
|
||||||
@@ -149,37 +149,8 @@ export function TopBar() {
|
|||||||
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
{t('topbar.profile')}
|
{t('topbar.profile', 'Mein Konto')}
|
||||||
</button>
|
</button>
|
||||||
{canAccess('settings:read') && (
|
|
||||||
<button
|
|
||||||
onClick={() => { setUserMenuOpen(false); navigate('/settings'); }}
|
|
||||||
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
{t('topbar.settings')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{canAccess('audit:read') && (
|
|
||||||
<button
|
|
||||||
onClick={() => { setUserMenuOpen(false); navigate('/audit-log'); }}
|
|
||||||
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
{t('nav.auditLog')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<a
|
|
||||||
href="/docs"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={() => setUserMenuOpen(false)}
|
|
||||||
className="w-full text-left px-3 py-2 text-sm hover:bg-secondary-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 flex items-center gap-2"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
<Code className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
|
||||||
{t('settings.apiDocs', 'API Dokumentation')}
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full text-left px-3 py-2 text-sm text-danger-600 hover:bg-danger-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-danger-500"
|
className="w-full text-left px-3 py-2 text-sm text-danger-600 hover:bg-danger-50 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-danger-500"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { NavLink, Outlet, useParams } from 'react-router-dom';
|
import { NavLink, Outlet, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
import { ChevronRight, ChevronDown, BookOpen, Users, Calendar, Mail, FolderOpen, Settings, Shield, Zap, Bot, ArrowLeft } from 'lucide-react';
|
import { ChevronRight, ChevronDown, BookOpen, Users, Calendar, Mail, FolderOpen, Settings, Shield, Zap, Bot, ArrowLeft, Code } from 'lucide-react';
|
||||||
|
|
||||||
interface HelpNode {
|
interface HelpNode {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -75,12 +75,10 @@ const HELP_TREE: HelpNode[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Automation & KI',
|
title: 'Entwicklung',
|
||||||
icon: <Bot className="w-4 h-4" />,
|
icon: <Code className="w-4 h-4" />,
|
||||||
children: [
|
children: [
|
||||||
{ title: 'KI-Assistent', path: '/help/ai-assistant' },
|
{ title: 'API Dokumentation', path: '/help/api-docs' },
|
||||||
{ title: 'Workflows', path: '/help/workflows' },
|
|
||||||
{ title: 'Automation', path: '/help/automation' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { NavLink, Outlet } from 'react-router-dom';
|
||||||
|
import { useUIStore } from '@/store/uiStore';
|
||||||
|
import { ChevronRight, ChevronDown, ScrollText, FileText, AlertTriangle, ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
interface LogNode {
|
||||||
|
title: string;
|
||||||
|
path?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children?: LogNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_TREE: LogNode[] = [
|
||||||
|
{
|
||||||
|
title: 'Audit-Log',
|
||||||
|
icon: <ScrollText className="w-4 h-4" />,
|
||||||
|
children: [
|
||||||
|
{ title: 'Übersicht', path: '/logs/overview' },
|
||||||
|
{ title: 'Filter', path: '/logs/filter' },
|
||||||
|
{ title: 'Export', path: '/logs/export' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'System-Logs',
|
||||||
|
icon: <FileText className="w-4 h-4" />,
|
||||||
|
children: [
|
||||||
|
{ title: 'Container', path: '/logs/container' },
|
||||||
|
{ title: 'Worker', path: '/logs/worker' },
|
||||||
|
{ title: 'Datenbank', path: '/logs/database' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Fehler',
|
||||||
|
icon: <AlertTriangle className="w-4 h-4" />,
|
||||||
|
children: [
|
||||||
|
{ title: 'API-Fehler', path: '/logs/api-errors' },
|
||||||
|
{ title: 'Plugin-Fehler', path: '/logs/plugin-errors' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function TreeItem({ node, depth }: { node: LogNode; depth: number }) {
|
||||||
|
const [expanded, setExpanded] = useState(depth < 1);
|
||||||
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
|
const paddingLeft = depth * 16 + 12;
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-secondary-700 hover:bg-secondary-100 transition-colors min-h-touch"
|
||||||
|
style={{ paddingLeft }}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
>
|
||||||
|
{expanded ? <ChevronDown className="w-3.5 h-3.5 flex-shrink-0" /> : <ChevronRight className="w-3.5 h-3.5 flex-shrink-0" />}
|
||||||
|
{node.icon}
|
||||||
|
<span>{node.title}</span>
|
||||||
|
</button>
|
||||||
|
{expanded && (
|
||||||
|
<div>
|
||||||
|
{node.children!.map((child) => (
|
||||||
|
<TreeItem key={child.path || child.title} node={child} depth={depth + 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
to={node.path!}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors min-h-touch ${
|
||||||
|
isActive
|
||||||
|
? 'bg-primary-100 text-primary-700 font-medium'
|
||||||
|
: 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
style={{ paddingLeft: paddingLeft + 20 }}
|
||||||
|
>
|
||||||
|
{node.icon}
|
||||||
|
<span>{node.title}</span>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogsPage() {
|
||||||
|
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full overflow-hidden" data-testid="logs-page">
|
||||||
|
<aside className={`${sidebarOpen ? 'w-64 border-r border-secondary-200' : 'w-0'} flex-shrink-0 min-h-[calc(100vh-4rem)] transition-all duration-200 overflow-hidden`}>
|
||||||
|
<div className="w-64 flex-shrink-0 p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/start'}
|
||||||
|
className="p-1.5 rounded-md text-secondary-400 hover:bg-secondary-100 hover:text-secondary-700 flex items-center justify-center focus:outline-none"
|
||||||
|
aria-label="Zurück zur Startseite"
|
||||||
|
title="Zurück zur Startseite"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<h1 className="text-xl font-bold text-secondary-900">Logs</h1>
|
||||||
|
</div>
|
||||||
|
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Logs Navigation">
|
||||||
|
{LOG_TREE.map((node) => (
|
||||||
|
<TreeItem key={node.title} node={node} depth={0} />
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main className="flex-1 p-6 overflow-y-auto" data-testid="logs-content">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
import { Settings, HelpCircle, LayoutGrid, ArrowRight, Plus, Bot, Zap, ArrowLeft } from 'lucide-react';
|
import { Settings, HelpCircle, LayoutGrid, ArrowRight, Plus, Bot, Zap, ArrowLeft, ScrollText } from 'lucide-react';
|
||||||
|
|
||||||
// Workspace tile definition
|
// Workspace tile definition
|
||||||
interface WorkspaceTile {
|
interface WorkspaceTile {
|
||||||
@@ -42,6 +42,7 @@ export function StartPage() {
|
|||||||
{ id: 'settings', label: 'Einstellungen', icon: <Settings className="w-4 h-4" />, onClick: () => navigate('/settings') },
|
{ id: 'settings', label: 'Einstellungen', icon: <Settings className="w-4 h-4" />, onClick: () => navigate('/settings') },
|
||||||
{ id: 'agents', label: 'Agenten', icon: <Bot className="w-4 h-4" />, onClick: () => navigate('/agents/overview') },
|
{ id: 'agents', label: 'Agenten', icon: <Bot className="w-4 h-4" />, onClick: () => navigate('/agents/overview') },
|
||||||
{ id: 'automation', label: 'Automation', icon: <Zap className="w-4 h-4" />, onClick: () => navigate('/automation/overview') },
|
{ id: 'automation', label: 'Automation', icon: <Zap className="w-4 h-4" />, onClick: () => navigate('/automation/overview') },
|
||||||
|
{ id: 'logs', label: 'Logs', icon: <ScrollText className="w-4 h-4" />, onClick: () => navigate('/logs/overview') },
|
||||||
{ id: 'help', label: 'Hilfe', icon: <HelpCircle className="w-4 h-4" />, onClick: () => navigate('/help/welcome') },
|
{ id: 'help', label: 'Hilfe', icon: <HelpCircle className="w-4 h-4" />, onClick: () => navigate('/help/welcome') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function HelpApiDocsPage() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto prose prose-secondary">
|
||||||
|
<h1 className="text-2xl font-bold text-secondary-900 mb-4">API Dokumentation</h1>
|
||||||
|
<p className="text-secondary-600 mb-4">
|
||||||
|
Die vollständige API-Dokumentation finden Sie unter <a href="/docs" target="_blank" rel="noopener noreferrer" className="text-primary-600 hover:underline">/docs</a> (Swagger UI).
|
||||||
|
</p>
|
||||||
|
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Übersicht</h2>
|
||||||
|
<p className="text-secondary-600 mb-4">
|
||||||
|
LeoCRM bietet eine REST-API mit über 224 Endpoints. Die API verwendet JSON für Request- und Response-Bodies.
|
||||||
|
</p>
|
||||||
|
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Authentifizierung</h2>
|
||||||
|
<p className="text-secondary-600 mb-4">
|
||||||
|
Die API verwendet Session-basierte Authentifizierung mit HttpOnly-Cookies. Nach dem Login über <code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">POST /api/v1/auth/login</code> wird ein Session-Cookie gesetzt.
|
||||||
|
</p>
|
||||||
|
<h2 className="text-lg font-semibold text-secondary-800 mt-6 mb-2">Wichtige Endpoints</h2>
|
||||||
|
<ul className="list-disc list-inside text-secondary-600 space-y-1">
|
||||||
|
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/contacts</code> — Kontakte abrufen</li>
|
||||||
|
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">POST /api/v1/contacts</code> — Kontakt erstellen</li>
|
||||||
|
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/calendar/entries</code> — Kalendereinträge</li>
|
||||||
|
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/mail/accounts</code> — Mail-Konten</li>
|
||||||
|
<li><code className="bg-secondary-100 px-1.5 py-0.5 rounded text-sm">GET /api/v1/plugins/active-manifests</code> — Plugin-Manifeste</li>
|
||||||
|
</ul>
|
||||||
|
<div className="mt-6 p-4 bg-primary-50 rounded-lg border border-primary-200">
|
||||||
|
<p className="text-sm text-primary-700">
|
||||||
|
📖 <strong>Vollständige Doku:</strong> <a href="/docs" target="_blank" rel="noopener noreferrer" className="underline">Swagger UI öffnen</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -83,6 +83,10 @@ const AgentsPlaceholderPage = React.lazy(() => import('@/pages/agents/AgentsPlac
|
|||||||
const AutomationPage = React.lazy(() => import('@/pages/Automation').then(m => ({ default: m.AutomationPage })));
|
const AutomationPage = React.lazy(() => import('@/pages/Automation').then(m => ({ default: m.AutomationPage })));
|
||||||
const AutomationOverviewPage = React.lazy(() => import('@/pages/automation/AutomationOverview').then(m => ({ default: m.AutomationOverviewPage })));
|
const AutomationOverviewPage = React.lazy(() => import('@/pages/automation/AutomationOverview').then(m => ({ default: m.AutomationOverviewPage })));
|
||||||
const AutomationPlaceholderPage = React.lazy(() => import('@/pages/automation/AutomationPlaceholder').then(m => ({ default: m.AutomationPlaceholderPage })));
|
const AutomationPlaceholderPage = React.lazy(() => import('@/pages/automation/AutomationPlaceholder').then(m => ({ default: m.AutomationPlaceholderPage })));
|
||||||
|
const LogsPage = React.lazy(() => import('@/pages/Logs').then(m => ({ default: m.LogsPage })));
|
||||||
|
const LogsOverviewPage = React.lazy(() => import('@/pages/logs/LogsOverview').then(m => ({ default: m.LogsOverviewPage })));
|
||||||
|
const LogsPlaceholderPage = React.lazy(() => import('@/pages/logs/LogsPlaceholder').then(m => ({ default: m.LogsPlaceholderPage })));
|
||||||
|
const HelpApiDocsPage = React.lazy(() => import('@/pages/help/HelpApiDocs').then(m => ({ default: m.HelpApiDocsPage })));
|
||||||
const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage })));
|
const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage })));
|
||||||
|
|
||||||
/** Centered spinner fallback for lazy-loaded routes */
|
/** Centered spinner fallback for lazy-loaded routes */
|
||||||
@@ -161,6 +165,7 @@ const router = createBrowserRouter([
|
|||||||
{ path: 'navigation', element: withSuspense(<HelpNavigationPage />) },
|
{ path: 'navigation', element: withSuspense(<HelpNavigationPage />) },
|
||||||
{ path: 'contacts', element: withSuspense(<HelpContactsPage />) },
|
{ path: 'contacts', element: withSuspense(<HelpContactsPage />) },
|
||||||
{ path: 'mail-setup', element: withSuspense(<HelpMailSetupPage />) },
|
{ path: 'mail-setup', element: withSuspense(<HelpMailSetupPage />) },
|
||||||
|
{ path: 'api-docs', element: withSuspense(<HelpApiDocsPage />) },
|
||||||
{ path: '*', element: withSuspense(<HelpPlaceholderPage />) },
|
{ path: '*', element: withSuspense(<HelpPlaceholderPage />) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -190,6 +195,14 @@ const router = createBrowserRouter([
|
|||||||
{ path: '*', element: withSuspense(<AutomationPlaceholderPage />) },
|
{ path: '*', element: withSuspense(<AutomationPlaceholderPage />) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/logs',
|
||||||
|
element: withSuspense(<LogsPage />),
|
||||||
|
children: [
|
||||||
|
{ path: 'overview', element: withSuspense(<LogsOverviewPage />) },
|
||||||
|
{ path: '*', element: withSuspense(<LogsPlaceholderPage />) },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsPage />)}</PermissionRoute>,
|
element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsPage />)}</PermissionRoute>,
|
||||||
|
|||||||
Reference in New Issue
Block a user