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

187
app/Views/errors/500.php Normal file
View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 - Internal Server Error</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#4A90E2',
dark: '#357ABD',
}
}
}
}
}
</script>
</head>
<body class="bg-gradient-to-br from-red-50 to-orange-100 min-h-screen flex items-center justify-center p-6">
<div class="max-w-2xl w-full">
<div class="bg-white rounded-2xl shadow-2xl p-12 text-center">
<!-- Error Icon -->
<div class="mb-8">
<i class="fas fa-exclamation-circle text-red-500 text-8xl mb-4"></i>
</div>
<!-- Error Message -->
<h1 class="text-9xl font-bold text-gray-800 mb-4">500</h1>
<h2 class="text-3xl font-bold text-gray-700 mb-4">Internal Server Error</h2>
<p class="text-gray-600 text-lg mb-8 leading-relaxed">
Oops! Something went wrong on our end. We're working to fix the issue.
</p>
<!-- Error Reference ID -->
<?php if (!empty($error_id)): ?>
<div class="bg-blue-50 border border-blue-200 rounded-lg p-5 mb-8">
<div class="flex items-center justify-center space-x-3">
<div class="flex-shrink-0">
<i class="fas fa-fingerprint text-blue-600 text-2xl"></i>
</div>
<div class="text-left">
<p class="text-sm font-medium text-gray-700 mb-1">Error Reference ID:</p>
<div class="flex items-center space-x-2">
<code class="text-lg font-mono font-bold text-primary bg-white px-3 py-1 rounded border border-blue-200">
<?= htmlspecialchars($error_id) ?>
</code>
<button onclick="copyErrorId()"
class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
title="Copy Error ID">
<i class="fas fa-copy"></i>
</button>
</div>
<p class="text-xs text-gray-600 mt-2">
<i class="fas fa-info-circle mr-1"></i>
Please include this ID when reporting the issue
</p>
</div>
</div>
</div>
<?php endif; ?>
<!-- Action Buttons -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/" class="inline-flex items-center justify-center px-8 py-4 bg-primary text-white rounded-lg hover:bg-primary-dark transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
<i class="fas fa-home mr-2"></i>
Go to Dashboard
</a>
<button onclick="history.back()" class="inline-flex items-center justify-center px-8 py-4 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-all duration-200 shadow-md hover:shadow-lg">
<i class="fas fa-arrow-left mr-2"></i>
Go Back
</button>
<button onclick="location.reload()" class="inline-flex items-center justify-center px-8 py-4 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-all duration-200 shadow-md hover:shadow-lg">
<i class="fas fa-redo mr-2"></i>
Try Again
</button>
</div>
<!-- Helpful Links -->
<div class="mt-12 pt-8 border-t border-gray-200">
<p class="text-sm text-gray-500 mb-4">Quick Links:</p>
<div class="flex flex-wrap justify-center gap-4">
<a href="/domains" class="text-primary hover:text-primary-dark transition-colors duration-150">
<i class="fas fa-globe mr-1"></i>
Domains
</a>
<a href="/groups" class="text-primary hover:text-primary-dark transition-colors duration-150">
<i class="fas fa-bell mr-1"></i>
Notification Groups
</a>
<a href="/debug/whois" class="text-primary hover:text-primary-dark transition-colors duration-150">
<i class="fas fa-search mr-1"></i>
WHOIS Lookup
</a>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<a href="/settings" class="text-primary hover:text-primary-dark transition-colors duration-150">
<i class="fas fa-cog mr-1"></i>
Settings
</a>
<?php endif; ?>
</div>
</div>
<!-- Support Info -->
<div class="mt-8 bg-gray-50 rounded-lg p-4">
<p class="text-sm text-gray-600">
<i class="fas fa-life-ring text-primary mr-1"></i>
If this problem persists, please contact your system administrator
<?php if (!empty($error_id)): ?>
and provide the error reference ID above.
<?php else: ?>
.
<?php endif; ?>
</p>
</div>
</div>
<!-- Footer -->
<div class="text-center mt-8">
<p class="text-gray-600">
<i class="fas fa-globe text-primary"></i>
<span class="ml-2">Domain Monitor &copy; <?= date('Y') ?></span>
</p>
</div>
</div>
<script>
function copyErrorId() {
const errorId = '<?= htmlspecialchars($error_id ?? '') ?>';
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(errorId).then(() => {
showSuccess();
}).catch(() => {
fallbackCopy(errorId);
});
} else {
fallbackCopy(errorId);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showSuccess();
} catch (err) {
console.error('Copy failed:', err);
}
document.body.removeChild(textArea);
}
function showSuccess() {
const btn = event.target.closest('button');
if (btn) {
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i>';
btn.classList.add('bg-green-600', 'hover:bg-green-700');
btn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('bg-green-600', 'hover:bg-green-700');
btn.classList.add('bg-blue-600', 'hover:bg-blue-700');
}, 2000);
}
}
</script>
</body>
</html>

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';
?>

View File

@@ -0,0 +1,522 @@
<?php
$title = 'Error Logs';
$pageTitle = 'Error Logs';
$pageDescription = 'Monitor and manage application errors';
$pageIcon = 'fas fa-bug';
ob_start();
// Helper function to generate sort URL
function sortUrl($column, $currentSort, $currentOrder, $filters) {
$newOrder = ($currentSort === $column && $currentOrder === 'asc') ? 'desc' : 'asc';
$params = $filters;
$params['sort'] = $column;
$params['order'] = $newOrder;
return '/errors?' . http_build_query($params);
}
// Helper function for sort icon
function sortIcon($column, $currentSort, $currentOrder) {
if ($currentSort !== $column) {
return '<i class="fas fa-sort text-gray-400 ml-1 text-xs"></i>';
}
$icon = $currentOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down';
return '<i class="fas ' . $icon . ' text-primary ml-1 text-xs"></i>';
}
// Get current filters
$currentFilters = $filters ?? ['resolved' => '', 'type' => '', 'sort' => 'last_occurred_at', 'order' => 'desc'];
?>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<!-- Total Errors Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Total Errors</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['total_errors'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-red-50 rounded-lg flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-red-600 text-lg"></i>
</div>
</div>
</div>
<!-- Unresolved Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Unresolved</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['unresolved'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-orange-50 rounded-lg flex items-center justify-center">
<i class="fas fa-exclamation-circle text-orange-600 text-lg"></i>
</div>
</div>
</div>
<!-- Last 24h Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Last 24h</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['last_24h'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-blue-600 text-lg"></i>
</div>
</div>
</div>
<!-- Total Occurrences Card -->
<div class="bg-white rounded-lg border border-gray-200 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Occurrences</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['total_occurrences'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center">
<i class="fas fa-layer-group text-indigo-600 text-lg"></i>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<form method="GET" action="/errors" id="filter-form">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Status</label>
<select name="resolved" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<option value="">All Errors</option>
<option value="0" <?= $currentFilters['resolved'] === '0' ? 'selected' : '' ?>>Unresolved Only</option>
<option value="1" <?= $currentFilters['resolved'] === '1' ? 'selected' : '' ?>>Resolved Only</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Error Type</label>
<input type="text" name="type" value="<?= htmlspecialchars($currentFilters['type']) ?>" placeholder="e.g., PDOException" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1.5">Sort By</label>
<select name="sort" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
<option value="last_occurred_at" <?= $currentFilters['sort'] === 'last_occurred_at' ? 'selected' : '' ?>>Last Occurred</option>
<option value="occurrences" <?= $currentFilters['sort'] === 'occurrences' ? 'selected' : '' ?>>Most Frequent</option>
<option value="occurred_at" <?= $currentFilters['sort'] === 'occurred_at' ? 'selected' : '' ?>>First Occurred</option>
</select>
</div>
<div class="flex items-end space-x-2">
<button type="submit" class="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-filter mr-2"></i>
Apply
</button>
<a href="/errors" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-times mr-2"></i>
Clear
</a>
</div>
</div>
<input type="hidden" name="order" value="<?= htmlspecialchars($currentFilters['order']) ?>">
</form>
</div>
<!-- Bulk Actions Toolbar (Hidden by default, shown when errors are selected) -->
<div id="bulk-actions" class="hidden mb-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span id="selected-count" class="text-sm font-medium text-blue-900"></span>
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>
Delete Selected
</button>
<button type="button" onclick="clearSelection()" class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-times mr-2"></i>
Clear Selection
</button>
</div>
</div>
</div>
<!-- Pagination Info -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600">
Showing <span class="font-semibold text-gray-900"><?= $pagination['showing_from'] ?></span> to
<span class="font-semibold text-gray-900"><?= $pagination['showing_to'] ?></span> of
<span class="font-semibold text-gray-900"><?= $pagination['total'] ?></span> error(s)
</div>
<form method="GET" action="/errors" class="flex items-center gap-2">
<!-- Preserve filters -->
<input type="hidden" name="resolved" value="<?= htmlspecialchars($currentFilters['resolved']) ?>">
<input type="hidden" name="type" value="<?= htmlspecialchars($currentFilters['type']) ?>">
<input type="hidden" name="sort" value="<?= htmlspecialchars($currentFilters['sort']) ?>">
<input type="hidden" name="order" value="<?= htmlspecialchars($currentFilters['order']) ?>">
<label for="per_page" class="text-sm text-gray-600">Show:</label>
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm">
<option value="10" <?= $pagination['per_page'] == 10 ? 'selected' : '' ?>>10</option>
<option value="25" <?= $pagination['per_page'] == 25 ? 'selected' : '' ?>>25</option>
<option value="50" <?= $pagination['per_page'] == 50 ? 'selected' : '' ?>>50</option>
<option value="100" <?= $pagination['per_page'] == 100 ? 'selected' : '' ?>>100</option>
</select>
</form>
</div>
<!-- Errors List -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<?php if (!empty($errors)): ?>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)" class="rounded border-gray-300 text-primary focus:ring-primary">
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Error
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Location
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Occurrences
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Last Occurred
</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Status
</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($errors as $error): ?>
<?php
$errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['error_type'];
$isResolved = (bool)$error['is_resolved'];
?>
<tr class="hover:bg-gray-50 transition-colors duration-150">
<td class="px-6 py-4">
<input type="checkbox" class="error-checkbox rounded border-gray-300 text-primary focus:ring-primary" value="<?= htmlspecialchars($error['error_id']) ?>" onchange="updateBulkActions()">
</td>
<td class="px-6 py-4">
<div class="flex items-start">
<div class="flex-shrink-0 h-10 w-10 bg-red-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-bug text-red-600"></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-mono font-semibold text-primary"><?= htmlspecialchars($error['error_id']) ?></span>
<button onclick="copyToClipboard('<?= htmlspecialchars($error['error_id']) ?>')" class="text-gray-400 hover:text-primary" title="Copy Error ID">
<i class="fas fa-copy text-xs"></i>
</button>
</div>
<p class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($errorTypeShort) ?></p>
<p class="text-xs text-gray-600 mt-0.5 truncate" style="max-width: 300px;" title="<?= htmlspecialchars($error['error_message']) ?>">
<?= htmlspecialchars($error['error_message']) ?>
</p>
</div>
</div>
</td>
<td class="px-6 py-4">
<div class="text-xs">
<p class="font-mono text-gray-600 truncate" style="max-width: 200px;" title="<?= htmlspecialchars($error['error_file']) ?>">
<?= htmlspecialchars(basename($error['error_file'])) ?>
</p>
<p class="text-gray-500 mt-0.5">
<i class="fas fa-hashtag mr-1"></i>
Line <?= $error['error_line'] ?>
</p>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold <?= $error['occurrences'] >= 10 ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800' ?>">
<i class="fas fa-redo mr-1"></i>
<?= $error['occurrences'] ?>×
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
<?= date('M d, H:i', strtotime($error['last_occurred_at'])) ?>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<?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; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end space-x-2">
<a href="/errors/<?= htmlspecialchars($error['error_id']) ?>" class="text-blue-600 hover:text-blue-800" title="View Details">
<i class="fas fa-eye"></i>
</a>
<?php if (!$isResolved): ?>
<button onclick="markResolved('<?= htmlspecialchars($error['error_id']) ?>')" class="text-green-600 hover:text-green-800" title="Mark as Resolved">
<i class="fas fa-check"></i>
</button>
<?php endif; ?>
<button onclick="deleteError('<?= htmlspecialchars($error['error_id']) ?>')" class="text-red-600 hover:text-red-800" title="Delete Error">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="text-center py-12 px-6">
<div class="mb-4">
<i class="fas fa-check-circle text-green-500 text-6xl"></i>
</div>
<h3 class="text-lg font-semibold text-gray-700 mb-1">No Errors Found</h3>
<p class="text-sm text-gray-500 mb-4">
<?php if (!empty($currentFilters['resolved']) || !empty($currentFilters['type'])): ?>
No errors match your filter criteria.
<?php else: ?>
Great! Your application is running smoothly.
<?php endif; ?>
</p>
</div>
<?php endif; ?>
</div>
<!-- Pagination Controls -->
<?php if ($pagination['total_pages'] > 1): ?>
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-sm text-gray-600">
Page <span class="font-semibold text-gray-900"><?= $pagination['current_page'] ?></span> of
<span class="font-semibold text-gray-900"><?= $pagination['total_pages'] ?></span>
</div>
<div class="flex items-center gap-1">
<?php
$currentPage = $pagination['current_page'];
$totalPages = $pagination['total_pages'];
function paginationUrl($page, $filters, $perPage) {
$params = $filters;
$params['page'] = $page;
$params['per_page'] = $perPage;
return '/errors?' . http_build_query($params);
}
?>
<?php if ($currentPage > 1): ?>
<a href="<?= paginationUrl(1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
<i class="fas fa-angle-double-left"></i>
</a>
<a href="<?= paginationUrl($currentPage - 1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
<i class="fas fa-angle-left"></i> Previous
</a>
<?php endif; ?>
<?php
$range = 2;
$start = max(1, $currentPage - $range);
$end = min($totalPages, $currentPage + $range);
if ($start > 1) {
echo '<a href="' . paginationUrl(1, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">1</a>';
if ($start > 2) echo '<span class="px-2 text-gray-500">...</span>';
}
for ($i = $start; $i <= $end; $i++) {
if ($i == $currentPage) {
echo '<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">' . $i . '</span>';
} else {
echo '<a href="' . paginationUrl($i, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">' . $i . '</a>';
}
}
if ($end < $totalPages) {
if ($end < $totalPages - 1) echo '<span class="px-2 text-gray-500">...</span>';
echo '<a href="' . paginationUrl($totalPages, $currentFilters, $pagination['per_page']) . '" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">' . $totalPages . '</a>';
}
?>
<?php if ($currentPage < $totalPages): ?>
<a href="<?= paginationUrl($currentPage + 1, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
Next <i class="fas fa-angle-right"></i>
</a>
<a href="<?= paginationUrl($totalPages, $currentFilters, $pagination['per_page']) ?>" class="px-3 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
<i class="fas fa-angle-double-right"></i>
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<script>
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(errorId) {
const notes = prompt('Add resolution notes (optional):');
if (notes === null) return; // User cancelled
const form = document.createElement('form');
form.method = 'POST';
form.action = '/errors/' + errorId + '/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(errorId) {
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/' + errorId + '/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();
}
// Checkbox selection functions
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.error-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const checkboxes = document.querySelectorAll('.error-checkbox:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (checkboxes.length > 0) {
bulkActions.classList.remove('hidden');
bulkActions.classList.add('flex');
selectedCount.textContent = checkboxes.length + ' error(s) selected';
} else {
bulkActions.classList.add('hidden');
bulkActions.classList.remove('flex');
}
// Update select all checkbox state
const allCheckboxes = document.querySelectorAll('.error-checkbox');
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
selectAllCheckbox.indeterminate = checkboxes.length > 0 && checkboxes.length < allCheckboxes.length;
}
function getSelectedErrorIds() {
const checkboxes = document.querySelectorAll('.error-checkbox:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
function clearSelection() {
const checkboxes = document.querySelectorAll('.error-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
document.getElementById('select-all').checked = false;
updateBulkActions();
}
function bulkDelete() {
const errorIds = getSelectedErrorIds();
if (errorIds.length === 0) {
alert('Please select at least one error to delete');
return;
}
if (!confirm(`Are you sure you want to delete ${errorIds.length} error(s) and all their occurrences? This action cannot be undone.`)) {
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/errors/bulk-delete';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = '<?= csrf_token() ?>';
form.appendChild(csrfInput);
const idsInput = document.createElement('input');
idsInput.type = 'hidden';
idsInput.name = 'error_ids';
idsInput.value = JSON.stringify(errorIds);
form.appendChild(idsInput);
document.body.appendChild(form);
form.submit();
}
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';
?>

565
app/Views/errors/debug.php Normal file
View File

@@ -0,0 +1,565 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debug Error - <?= htmlspecialchars($error_type ?? 'Application Error') ?></title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#4A90E2',
dark: '#357ABD',
light: '#6BA3E8',
}
}
}
}
}
</script>
<style>
body {
background-color: #f8f9fa;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.4s ease-out;
}
.code-block {
background-color: #1e1e1e;
color: #d4d4d4;
}
.line-number {
color: #858585;
user-select: none;
}
</style>
</head>
<body class="min-h-screen p-6">
<!-- Header -->
<div class="max-w-7xl mx-auto mb-6">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
<div class="flex items-center justify-between flex-wrap gap-4">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
<i class="fas fa-bug text-red-600 text-xl"></i>
</div>
<div>
<h1 class="text-2xl font-bold text-gray-900">Debug Mode</h1>
<p class="text-sm text-gray-600 mt-0.5">
<i class="fas fa-circle text-orange-500 mr-1 text-xs animate-pulse"></i>
Development Environment - Detailed Error Information
</p>
</div>
</div>
<button onclick="copyErrorReport()"
class="inline-flex items-center px-4 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg transition-colors font-medium text-sm">
<i class="fas fa-clipboard mr-2"></i>
Copy Error Report
</button>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto">
<!-- Primary Error Card -->
<div class="bg-white rounded-lg shadow-sm border-l-4 border-red-500 mb-6 animate-fade-in">
<div class="p-6">
<!-- Error Header -->
<div class="flex items-start mb-6">
<div class="flex-shrink-0 w-14 h-14 bg-red-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-exclamation-triangle text-red-600 text-2xl"></i>
</div>
<div class="flex-1">
<h2 class="text-2xl font-bold text-gray-900 mb-2">
<?= htmlspecialchars($error_type ?? 'Error') ?>
</h2>
<p class="text-lg text-gray-700 mb-4"><?= htmlspecialchars($error_message ?? 'An error occurred') ?></p>
<!-- Error Location - Most Critical -->
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-900 mb-3 flex items-center">
<i class="fas fa-map-marker-alt text-red-500 mr-2 text-xs"></i>
Error Location
</h3>
<div class="space-y-2">
<div>
<span class="text-xs font-medium text-gray-600">File:</span>
<code class="block mt-1 bg-white px-3 py-2 rounded text-sm text-gray-800 border border-gray-200 font-mono break-all">
<?= htmlspecialchars($error_file ?? 'Unknown') ?>
</code>
</div>
<div class="flex items-center">
<span class="text-xs font-medium text-gray-600 mr-2">Line:</span>
<span class="font-mono text-red-600 font-bold text-lg"><?= htmlspecialchars($error_line ?? '?') ?></span>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Info Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Error Reference ID -->
<div class="bg-blue-50 rounded-lg border border-blue-200 p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-xs font-semibold text-gray-700 uppercase tracking-wide">Error ID</h4>
<button onclick="copyToClipboard('<?= htmlspecialchars($error_id ?? 'N/A') ?>')"
class="text-primary hover:text-primary-dark" title="Copy Error ID">
<i class="fas fa-copy text-xs"></i>
</button>
</div>
<code class="text-sm font-mono font-bold text-primary"><?= htmlspecialchars($error_id ?? 'N/A') ?></code>
<p class="text-xs text-gray-600 mt-2">
<i class="fas fa-info-circle mr-1"></i>
Use for bug reports
</p>
</div>
<!-- Request Info -->
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4">
<h4 class="text-xs font-semibold text-gray-700 uppercase tracking-wide mb-2">Request</h4>
<div class="space-y-1">
<p class="text-sm">
<span class="font-mono font-bold text-gray-900"><?= htmlspecialchars($request_method ?? 'GET') ?></span>
</p>
<code class="text-xs text-gray-600 font-mono block truncate" title="<?= htmlspecialchars($request_uri ?? '/') ?>">
<?= htmlspecialchars($request_uri ?? '/') ?>
</code>
</div>
</div>
<!-- User Context -->
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4">
<h4 class="text-xs font-semibold text-gray-700 uppercase tracking-wide mb-2">User</h4>
<?php if ($user_info): ?>
<p class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($user_info['username']) ?></p>
<p class="text-xs text-gray-600">
<i class="fas fa-user mr-1"></i>
<?= htmlspecialchars($user_info['role']) ?>
</p>
<?php else: ?>
<p class="text-sm text-gray-500">
<i class="fas fa-user-slash mr-1"></i>
Guest (Not logged in)
</p>
<?php endif; ?>
</div>
<!-- System Info -->
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4">
<h4 class="text-xs font-semibold text-gray-700 uppercase tracking-wide mb-2">System</h4>
<div class="space-y-1">
<p class="text-xs text-gray-600">
<i class="fas fa-code mr-1"></i>
PHP <?= htmlspecialchars($php_version ?? PHP_VERSION) ?>
</p>
<p class="text-xs text-gray-600">
<i class="fas fa-memory mr-1"></i>
<?= round(($memory_usage ?? memory_get_usage(true)) / 1024 / 1024, 2) ?>MB
</p>
<p class="text-xs text-gray-600">
<i class="fas fa-clock mr-1"></i>
<?= date('H:i:s', strtotime($occurred_at ?? 'now')) ?>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Two Column Layout -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Left Column -->
<div class="space-y-6">
<!-- Stack Trace -->
<?php if (!empty($stack_trace)): ?>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden animate-fade-in">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-layer-group text-primary mr-2 text-sm"></i>
Stack Trace
</h3>
<button onclick="copyStackTrace()"
class="text-sm text-primary hover:text-primary-dark font-medium">
<i class="fas fa-copy mr-1"></i>
Copy
</button>
</div>
</div>
<div class="p-6">
<div class="code-block rounded-lg p-4 overflow-x-auto max-h-96 overflow-y-auto border border-gray-700" id="stack-trace">
<?php
$traceLines = explode("\n", $stack_trace);
foreach ($traceLines as $index => $line) {
if (trim($line)) {
echo '<div class="flex font-mono text-sm">';
echo '<span class="line-number mr-4 text-right" style="min-width: 2rem">' . str_pad($index, 2, '0', STR_PAD_LEFT) . '</span>';
echo '<span class="flex-1 text-green-400">' . htmlspecialchars($line) . '</span>';
echo '</div>';
}
}
?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Request Data -->
<?php if (!empty($request_data)): ?>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden animate-fade-in">
<button onclick="toggleSection('request-data')"
class="w-full px-6 py-4 border-b border-gray-200 bg-gray-50 text-left hover:bg-gray-100 transition-colors">
<h3 class="text-lg font-semibold text-gray-900 flex items-center justify-between">
<span class="flex items-center">
<i class="fas fa-paper-plane text-blue-500 mr-2 text-sm"></i>
Request Data
<span class="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded font-medium">
<?= count($request_data) ?>
</span>
</span>
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform" id="request-data-chevron"></i>
</h3>
</button>
<div id="request-data" class="hidden p-6">
<div class="space-y-3">
<?php foreach ($request_data as $key => $value): ?>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200">
<span class="text-xs font-semibold text-gray-700 uppercase tracking-wide block mb-1">
<?= htmlspecialchars($key) ?>
</span>
<code class="text-sm text-gray-800 font-mono block break-all">
<?= htmlspecialchars(is_array($value) ? json_encode($value, JSON_PRETTY_PRINT) : $value) ?>
</code>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Right Column -->
<div class="space-y-6">
<!-- Request Details -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden animate-fade-in">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-globe text-green-500 mr-2 text-sm"></i>
Request Details
</h3>
</div>
<div class="p-6">
<div class="space-y-3 text-sm">
<div class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="font-medium text-gray-600">Method</span>
<span class="font-mono font-bold text-gray-900"><?= htmlspecialchars($request_method ?? 'GET') ?></span>
</div>
<div class="py-2 border-b border-gray-100">
<span class="font-medium text-gray-600 block mb-1">URI</span>
<code class="text-xs text-gray-800 font-mono block break-all bg-gray-50 px-2 py-1 rounded">
<?= htmlspecialchars($request_uri ?? '/') ?>
</code>
</div>
<div class="py-2 border-b border-gray-100">
<span class="font-medium text-gray-600 block mb-1">IP Address</span>
<code class="text-xs text-gray-800 font-mono block bg-gray-50 px-2 py-1 rounded">
<?= htmlspecialchars($ip_address ?? 'Unknown') ?>
</code>
</div>
<div class="py-2">
<span class="font-medium text-gray-600 block mb-1">User Agent</span>
<code class="text-xs text-gray-600 font-mono block break-all bg-gray-50 px-2 py-1 rounded">
<?= htmlspecialchars($user_agent ?? 'Unknown') ?>
</code>
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden animate-fade-in">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
<i class="fas fa-server text-indigo-500 mr-2 text-sm"></i>
System Information
</h3>
</div>
<div class="p-6">
<div class="space-y-3 text-sm">
<div class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="font-medium text-gray-600">PHP Version</span>
<span class="font-mono text-gray-900"><?= htmlspecialchars($php_version ?? PHP_VERSION) ?></span>
</div>
<div class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="font-medium text-gray-600">Memory Usage</span>
<span class="font-mono text-gray-900"><?= round(($memory_usage ?? memory_get_usage(true)) / 1024 / 1024, 2) ?>MB</span>
</div>
<div class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="font-medium text-gray-600">Peak Memory</span>
<span class="font-mono text-gray-900"><?= round(memory_get_peak_usage(true) / 1024 / 1024, 2) ?>MB</span>
</div>
<div class="flex items-center justify-between py-2">
<span class="font-medium text-gray-600">Timestamp</span>
<span class="font-mono text-gray-900 text-xs"><?= date('Y-m-d H:i:s T', strtotime($occurred_at ?? 'now')) ?></span>
</div>
</div>
</div>
</div>
<!-- Session Data -->
<?php if (!empty($session_data)): ?>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden animate-fade-in">
<button onclick="toggleSection('session-data')"
class="w-full px-6 py-4 border-b border-gray-200 bg-gray-50 text-left hover:bg-gray-100 transition-colors">
<h3 class="text-lg font-semibold text-gray-900 flex items-center justify-between">
<span class="flex items-center">
<i class="fas fa-user-lock text-orange-500 mr-2 text-sm"></i>
Session Data
<span class="ml-2 text-xs bg-orange-100 text-orange-800 px-2 py-1 rounded font-medium">
<?= count($session_data) ?>
</span>
</span>
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform" id="session-data-chevron"></i>
</h3>
</button>
<div id="session-data" class="hidden p-6">
<div class="max-h-80 overflow-y-auto">
<table class="min-w-full text-sm">
<thead class="bg-gray-50 sticky top-0">
<tr>
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase">Key</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase">Value</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php foreach ($session_data as $key => $value): ?>
<tr class="hover:bg-gray-50">
<td class="px-3 py-2 font-mono text-gray-700 align-top"><?= htmlspecialchars($key) ?></td>
<td class="px-3 py-2 font-mono text-gray-600 break-all">
<?= htmlspecialchars(is_array($value) ? json_encode($value) : $value) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Help Card -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-5 mt-6 animate-fade-in">
<div class="flex items-start">
<div class="flex-shrink-0">
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
<i class="fas fa-lightbulb text-white"></i>
</div>
</div>
<div class="ml-4">
<h3 class="text-sm font-semibold text-gray-900 mb-2">Debug Mode Active</h3>
<p class="text-sm text-gray-700 mb-3">
This detailed error page is only shown in development mode. In production, users will see a clean error page with just the error ID.
</p>
<div class="flex flex-wrap gap-2">
<a href="/" class="inline-flex items-center px-3 py-2 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-home mr-2"></i>
Go to Dashboard
</a>
<button onclick="location.reload()" class="inline-flex items-center px-3 py-2 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<i class="fas fa-redo mr-2"></i>
Reload Page
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="max-w-7xl mx-auto mt-8">
<div class="text-center text-sm text-gray-500">
<p>
<i class="fas fa-globe text-primary mr-1"></i>
Domain Monitor &copy; <?= date('Y') ?> • Development Mode
</p>
</div>
</div>
<!-- JavaScript -->
<script>
function toggleSection(sectionId) {
const section = document.getElementById(sectionId);
const chevron = document.getElementById(sectionId + '-chevron');
if (section.classList.contains('hidden')) {
section.classList.remove('hidden');
if (chevron) {
chevron.style.transform = 'rotate(180deg)';
}
} else {
section.classList.add('hidden');
if (chevron) {
chevron.style.transform = 'rotate(0deg)';
}
}
}
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showCopySuccess();
}).catch(err => {
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showCopySuccess();
} catch (err) {
console.error('Copy failed:', err);
}
document.body.removeChild(textArea);
}
function copyStackTrace() {
const stackTraceElement = document.getElementById('stack-trace');
const lines = stackTraceElement.querySelectorAll('div');
let stackText = '';
lines.forEach(line => {
const textSpan = line.querySelector('span:last-child');
if (textSpan) {
stackText += textSpan.textContent + '\n';
}
});
copyToClipboard(stackText.trim());
}
function copyErrorReport() {
const errorType = <?= json_encode($error_type ?? 'Error') ?>;
const errorMessage = <?= json_encode($error_message ?? 'Unknown error') ?>;
const errorFile = <?= json_encode($error_file ?? 'Unknown') ?>;
const errorLine = <?= json_encode($error_line ?? '?') ?>;
const errorId = <?= json_encode($error_id ?? 'N/A') ?>;
const phpVersion = <?= json_encode($php_version ?? PHP_VERSION) ?>;
const requestMethod = <?= json_encode($request_method ?? 'GET') ?>;
const requestUri = <?= json_encode($request_uri ?? '/') ?>;
const userAgent = <?= json_encode($user_agent ?? 'Unknown') ?>;
const ipAddress = <?= json_encode($ip_address ?? 'Unknown') ?>;
const timestamp = <?= json_encode(date('Y-m-d H:i:s', strtotime($occurred_at ?? 'now'))) ?>;
const userInfo = <?= json_encode($user_info ?? null) ?>;
const userText = userInfo ? `${userInfo.username} (${userInfo.role}, ID: ${userInfo.id})` : 'Guest (Not logged in)';
// Get stack trace
const stackTraceElement = document.getElementById('stack-trace');
let stackTrace = 'Not available';
if (stackTraceElement) {
const lines = stackTraceElement.querySelectorAll('div');
let stackText = '';
lines.forEach(line => {
const textSpan = line.querySelector('span:last-child');
if (textSpan) {
stackText += textSpan.textContent + '\n';
}
});
stackTrace = stackText.trim();
}
const errorReport = `=== DOMAIN MONITOR ERROR REPORT ===
ERROR INFORMATION:
- Error ID: ${errorId}
- Type: ${errorType}
- Message: ${errorMessage}
LOCATION:
- File: ${errorFile}
- Line: ${errorLine}
REQUEST DETAILS:
- Method: ${requestMethod}
- URI: ${requestUri}
- Timestamp: ${timestamp}
USER CONTEXT:
- User: ${userText}
- IP Address: ${ipAddress}
- User Agent: ${userAgent}
SYSTEM INFORMATION:
- PHP Version: ${phpVersion}
- Memory Usage: ${<?= round(($memory_usage ?? memory_get_usage(true)) / 1024 / 1024, 2) ?>}MB
- Peak Memory: ${<?= round(memory_get_peak_usage(true) / 1024 / 1024, 2) ?>}MB
STACK TRACE:
${stackTrace}
=== END OF ERROR REPORT ===
Reference ID: ${errorId}
Please include this report when reporting bugs.`;
copyToClipboard(errorReport);
}
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 animate-fade-in';
message.innerHTML = '<i class="fas fa-check mr-2"></i>Copied to clipboard!';
document.body.appendChild(message);
setTimeout(() => {
message.style.opacity = '0';
message.style.transform = 'translateY(-20px)';
message.style.transition = 'all 0.3s ease-out';
setTimeout(() => message.remove(), 300);
}, 2000);
}
</script>
</body>
</html>