212 lines
6.8 KiB
TypeScript
212 lines
6.8 KiB
TypeScript
/**
|
|
* 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';
|
|
|
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
|
|
|
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 { t } = useLanguage();
|
|
|
|
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 || t('Fehler beim Laden der Logs'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [count, t]);
|
|
|
|
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 || t('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}>{t('Gateway-Logs')}</h1>
|
|
<p className={styles.pageSubtitle}>
|
|
{lines.length > 0 ? t('{count} Einträge', { count: lines.length }) : t('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={t('Log herunterladen')}
|
|
>
|
|
<FaDownload /> {t('Download')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={logStyles.controls}>
|
|
<div className={logStyles.loadGroup}>
|
|
<label className={logStyles.controlLabel}>{t('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}>{t('Einträge')}</label>
|
|
<button
|
|
className={styles.primaryButton}
|
|
onClick={_handleLoad}
|
|
disabled={loading}
|
|
>
|
|
<FaSync className={loading ? 'spinning' : ''} /> {t('Laden')}
|
|
</button>
|
|
</div>
|
|
|
|
<div className={logStyles.refreshGroup}>
|
|
<label className={logStyles.toggleLabel}>
|
|
<input
|
|
type="checkbox"
|
|
checked={autoRefresh}
|
|
onChange={(e) => setAutoRefresh(e.target.checked)}
|
|
/>
|
|
{t('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}>{t('Keine Logs geladen')}</p>
|
|
<p className={styles.emptyDescription}>{t('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>{t('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;
|