log admin

This commit is contained in:
ValueOn AG 2026-02-18 21:39:45 +01:00
parent 9793360235
commit 32c071911a
5 changed files with 323 additions and 2 deletions

View file

@ -37,7 +37,7 @@ import { DashboardPage } from './pages/Dashboard';
import { SettingsPage } from './pages/Settings';
import { GDPRPage } from './pages/GDPR';
import { FeatureViewPage } from './pages/FeatureView';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage } from './pages/admin';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage } from './pages/admin';
import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
import { BillingDataView, BillingAdmin } from './pages/billing';
@ -187,6 +187,7 @@ function App() {
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
<Route path="billing" element={<BillingAdmin />} />
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
<Route path="logs" element={<AdminLogsPage />} />
</Route>
</Route>

View file

@ -67,6 +67,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
'page.admin.billing': <FaMoneyBillAlt />,
'page.admin.automationEvents': <FaClock />,
'page.admin.automation-events': <FaClock />,
'page.admin.logs': <FaFileAlt />,
// Feature pages - Trustee
'page.feature.trustee.dashboard': <FaChartLine />,

View file

@ -0,0 +1,106 @@
/**
* AdminLogsPage Styles
*
* Log viewer specific styles: monospace container, color coding, controls.
*/
.controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
flex-shrink: 0;
flex-wrap: wrap;
}
.loadGroup {
display: flex;
align-items: center;
gap: 0.5rem;
}
.controlLabel {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
}
.countInput {
width: 100px;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
text-align: center;
}
.countInput:focus {
outline: none;
border-color: var(--primary-color, #f25843);
box-shadow: 0 0 0 3px rgba(242, 88, 67, 0.1);
}
.refreshGroup {
display: flex;
align-items: center;
}
.toggleLabel {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
user-select: none;
}
.toggleLabel input[type="checkbox"] {
width: 1rem;
height: 1rem;
cursor: pointer;
accent-color: var(--primary-color, #f25843);
}
.logContainer {
flex: 1;
min-height: 0;
overflow: auto;
background: var(--bg-tertiary, #1e1e1e);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem 1rem;
font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Consolas', 'Courier New', monospace;
font-size: 0.8125rem;
line-height: 1.5;
}
:global(.dark-theme) .logContainer {
background: #0d1117;
border-color: #30363d;
}
:global(:not(.dark-theme)) .logContainer {
background: #fafafa;
}
.logLine {
white-space: pre-wrap;
word-break: break-all;
padding: 1px 0;
color: var(--text-primary);
}
.logLine:hover {
background: var(--bg-secondary, rgba(255, 255, 255, 0.04));
}
.logDirHint {
font-size: 0.75rem;
color: var(--text-tertiary);
font-family: monospace;
}

View file

@ -0,0 +1,212 @@
/**
* AdminLogsPage
*
* SysAdmin-only page for viewing gateway application logs.
* Color-coded log levels (ERROR, WARNING, INFO, DEBUG).
* Auto-refresh with configurable entry count.
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { FaSync, FaDownload } from 'react-icons/fa';
import api from '../../api';
import styles from './Admin.module.css';
import logStyles from './AdminLogsPage.module.css';
const LOG_LEVEL_COLORS: Record<string, string> = {
ERROR: 'var(--log-error, #e53e3e)',
WARNING: 'var(--log-warning, #d69e2e)',
INFO: 'var(--log-info, #3182ce)',
DEBUG: 'var(--log-debug, #718096)',
};
const AUTO_REFRESH_INTERVAL = 5000;
function _parseLogLevel(line: string): string | null {
const match = line.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} - (\w+) -/);
return match ? match[1] : null;
}
export const AdminLogsPage: React.FC = () => {
const [lines, setLines] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [count, setCount] = useState(200);
const [countInput, setCountInput] = useState('200');
const [autoRefresh, setAutoRefresh] = useState(false);
const [logDir, setLogDir] = useState<string>('');
const logContainerRef = useRef<HTMLDivElement>(null);
const autoScrollRef = useRef(true);
const _fetchLogs = useCallback(async (entryCount?: number) => {
try {
setLoading(true);
setError(null);
const n = entryCount ?? count;
const response = await api.get(`/api/admin/logs?count=${n}`);
setLines(response.data.lines || []);
setLogDir(response.data.logDir || '');
} catch (err: any) {
setError(err.response?.data?.detail || 'Fehler beim Laden der Logs');
} finally {
setLoading(false);
}
}, [count]);
const _handleLoad = () => {
const parsed = parseInt(countInput, 10);
if (isNaN(parsed) || parsed < 1) return;
setCount(parsed);
_fetchLogs(parsed);
};
const _handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') _handleLoad();
};
const _handleDownload = async () => {
try {
const response = await api.get(`/api/admin/logs/download?count=${count}`, {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `gateway_log_${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err: any) {
setError(err.response?.data?.detail || 'Fehler beim Download');
}
};
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(() => _fetchLogs(), AUTO_REFRESH_INTERVAL);
return () => clearInterval(interval);
}, [autoRefresh, _fetchLogs]);
useEffect(() => {
if (autoScrollRef.current && logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
}
}, [lines]);
const _handleScroll = () => {
if (!logContainerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current;
autoScrollRef.current = scrollHeight - scrollTop - clientHeight < 40;
};
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Gateway Logs</h1>
<p className={styles.pageSubtitle}>
{lines.length > 0
? `${lines.length} Einträge`
: 'Keine Logs geladen'}
{logDir && <span className={logStyles.logDirHint}> {logDir}</span>}
</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={_handleDownload}
disabled={lines.length === 0}
title="Log herunterladen"
>
<FaDownload /> Download
</button>
</div>
</div>
<div className={logStyles.controls}>
<div className={logStyles.loadGroup}>
<label className={logStyles.controlLabel}>Letzte</label>
<input
type="number"
className={logStyles.countInput}
value={countInput}
onChange={(e) => setCountInput(e.target.value)}
onKeyDown={_handleKeyDown}
min={1}
max={50000}
/>
<label className={logStyles.controlLabel}>Einträge</label>
<button
className={styles.primaryButton}
onClick={_handleLoad}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Laden
</button>
</div>
<div className={logStyles.refreshGroup}>
<label className={logStyles.toggleLabel}>
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
/>
Auto-Refresh (5s)
</label>
</div>
</div>
{error && (
<div
className={styles.infoBox}
style={{
background: 'var(--warning-bg, #fffbeb)',
borderColor: 'var(--warning-color, #d69e2e)',
}}
>
<span style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }}>!</span>
{error}
</div>
)}
<div
ref={logContainerRef}
className={logStyles.logContainer}
onScroll={_handleScroll}
>
{lines.length === 0 && !loading && (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>📋</div>
<p className={styles.emptyTitle}>Keine Logs geladen</p>
<p className={styles.emptyDescription}>
Gib die gewünschte Anzahl Einträge ein und klicke auf "Laden".
</p>
</div>
)}
{loading && lines.length === 0 && (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Logs werden geladen...</span>
</div>
)}
{lines.map((line, idx) => {
const level = _parseLogLevel(line);
const color = level ? LOG_LEVEL_COLORS[level] : undefined;
return (
<div
key={idx}
className={logStyles.logLine}
style={color ? { color } : undefined}
>
{line}
</div>
);
})}
</div>
</div>
);
};
export default AdminLogsPage;

View file

@ -15,4 +15,5 @@ export { AdminFeatureRolesPage } from './AdminFeatureRolesPage';
export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage';
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
export { AdminLogsPage } from './AdminLogsPage';