Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,321 @@
<?php
/** @var \App\Framework\Core\ViewModel $this */
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Health Dashboard - Admin</title>
<!-- CSS wird automatisch über AssetInjector geladen -->
</head>
<body class="admin-page">
<!-- Admin Header -->
<header class="admin-header">
<div class="admin-header__info">
<h1 class="admin-header__title">🏥 System Health Dashboard</h1>
<p class="admin-header__subtitle">Real-time System Health Monitoring</p>
</div>
<div class="admin-header__actions">
<button class="admin-button admin-button--secondary admin-button--small" id="refreshBtn">
🔄 Refresh All
</button>
</div>
</header>
<!-- Admin Main Content -->
<main class="admin-main">
<!-- Overall Status Card -->
<div class="admin-card status-card status-card--info" id="overallStatus">
<div class="admin-card__content" style="text-align: center;">
<div style="font-size: 4rem; margin-bottom: 1rem;" id="statusIcon">⏳</div>
<div class="metric-card__value" id="statusText" style="font-size: 1.5rem; margin-bottom: 0.5rem;">Loading...</div>
<div class="admin-card__subtitle" id="statusDescription">Checking system health...</div>
</div>
</div>
<!-- Health Checks Grid -->
<div class="admin-cards admin-cards--3-col" id="healthGrid">
<!-- Health cards will be populated here -->
</div>
<!-- Quick Actions -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Quick Actions</h3>
</div>
<div class="admin-card__content">
<div style="display: flex; gap: var(--space-md); flex-wrap: wrap;">
<button class="admin-button" id="runAllBtn">🔍 Run All Checks</button>
<button class="admin-button admin-button--secondary" id="exportBtn">📊 Export Report</button>
<button class="admin-button admin-button--secondary" onclick="window.location.href='../../../../logs'">📄 View Logs</button>
</div>
</div>
</div>
</main>
<script>
class HealthDashboard {
constructor() {
this.refreshButton = document.getElementById('refreshBtn');
this.runAllButton = document.getElementById('runAllBtn');
this.exportButton = document.getElementById('exportBtn');
this.setupEventListeners();
this.loadHealthStatus();
// Auto-refresh every 30 seconds
setInterval(() => this.loadHealthStatus(), 30000);
}
setupEventListeners() {
this.refreshButton.addEventListener('click', () => {
this.loadHealthStatus();
});
this.runAllButton.addEventListener('click', () => {
this.runAllChecks();
});
this.exportButton.addEventListener('click', () => {
this.exportReport();
});
}
async loadHealthStatus() {
try {
this.setLoading(true);
const response = await fetch('/admin/health/api/status');
if (!response.ok) throw new Error('Network error');
const data = await response.json();
if (data.status === 'success') {
this.updateOverallStatus(data.data);
this.updateHealthCards(data.data.checks);
} else {
throw new Error(data.message || 'Unknown error occurred');
}
} catch (error) {
console.error('Health check failed:', error);
this.showError(error.message);
} finally {
this.setLoading(false);
}
}
updateOverallStatus(data) {
const statusIcon = document.getElementById('statusIcon');
const statusText = document.getElementById('statusText');
const statusDescription = document.getElementById('statusDescription');
const overallCard = document.getElementById('overallStatus');
// Remove all status classes
overallCard.classList.remove('status-card--success', 'status-card--warning', 'status-card--error', 'status-card--info');
if (data.overall_status === 'healthy') {
statusIcon.textContent = '✅';
statusText.textContent = 'System Healthy';
statusDescription.textContent = 'All systems are operating normally';
overallCard.classList.add('status-card--success');
} else if (data.overall_status === 'warning') {
statusIcon.textContent = '⚠️';
statusText.textContent = 'System Warning';
statusDescription.textContent = 'Some issues detected that need attention';
overallCard.classList.add('status-card--warning');
} else if (data.overall_status === 'critical') {
statusIcon.textContent = '❌';
statusText.textContent = 'System Critical';
statusDescription.textContent = 'Critical issues detected requiring immediate attention';
overallCard.classList.add('status-card--error');
} else {
statusIcon.textContent = '❓';
statusText.textContent = 'Status Unknown';
statusDescription.textContent = 'Unable to determine system status';
overallCard.classList.add('status-card--info');
}
}
updateHealthCards(checks) {
const healthGrid = document.getElementById('healthGrid');
healthGrid.innerHTML = '';
checks.forEach(check => {
const card = this.createHealthCard(check);
healthGrid.appendChild(card);
});
}
createHealthCard(check) {
const card = document.createElement('div');
card.className = 'admin-card status-card';
// Add status-specific class
if (check.status === 'pass') {
card.classList.add('status-card--success');
} else if (check.status === 'warn') {
card.classList.add('status-card--warning');
} else if (check.status === 'fail') {
card.classList.add('status-card--error');
} else {
card.classList.add('status-card--info');
}
card.innerHTML = `
<div class="admin-card__header">
<h3 class="admin-card__title">${this.getCheckIcon(check.status)} ${check.componentName}</h3>
<span class="admin-table__status admin-table__status--${this.getStatusVariant(check.status)}">
<span class="status-indicator status-indicator--${this.getStatusVariant(check.status)}"></span>
${check.status.toUpperCase()}
</span>
</div>
<div class="admin-card__content">
${check.output ? `<p style="margin-bottom: var(--space-sm);">${check.output}</p>` : ''}
${check.details ? this.renderCheckDetails(check.details) : ''}
${check.time ? `<p class="admin-table__timestamp">Last checked: ${new Date(check.time).toLocaleString()}</p>` : ''}
</div>
`;
return card;
}
getCheckIcon(status) {
switch(status) {
case 'pass': return '✅';
case 'warn': return '⚠️';
case 'fail': return '❌';
default: return '❓';
}
}
getStatusVariant(status) {
switch(status) {
case 'pass': return 'success';
case 'warn': return 'warning';
case 'fail': return 'error';
default: return 'info';
}
}
renderCheckDetails(details) {
if (typeof details === 'object') {
return Object.entries(details).map(([key, value]) =>
`<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
<span style="color: var(--muted); font-size: 0.875rem;">${key}:</span>
<span style="font-weight: 500; font-size: 0.875rem;">${value}</span>
</div>`
).join('');
}
return `<p>${details}</p>`;
}
async runAllChecks() {
try {
this.runAllButton.disabled = true;
this.runAllButton.textContent = '🔄 Running...';
const response = await fetch('/admin/health/api/run-all', {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to run checks');
// Reload status after running checks
await this.loadHealthStatus();
this.showSuccess('All health checks completed successfully');
} catch (error) {
console.error('Failed to run checks:', error);
this.showError('Failed to run health checks: ' + error.message);
} finally {
this.runAllButton.disabled = false;
this.runAllButton.textContent = '🔍 Run All Checks';
}
}
async exportReport() {
try {
this.exportButton.disabled = true;
this.exportButton.textContent = '📤 Exporting...';
const response = await fetch('/admin/health/api/export');
if (!response.ok) throw new Error('Export failed');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `health-report-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
this.showSuccess('Health report exported successfully');
} catch (error) {
console.error('Export failed:', error);
this.showError('Failed to export report: ' + error.message);
} finally {
this.exportButton.disabled = false;
this.exportButton.textContent = '📊 Export Report';
}
}
setLoading(loading) {
if (loading) {
document.getElementById('statusIcon').textContent = '⏳';
document.getElementById('statusText').textContent = 'Loading...';
document.getElementById('statusDescription').textContent = 'Checking system health...';
}
}
showError(message) {
// Create temporary error notification
const errorDiv = document.createElement('div');
errorDiv.className = 'admin-card status-card status-card--error';
errorDiv.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 1000; min-width: 300px;';
errorDiv.innerHTML = `
<div class="admin-card__content">
<strong>Error:</strong> ${message}
<button onclick="this.parentElement.parentElement.remove()" style="float: right; background: none; border: none; color: var(--error); cursor: pointer;">×</button>
</div>
`;
document.body.appendChild(errorDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
if (errorDiv.parentElement) {
errorDiv.parentElement.removeChild(errorDiv);
}
}, 5000);
}
showSuccess(message) {
// Create temporary success notification
const successDiv = document.createElement('div');
successDiv.className = 'admin-card status-card status-card--success';
successDiv.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 1000; min-width: 300px;';
successDiv.innerHTML = `
<div class="admin-card__content">
<strong>Success:</strong> ${message}
<button onclick="this.parentElement.parentElement.remove()" style="float: right; background: none; border: none; color: var(--success); cursor: pointer;">×</button>
</div>
`;
document.body.appendChild(successDiv);
// Auto-remove after 3 seconds
setTimeout(() => {
if (successDiv.parentElement) {
successDiv.parentElement.removeChild(successDiv);
}
}, 3000);
}
}
// Initialize dashboard when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new HealthDashboard();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,496 @@
<?php
/** @var \App\Framework\Core\ViewModel $this */
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Health Dashboard - Admin</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f7;
color: #1d1d1f;
line-height: 1.6;
}
.header {
background: #fff;
border-bottom: 1px solid #e5e5e7;
padding: 1rem 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 2rem;
font-weight: 700;
color: #1d1d1f;
}
.header .subtitle {
color: #86868b;
margin-top: 0.5rem;
}
.dashboard {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.overall-status {
background: #fff;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.07);
border: 1px solid #e5e5e7;
text-align: center;
}
.status-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.status-text {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.status-description {
color: #86868b;
font-size: 1rem;
}
.health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.health-card {
background: #fff;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.07);
border: 1px solid #e5e5e7;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.health-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0,0,0,0.1);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.card-icon {
font-size: 2rem;
margin-right: 1rem;
}
.card-title {
font-size: 1.2rem;
font-weight: 600;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 1rem;
}
.status-healthy {
background: #d1f2db;
color: #0f5132;
}
.status-warning {
background: #fff3cd;
color: #664d03;
}
.status-unhealthy {
background: #f8d7da;
color: #721c24;
}
.check-details {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #f2f2f7;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.detail-label {
color: #86868b;
}
.detail-value {
font-weight: 500;
}
.actions {
background: #fff;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.07);
border: 1px solid #e5e5e7;
}
.action-button {
background: #007aff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
margin-right: 1rem;
}
.action-button:hover {
background: #0051d0;
}
.action-button.secondary {
background: #f2f2f7;
color: #1d1d1f;
}
.action-button.secondary:hover {
background: #e5e5e7;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.error-message {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.header {
padding: 1rem;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.dashboard {
padding: 1rem;
}
.health-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
</style>
</head>
<body>
<div class="header">
<div>
<h1>🏥 System Health Dashboard</h1>
<p class="subtitle">Real-time System Health Monitoring</p>
</div>
<div>
<button class="action-button" id="refreshBtn">
🔄 Refresh All
</button>
</div>
</div>
<div class="dashboard" id="dashboard">
<!-- Overall Status -->
<div class="overall-status" id="overallStatus">
<div class="status-icon" id="statusIcon">⏳</div>
<div class="status-text" id="statusText">Loading...</div>
<div class="status-description" id="statusDescription">Checking system health...</div>
</div>
<!-- Health Checks Grid -->
<div class="health-grid" id="healthGrid">
<!-- Health cards will be populated here -->
</div>
<!-- Actions -->
<div class="actions">
<h3>Quick Actions</h3>
<button class="action-button" id="runAllBtn">🔍 Run All Checks</button>
<button class="action-button secondary" id="exportBtn">📊 Export Report</button>
<button class="action-button secondary" onclick="window.location.href='../../../../logs'">📄 View Logs</button>
</div>
</div>
<script>
class HealthDashboard {
constructor() {
this.refreshButton = document.getElementById('refreshBtn');
this.runAllButton = document.getElementById('runAllBtn');
this.exportButton = document.getElementById('exportBtn');
this.setupEventListeners();
this.loadHealthStatus();
// Auto-refresh every 30 seconds
setInterval(() => this.loadHealthStatus(), 30000);
}
setupEventListeners() {
this.refreshButton.addEventListener('click', () => {
this.loadHealthStatus();
});
this.runAllButton.addEventListener('click', () => {
this.runAllChecks();
});
this.exportButton.addEventListener('click', () => {
this.exportReport();
});
}
async loadHealthStatus() {
try {
this.setLoading(true);
const response = await fetch('/admin/health/api/status');
if (!response.ok) throw new Error('Network error');
const data = await response.json();
if (data.status === 'success') {
this.updateOverallStatus(data.data);
this.updateHealthCards(data.data.checks);
}
} catch (error) {
console.error('Error loading health status:', error);
this.showError('Failed to load health status');
} finally {
this.setLoading(false);
}
}
async runAllChecks() {
try {
this.setLoading(true);
const response = await fetch('/admin/health/api/run-all', {
method: 'POST'
});
if (!response.ok) throw new Error('Network error');
const data = await response.json();
if (data.status === 'success') {
this.updateOverallStatus(data.data);
this.updateHealthCards(data.data.checks);
}
} catch (error) {
console.error('Error running health checks:', error);
this.showError('Failed to run health checks');
} finally {
this.setLoading(false);
}
}
updateOverallStatus(data) {
const statusIcon = document.getElementById('statusIcon');
const statusText = document.getElementById('statusText');
const statusDescription = document.getElementById('statusDescription');
statusIcon.textContent = data.overall_icon;
statusIcon.style.color = data.overall_color;
statusText.textContent = data.overall_status.charAt(0).toUpperCase() + data.overall_status.slice(1);
statusDescription.textContent = `${data.summary.healthy_count} healthy, ${data.summary.warning_count} warnings, ${data.summary.unhealthy_count} failed`;
}
updateHealthCards(checks) {
const grid = document.getElementById('healthGrid');
grid.innerHTML = '';
checks.forEach(check => {
const card = this.createHealthCard(check);
grid.appendChild(card);
});
}
createHealthCard(check) {
const result = check.result;
const card = document.createElement('div');
card.className = 'health-card';
const statusClass = this.getStatusClass(result.status);
const responseTime = result.response_time_ms ? `${result.response_time_ms}ms` : 'N/A';
card.innerHTML = `
<div class="card-header">
<div class="card-icon">${this.getCategoryIcon(check.name)}</div>
<div class="card-title">${check.name}</div>
</div>
<div class="status-badge ${statusClass}">
${result.status.charAt(0).toUpperCase() + result.status.slice(1)}
</div>
<p>${result.message}</p>
<div class="check-details">
<div class="detail-item">
<span class="detail-label">Response Time:</span>
<span class="detail-value">${responseTime}</span>
</div>
${this.renderDetails(result.details)}
</div>
`;
return card;
}
renderDetails(details) {
if (!details || Object.keys(details).length === 0) {
return '';
}
return Object.entries(details).map(([key, value]) => `
<div class="detail-item">
<span class="detail-label">${this.formatKey(key)}:</span>
<span class="detail-value">${this.formatValue(value)}</span>
</div>
`).join('');
}
getStatusClass(status) {
switch (status) {
case 'healthy': return 'status-healthy';
case 'warning': return 'status-warning';
case 'unhealthy': return 'status-unhealthy';
default: return 'status-unhealthy';
}
}
getCategoryIcon(name) {
const icons = {
'Database Connection': '🗄️',
'Cache System': '⚡',
'Disk Space': '💾',
'System Resources': '🖥️'
};
return icons[name] || '🔧';
}
formatKey(key) {
return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
formatValue(value) {
if (typeof value === 'number') {
return value.toLocaleString();
}
if (Array.isArray(value)) {
return value.join(', ');
}
if (typeof value === 'object' && value !== null) {
// Handle objects that might be arrays in disguise
if (Object.prototype.toString.call(value) === '[object Array]') {
return value.join(', ');
}
// Check if it's an object with numeric keys (PHP array)
const keys = Object.keys(value);
if (keys.length > 0 && keys.every(key => !isNaN(key))) {
return Object.values(value).join(', ');
}
// For other objects, try to stringify
try {
return JSON.stringify(value);
} catch (e) {
return String(value);
}
}
return String(value);
}
setLoading(loading) {
const dashboard = document.getElementById('dashboard');
if (loading) {
dashboard.classList.add('loading');
this.refreshButton.textContent = '⏳ Loading...';
} else {
dashboard.classList.remove('loading');
this.refreshButton.textContent = '🔄 Refresh All';
}
}
showError(message) {
const grid = document.getElementById('healthGrid');
grid.innerHTML = `
<div class="error-message">
<strong>Error:</strong> ${message}
</div>
`;
}
async exportReport() {
try {
const response = await fetch('/admin/health/api/status');
const data = await response.json();
const report = JSON.stringify(data, null, 2);
const blob = new Blob([report], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `health-report-${new Date().toISOString().slice(0, 19)}.json`;
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error('Export failed:', error);
}
}
}
// Initialize dashboard when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new HealthDashboard();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,747 @@
<?php
/** @var \App\Framework\Core\ViewModel $this */
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log Viewer - Admin</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f7;
color: #1d1d1f;
line-height: 1.6;
}
.header {
background: #fff;
border-bottom: 1px solid #e5e5e7;
padding: 1rem 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 2rem;
font-weight: 700;
color: #1d1d1f;
}
.header .subtitle {
color: #86868b;
margin-top: 0.5rem;
}
.container {
display: flex;
height: calc(100vh - 80px);
}
.sidebar {
width: 300px;
background: #fff;
border-right: 1px solid #e5e5e7;
padding: 1rem;
overflow-y: auto;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
}
.controls {
background: #fff;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.log-list {
list-style: none;
}
.log-item {
padding: 0.75rem;
border-radius: 8px;
cursor: pointer;
margin-bottom: 0.5rem;
border: 1px solid #f2f2f7;
transition: all 0.2s ease;
}
.log-item:hover {
background: #f2f2f7;
border-color: #007aff;
}
.log-item.active {
background: #007aff;
color: white;
border-color: #007aff;
}
.log-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.log-info {
font-size: 0.85rem;
opacity: 0.7;
}
.log-content {
background: #1e1e1e;
color: #f8f8f2;
border-radius: 8px;
padding: 1rem;
height: 100%;
overflow-y: auto;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.4;
}
.log-entry {
margin-bottom: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
border-left: 3px solid transparent;
}
.log-entry.level-debug {
border-left-color: #6c757d;
background: rgba(108, 117, 125, 0.1);
}
.log-entry.level-info {
border-left-color: #17a2b8;
background: rgba(23, 162, 184, 0.1);
}
.log-entry.level-warning {
border-left-color: #ffc107;
background: rgba(255, 193, 7, 0.1);
}
.log-entry.level-error {
border-left-color: #dc3545;
background: rgba(220, 53, 69, 0.1);
}
.log-entry.level-critical {
border-left-color: #800020;
background: rgba(128, 0, 32, 0.2);
}
.log-timestamp {
color: #a6a6a6;
font-size: 0.85rem;
}
.log-level {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: 600;
margin: 0 0.5rem;
}
.log-level.debug { background: #6c757d; color: white; }
.log-level.info { background: #17a2b8; color: white; }
.log-level.warning { background: #ffc107; color: black; }
.log-level.error { background: #dc3545; color: white; }
.log-level.critical { background: #800020; color: white; }
.log-message {
margin-top: 0.25rem;
}
.log-context {
margin-top: 0.25rem;
font-size: 0.8rem;
color: #a6a6a6;
font-style: italic;
}
.form-control {
padding: 0.5rem;
border: 1px solid #e5e5e7;
border-radius: 4px;
font-size: 0.9rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s ease;
}
.btn-primary {
background: #007aff;
color: white;
}
.btn-primary:hover {
background: #0051d0;
}
.btn-secondary {
background: #f2f2f7;
color: #1d1d1f;
}
.btn-secondary:hover {
background: #e5e5e7;
}
.stream-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: #4CAF50;
border-radius: 50%;
margin-right: 0.5rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.stream-status {
background: linear-gradient(135deg, #2a2a2a, #333);
border: 1px solid #4CAF50;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #86868b;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: #86868b;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
height: auto;
}
.sidebar {
width: 100%;
height: auto;
max-height: 200px;
}
.controls {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.log-content {
height: 400px;
}
}
</style>
</head>
<body>
<div class="header">
<div>
<h1>📄 Log Viewer</h1>
<p class="subtitle">Real-time Log Analysis and Monitoring</p>
</div>
<div>
<button class="btn btn-secondary" onclick="window.location.href='/admin/health'">
🏥 Health Dashboard
</button>
</div>
</div>
<div class="container">
<div class="sidebar">
<h3>Available Logs</h3>
<ul class="log-list" id="logList">
<li class="loading">Loading logs...</li>
</ul>
</div>
<div class="main-content">
<div class="controls">
<select class="form-control" id="levelFilter">
<option value="">All Levels</option>
<option value="DEBUG">Debug</option>
<option value="INFO">Info</option>
<option value="WARNING">Warning</option>
<option value="ERROR">Error</option>
<option value="CRITICAL">Critical</option>
</select>
<input type="text" class="form-control" id="searchInput" placeholder="Search logs...">
<input type="number" class="form-control" id="limitInput" value="100" min="10" max="1000" placeholder="Limit">
<button class="btn btn-primary" id="refreshBtn">🔄 Refresh</button>
<button class="btn btn-secondary" id="tailBtn">📡 Tail</button>
<button class="btn btn-secondary" id="streamBtn">📺 Stream Live</button>
<button class="btn btn-secondary" id="searchBtn">🔍 Search All</button>
</div>
<div class="log-content" id="logContent">
<div class="empty-state">
<h3>Select a log file to view</h3>
<p>Choose a log file from the sidebar to start viewing logs</p>
</div>
</div>
</div>
</div>
<script>
class LogViewer {
constructor() {
this.currentLog = null;
this.refreshInterval = null;
this.streamEventSource = null;
this.isStreaming = false;
this.streamEntries = [];
this.setupEventListeners();
this.loadAvailableLogs();
}
setupEventListeners() {
document.getElementById('refreshBtn').addEventListener('click', () => {
if (this.currentLog) {
this.loadLogContent(this.currentLog);
}
});
document.getElementById('tailBtn').addEventListener('click', () => {
if (this.currentLog) {
this.tailLog(this.currentLog);
}
});
document.getElementById('streamBtn').addEventListener('click', () => {
if (this.currentLog) {
this.toggleStream();
}
});
document.getElementById('searchBtn').addEventListener('click', () => {
this.searchAllLogs();
});
document.getElementById('searchInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
if (this.currentLog) {
this.loadLogContent(this.currentLog);
}
}
});
document.getElementById('levelFilter').addEventListener('change', () => {
if (this.currentLog) {
this.loadLogContent(this.currentLog);
}
});
}
async loadAvailableLogs() {
try {
const response = await fetch('/admin/logs/api/list');
const data = await response.json();
if (data.status === 'success') {
this.renderLogList(data.data.logs);
}
} catch (error) {
console.error('Error loading logs:', error);
document.getElementById('logList').innerHTML =
'<li class="log-item">Error loading logs</li>';
}
}
renderLogList(logs) {
const logList = document.getElementById('logList');
logList.innerHTML = '';
Object.values(logs).forEach(log => {
const li = document.createElement('li');
li.className = 'log-item';
li.innerHTML = `
<div class="log-name">${log.name}</div>
<div class="log-info">
${log.size_human} • ${log.modified_human}
${!log.readable ? ' • ⚠️ Not readable' : ''}
</div>
`;
if (log.readable) {
li.addEventListener('click', () => {
this.selectLog(log.name, li);
});
} else {
li.style.opacity = '0.5';
li.style.cursor = 'not-allowed';
}
logList.appendChild(li);
});
}
selectLog(logName, element) {
// Stop any active stream
if (this.isStreaming) {
this.stopStream();
}
// Update active state
document.querySelectorAll('.log-item').forEach(item => {
item.classList.remove('active');
});
element.classList.add('active');
this.currentLog = logName;
this.loadLogContent(logName);
}
async loadLogContent(logName) {
const logContent = document.getElementById('logContent');
logContent.innerHTML = '<div class="loading">Loading log content...</div>';
try {
const params = new URLSearchParams();
const level = document.getElementById('levelFilter').value;
const search = document.getElementById('searchInput').value;
const limit = document.getElementById('limitInput').value;
if (level) params.set('level', level);
if (search) params.set('search', search);
if (limit) params.set('limit', limit);
const response = await fetch(`/admin/logs/api/read/${logName}?${params}`);
const data = await response.json();
if (data.status === 'success') {
this.renderLogEntries(data.data.entries);
} else {
logContent.innerHTML = `<div class="empty-state">Error: ${data.message}</div>`;
}
} catch (error) {
console.error('Error loading log content:', error);
logContent.innerHTML = '<div class="empty-state">Error loading log content</div>';
}
}
async tailLog(logName) {
try {
const lines = document.getElementById('limitInput').value || 50;
const response = await fetch(`/admin/logs/api/tail/${logName}?lines=${lines}`);
const data = await response.json();
if (data.status === 'success') {
this.renderLogEntries(data.data.entries);
}
} catch (error) {
console.error('Error tailing log:', error);
}
}
async searchAllLogs() {
const search = document.getElementById('searchInput').value;
if (!search) {
alert('Please enter a search query');
return;
}
const logContent = document.getElementById('logContent');
logContent.innerHTML = '<div class="loading">Searching all logs...</div>';
try {
const params = new URLSearchParams();
params.set('query', search);
const level = document.getElementById('levelFilter').value;
if (level) params.set('level', level);
const response = await fetch(`/admin/logs/api/search?${params}`);
const data = await response.json();
if (data.status === 'success') {
this.renderSearchResults(data.data);
}
} catch (error) {
console.error('Error searching logs:', error);
logContent.innerHTML = '<div class="empty-state">Error searching logs</div>';
}
}
toggleStream() {
if (this.isStreaming) {
this.stopStream();
} else {
this.startStream();
}
}
startStream() {
if (!this.currentLog || this.isStreaming) return;
const streamBtn = document.getElementById('streamBtn');
streamBtn.textContent = '⏹️ Stop Stream';
streamBtn.classList.remove('btn-secondary');
streamBtn.classList.add('btn-primary');
this.isStreaming = true;
this.streamEntries = [];
const logContent = document.getElementById('logContent');
logContent.innerHTML = `
<div class="stream-status" style="padding: 1rem; border-radius: 4px; margin-bottom: 1rem; color: #4CAF50;">
<span class="stream-indicator"></span><strong>Live Streaming:</strong> ${this.currentLog}
<span style="float: right;" id="streamStats">0 entries</span>
</div>
<div id="streamContent"></div>
`;
const params = new URLSearchParams();
const level = document.getElementById('levelFilter').value;
const search = document.getElementById('searchInput').value;
const batchSize = 10;
if (level) params.set('level', level);
if (search) params.set('search', search);
params.set('batch_size', batchSize);
const url = `/admin/logs/api/stream/${this.currentLog}?${params}`;
this.streamEventSource = new EventSource(url);
this.streamEventSource.addEventListener('log_batch', (event) => {
try {
const data = JSON.parse(event.data);
if (data.status === 'success') {
this.handleStreamBatch(data);
}
} catch (error) {
console.error('Error parsing stream data:', error);
}
});
this.streamEventSource.addEventListener('stream_complete', (event) => {
console.log('Stream completed');
this.updateStreamStatus('Stream completed');
});
this.streamEventSource.addEventListener('error', (event) => {
console.error('Stream error:', event);
this.stopStream();
this.updateStreamStatus('Stream error occurred');
});
this.streamEventSource.onerror = (error) => {
console.error('EventSource error:', error);
this.stopStream();
};
}
stopStream() {
if (!this.isStreaming) return;
const streamBtn = document.getElementById('streamBtn');
streamBtn.textContent = '📺 Stream Live';
streamBtn.classList.remove('btn-primary');
streamBtn.classList.add('btn-secondary');
this.isStreaming = false;
if (this.streamEventSource) {
this.streamEventSource.close();
this.streamEventSource = null;
}
this.updateStreamStatus('Stream stopped');
}
handleStreamBatch(data) {
const streamContent = document.getElementById('streamContent');
const streamStats = document.getElementById('streamStats');
// Füge neue Einträge hinzu
data.batch.forEach(entry => {
this.streamEntries.push(entry);
});
// Update Statistics
if (streamStats) {
streamStats.textContent = `${this.streamEntries.length} entries (Batch ${data.batch_number})`;
}
// Render neue Einträge
const newEntriesHtml = data.batch.map(entry => {
const levelClass = `level-${entry.level.toLowerCase()}`;
return `
<div class="log-entry ${levelClass}">
<div>
<span class="log-timestamp">${entry.timestamp}</span>
<span class="log-level ${entry.level.toLowerCase()}">${entry.level}</span>
</div>
<div class="log-message">${this.escapeHtml(entry.message)}</div>
${entry.context ? `<div class="log-context">${this.escapeHtml(entry.context)}</div>` : ''}
</div>
`;
}).join('');
if (streamContent) {
streamContent.innerHTML += newEntriesHtml;
// Auto-scroll to bottom
const logContent = document.getElementById('logContent');
logContent.scrollTop = logContent.scrollHeight;
}
// Begrenze die Anzahl der angezeigten Einträge (Performance)
if (this.streamEntries.length > 500) {
this.streamEntries = this.streamEntries.slice(-400); // Keep last 400
this.reRenderStreamEntries();
}
}
reRenderStreamEntries() {
const streamContent = document.getElementById('streamContent');
if (streamContent) {
streamContent.innerHTML = this.streamEntries.map(entry => {
const levelClass = `level-${entry.level.toLowerCase()}`;
return `
<div class="log-entry ${levelClass}">
<div>
<span class="log-timestamp">${entry.timestamp}</span>
<span class="log-level ${entry.level.toLowerCase()}">${entry.level}</span>
</div>
<div class="log-message">${this.escapeHtml(entry.message)}</div>
${entry.context ? `<div class="log-context">${this.escapeHtml(entry.context)}</div>` : ''}
</div>
`;
}).join('');
}
}
updateStreamStatus(message) {
const streamStats = document.getElementById('streamStats');
if (streamStats) {
streamStats.textContent = message;
}
}
renderLogEntries(entries) {
const logContent = document.getElementById('logContent');
if (entries.length === 0) {
logContent.innerHTML = '<div class="empty-state">No log entries found</div>';
return;
}
logContent.innerHTML = entries.map(entry => {
const levelClass = `level-${entry.level.toLowerCase()}`;
return `
<div class="log-entry ${levelClass}">
<div>
<span class="log-timestamp">${entry.timestamp}</span>
<span class="log-level ${entry.level.toLowerCase()}">${entry.level}</span>
</div>
<div class="log-message">${this.escapeHtml(entry.message)}</div>
${entry.context ? `<div class="log-context">${this.escapeHtml(entry.context)}</div>` : ''}
</div>
`;
}).join('');
// Scroll to bottom
logContent.scrollTop = logContent.scrollHeight;
}
renderSearchResults(searchData) {
const logContent = document.getElementById('logContent');
if (searchData.total_matches === 0) {
logContent.innerHTML = '<div class="empty-state">No matches found</div>';
return;
}
let html = `
<div style="padding: 1rem; background: #2a2a2a; border-radius: 4px; margin-bottom: 1rem;">
<strong>Search Results:</strong> ${searchData.total_matches} matches for "${searchData.query}"
</div>
`;
Object.entries(searchData.results).forEach(([logName, logData]) => {
if (logData.entries) {
html += `
<div style="padding: 0.5rem; background: #333; margin-bottom: 1rem; border-radius: 4px;">
<strong>${logName}</strong> (${logData.total_entries} matches)
</div>
`;
logData.entries.forEach(entry => {
const levelClass = `level-${entry.level.toLowerCase()}`;
html += `
<div class="log-entry ${levelClass}">
<div>
<span class="log-timestamp">${entry.timestamp}</span>
<span class="log-level ${entry.level.toLowerCase()}">${entry.level}</span>
</div>
<div class="log-message">${this.escapeHtml(entry.message)}</div>
${entry.context ? `<div class="log-context">${this.escapeHtml(entry.context)}</div>` : ''}
</div>
`;
});
}
});
logContent.innerHTML = html;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize log viewer when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new LogViewer();
});
</script>
</body>
</html>

View File

@@ -1,11 +1,46 @@
<layout src="admin-main"/>
<div>Routes Page</div>
<div class="admin-content">
<h1>{{ title }}</h1>
<div class="admin-tools">
<input type="text" id="routeFilter" placeholder="Routen filtern..." class="search-input">
</div>
<p>{{ name }}</p>
<table class="admin-table" id="routesTable">
<thead>
<tr>
<th>Pfad</th>
<th>Methode</th>
<th>Controller</th>
<th>Handler</th>
<th>Name</th>
<th>Middleware</th>
</tr>
</thead>
<tbody>
<for var="route" in="routes">
<tr>
<td>{{ route.path }}</td>
<td><span class="method-badge method-{{ route.method | lower }}">{{ route.method }}</span></td>
<td>{{ route.controller }}</td>
<td>{{ route.handler }}</td>
<td>{{ route.name | default('-') }}</td>
<td>{{ route.middleware | join(', ') | default('-') }}</td>
</tr>
</for>
</tbody>
</table>
</div>
<ul>
<for var="route" in="routes">
<li><a href="{{ route.path }}">{{ route.path }} </a> <i> ({{ route.class }}) {{ route.attributes.0 }} </i> </li>
</for>
</ul>
<script>
document.getElementById('routeFilter').addEventListener('input', function() {
const filterValue = this.value.toLowerCase();
const rows = document.querySelectorAll('#routesTable tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(filterValue) ? '' : 'none';
});
});
</script>