Add error log management and bulk admin actions

Introduces error log tracking with new ErrorLog model, controller, views, and migration. Adds admin UI for viewing, resolving, and deleting errors. Implements bulk actions for users and notification groups, refactors domain filtering/pagination, and centralizes admin access checks using Auth::requireAdmin().
This commit is contained in:
Hosteroid
2025-10-10 14:01:19 +03:00
parent a29becc944
commit b50377492c
38 changed files with 3726 additions and 428 deletions

View File

@@ -0,0 +1,360 @@
<?php
$title = 'Error Details';
$pageTitle = 'Error Details';
$pageDescription = 'Detailed information about this error';
$pageIcon = 'fas fa-bug';
ob_start();
$isResolved = (bool)$error['is_resolved'];
$errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['error_type'];
?>
<!-- Action Buttons -->
<div class="mb-4 flex items-center justify-between">
<a href="/errors" class="text-gray-600 hover:text-primary">
<i class="fas fa-arrow-left mr-2"></i>
Back to Error Logs
</a>
<div class="flex items-center space-x-2">
<?php if ($isResolved): ?>
<form method="POST" action="/errors/<?= htmlspecialchars($error['error_id']) ?>/unresolve" class="inline">
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
<button type="submit" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors text-sm font-medium">
<i class="fas fa-undo mr-2"></i>
Mark as Unresolved
</button>
</form>
<?php else: ?>
<button onclick="markResolved()" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium">
<i class="fas fa-check mr-2"></i>
Mark as Resolved
</button>
<?php endif; ?>
<button onclick="deleteError()" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium">
<i class="fas fa-trash mr-2"></i>
Delete Error
</button>
</div>
</div>
<!-- Error Header Card -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
<div class="flex items-start justify-between mb-4">
<div class="flex items-start">
<div class="flex-shrink-0 h-14 w-14 bg-red-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-bug text-red-600 text-2xl"></i>
</div>
<div>
<div class="flex items-center gap-3 mb-2">
<h2 class="text-2xl font-semibold text-gray-900"><?= htmlspecialchars($errorTypeShort) ?></h2>
<?php if ($isResolved): ?>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200">
<i class="fas fa-check-circle mr-1"></i>
Resolved
</span>
<?php else: ?>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800 border border-orange-200">
<i class="fas fa-exclamation-triangle mr-1"></i>
Unresolved
</span>
<?php endif; ?>
</div>
<p class="text-gray-600 mb-3"><?= htmlspecialchars($error['error_message']) ?></p>
<div class="flex items-center gap-4 text-sm text-gray-500">
<div class="flex items-center">
<i class="fas fa-hashtag mr-1.5"></i>
<span class="font-mono font-semibold text-primary"><?= htmlspecialchars($error['error_id']) ?></span>
<button onclick="copyToClipboard('<?= htmlspecialchars($error['error_id']) ?>')" class="ml-2 text-gray-400 hover:text-primary" title="Copy Error ID">
<i class="fas fa-copy"></i>
</button>
</div>
<div class="flex items-center">
<i class="fas fa-redo mr-1.5"></i>
<span><?= count($errorOccurrences) ?> occurrence<?= count($errorOccurrences) != 1 ? 's' : '' ?></span>
</div>
<div class="flex items-center">
<i class="far fa-clock mr-1.5"></i>
<span>Last: <?= date('M d, Y H:i:s', strtotime($error['occurred_at'])) ?></span>
</div>
</div>
</div>
</div>
</div>
<!-- Location Info -->
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">File</p>
<p class="font-mono text-sm text-gray-900 break-all"><?= htmlspecialchars($error['error_file']) ?></p>
</div>
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Line</p>
<p class="font-mono text-sm text-gray-900"><?= $error['error_line'] ?></p>
</div>
</div>
</div>
</div>
<!-- Resolution Info (if resolved) -->
<?php if ($isResolved && $error['resolved_at']): ?>
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<i class="fas fa-check-circle text-green-600 mt-0.5 mr-3"></i>
<div class="flex-1">
<h3 class="text-sm font-semibold text-green-900 mb-2">Resolved</h3>
<div class="text-sm text-green-800 space-y-1">
<p><strong>Date:</strong> <?= date('M d, Y H:i:s', strtotime($error['resolved_at'])) ?></p>
<?php if ($error['resolution_notes']): ?>
<p><strong>Notes:</strong> <?= htmlspecialchars($error['resolution_notes']) ?></p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- Tabs -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
<div class="border-b border-gray-200">
<nav class="-mb-px flex">
<button onclick="switchTab('stack-trace')" id="tab-stack-trace" class="tab-button active px-6 py-3 text-sm font-medium border-b-2 border-primary text-primary">
<i class="fas fa-layer-group mr-2"></i>
Stack Trace
</button>
<button onclick="switchTab('request')" id="tab-request" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
<i class="fas fa-exchange-alt mr-2"></i>
Request Data
</button>
<button onclick="switchTab('session')" id="tab-session" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
<i class="fas fa-user mr-2"></i>
Session Data
</button>
<button onclick="switchTab('occurrences')" id="tab-occurrences" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
<i class="fas fa-history mr-2"></i>
All Occurrences (<?= count($errorOccurrences) ?>)
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="p-6">
<!-- Stack Trace Tab -->
<div id="content-stack-trace" class="tab-content">
<?php if (!empty($error['stack_trace_array'])): ?>
<div class="space-y-2">
<?php foreach ($error['stack_trace_array'] as $index => $trace): ?>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 hover:border-primary transition-colors">
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center font-semibold text-sm mr-3">
<?= $index ?>
</div>
<div class="flex-1 min-w-0">
<?php if (isset($trace['file'])): ?>
<p class="font-mono text-xs text-gray-600 break-all mb-1">
<?= htmlspecialchars($trace['file']) ?>
<span class="text-primary font-semibold">line <?= $trace['line'] ?? '?' ?></span>
</p>
<?php endif; ?>
<?php if (isset($trace['function'])): ?>
<p class="font-mono text-sm text-gray-900">
<?php if (isset($trace['class'])): ?>
<span class="text-blue-600"><?= htmlspecialchars($trace['class']) ?></span><?= htmlspecialchars($trace['type']) ?>
<?php endif; ?>
<span class="text-indigo-600"><?= htmlspecialchars($trace['function']) ?></span>()
</p>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-gray-500 text-center py-8">No stack trace available</p>
<?php endif; ?>
</div>
<!-- Request Data Tab -->
<div id="content-request" class="tab-content hidden">
<?php if (!empty($error['request_data'])): ?>
<div class="space-y-4">
<div>
<h3 class="text-sm font-semibold text-gray-700 mb-2">Request Info</h3>
<div class="bg-gray-50 rounded-lg p-4 font-mono text-xs">
<p><strong>Method:</strong> <?= htmlspecialchars($error['request_method']) ?></p>
<p><strong>URI:</strong> <?= htmlspecialchars($error['request_uri']) ?></p>
<p><strong>IP:</strong> <?= htmlspecialchars($error['ip_address']) ?></p>
<p><strong>User Agent:</strong> <?= htmlspecialchars($error['user_agent']) ?></p>
</div>
</div>
<?php foreach ($error['request_data'] as $key => $value): ?>
<div>
<h3 class="text-sm font-semibold text-gray-700 mb-2"><?= htmlspecialchars(strtoupper($key)) ?></h3>
<pre class="bg-gray-900 text-green-400 p-4 rounded-lg overflow-x-auto text-xs"><?= htmlspecialchars(json_encode($value, JSON_PRETTY_PRINT)) ?></pre>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-gray-500 text-center py-8">No request data available</p>
<?php endif; ?>
</div>
<!-- Session Data Tab -->
<div id="content-session" class="tab-content hidden">
<?php if (!empty($error['session_data'])): ?>
<pre class="bg-gray-900 text-green-400 p-4 rounded-lg overflow-x-auto text-xs"><?= htmlspecialchars(json_encode($error['session_data'], JSON_PRETTY_PRINT)) ?></pre>
<?php else: ?>
<p class="text-gray-500 text-center py-8">No session data available</p>
<?php endif; ?>
</div>
<!-- Occurrences Tab -->
<div id="content-occurrences" class="tab-content hidden">
<div class="space-y-2">
<?php foreach ($errorOccurrences as $occurrence): ?>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-semibold text-gray-900"><?= date('M d, Y H:i:s', strtotime($occurrence['occurred_at'])) ?></p>
<p class="text-xs text-gray-500 mt-1">
<?= htmlspecialchars($occurrence['request_method']) ?>
<?= htmlspecialchars($occurrence['request_uri']) ?>
from <?= htmlspecialchars($occurrence['ip_address']) ?>
</p>
</div>
<div class="text-xs text-gray-500">
ID: <span class="font-mono"><?= $occurrence['id'] ?></span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">System Information</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">PHP Version</p>
<p class="text-sm text-gray-900"><?= htmlspecialchars($error['php_version']) ?></p>
</div>
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Memory Usage</p>
<p class="text-sm text-gray-900"><?= htmlspecialchars($error['memory_usage']) ?></p>
</div>
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">First Occurred</p>
<p class="text-sm text-gray-900"><?= date('M d, Y H:i:s', strtotime($errorOccurrences[count($errorOccurrences)-1]['occurred_at'])) ?></p>
</div>
</div>
</div>
<script>
function switchTab(tabName) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
// Remove active class from all tabs
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active', 'border-primary', 'text-primary');
button.classList.add('border-transparent', 'text-gray-500');
});
// Show selected tab content
document.getElementById('content-' + tabName).classList.remove('hidden');
// Add active class to selected tab
const activeTab = document.getElementById('tab-' + tabName);
activeTab.classList.add('active', 'border-primary', 'text-primary');
activeTab.classList.remove('border-transparent', 'text-gray-500');
}
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showCopySuccess();
});
} else {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showCopySuccess();
}
}
function showCopySuccess() {
const message = document.createElement('div');
message.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-3 rounded-lg shadow-lg z-50 flex items-center';
message.innerHTML = '<i class="fas fa-check mr-2"></i>Copied to clipboard!';
document.body.appendChild(message);
setTimeout(() => {
message.style.opacity = '0';
message.style.transition = 'opacity 0.3s';
setTimeout(() => message.remove(), 300);
}, 2000);
}
function markResolved() {
const notes = prompt('Add resolution notes (optional):');
if (notes === null) return; // User cancelled
const form = document.createElement('form');
form.method = 'POST';
form.action = '/errors/<?= htmlspecialchars($error['error_id']) ?>/resolve';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = '<?= csrf_token() ?>';
form.appendChild(csrfInput);
if (notes) {
const notesInput = document.createElement('input');
notesInput.type = 'hidden';
notesInput.name = 'notes';
notesInput.value = notes;
form.appendChild(notesInput);
}
document.body.appendChild(form);
form.submit();
}
function deleteError() {
if (!confirm('Are you sure you want to delete this error and all its occurrences? This action cannot be undone.')) {
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/errors/<?= htmlspecialchars($error['error_id']) ?>/delete';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = '<?= csrf_token() ?>';
form.appendChild(csrfInput);
document.body.appendChild(form);
form.submit();
}
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';
?>