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:
@@ -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>
|
||||
496
src/Application/Admin/templates/health-dashboard.view.php
Normal file
496
src/Application/Admin/templates/health-dashboard.view.php
Normal 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>
|
||||
747
src/Application/Admin/templates/log-viewer.view.php
Normal file
747
src/Application/Admin/templates/log-viewer.view.php
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user