Switch PHP views to Twig and add 2FA/UI enhancements
Migrate many view templates from raw PHP to Twig and modernize UI/UX for 2FA and settings. Controllers updated to provide avatar data and two-factor info (ProfileController, UserController) and SettingsController now includes timezone lists, notification preset selection, cron path, cached update state and rollback availability. ErrorHandler now attempts to render error pages via a new Core\TwigService with a safe fallback to raw PHP views. TwoFactorService generation silences deprecated warnings during QR code creation. Numerous .php view files were removed and replaced with .twig equivalents (2fa setup/verify/backup-codes and many auth, dashboard, domains, errors, layout, users, tags, tld-registry, etc.), and core/TwigService was added. These changes move the app toward a Twig-based templating system, improve 2FA flows, surface avatar images in lists/profiles, and make error rendering more robust.
This commit is contained in:
@@ -5,10 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Page Not Found</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>
|
||||
@@ -29,19 +26,16 @@
|
||||
<body class="bg-gradient-to-br from-blue-50 to-indigo-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">
|
||||
<!-- 404 Icon -->
|
||||
<div class="mb-8">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500 text-8xl mb-4 animate-pulse"></i>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h1 class="text-9xl font-bold text-gray-800 mb-4">404</h1>
|
||||
<h2 class="text-3xl font-bold text-gray-700 mb-4">Page Not Found</h2>
|
||||
<p class="text-gray-600 text-lg mb-8 leading-relaxed">
|
||||
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
|
||||
</p>
|
||||
|
||||
<!-- 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>
|
||||
@@ -53,7 +47,6 @@
|
||||
</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">
|
||||
@@ -73,11 +66,10 @@
|
||||
</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"><a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="hover:text-blue-600 transition-colors duration-150" title="Visit Domain Monitor on GitHub">Domain Monitor</a> © <?= date('Y') ?></span>
|
||||
<span class="ml-2"><a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="hover:text-blue-600 transition-colors duration-150" title="Visit Domain Monitor on GitHub">Domain Monitor</a> © {{ "now"|date("Y") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,10 +5,7 @@
|
||||
<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>
|
||||
@@ -29,20 +26,17 @@
|
||||
<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)): ?>
|
||||
{% if error_id is defined and 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">
|
||||
@@ -52,7 +46,7 @@
|
||||
<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) ?>
|
||||
{{ error_id }}
|
||||
</code>
|
||||
<button onclick="copyErrorId()"
|
||||
class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||
@@ -67,9 +61,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
{% 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>
|
||||
@@ -85,7 +78,6 @@
|
||||
</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">
|
||||
@@ -101,41 +93,39 @@
|
||||
<i class="fas fa-search mr-1"></i>
|
||||
WHOIS Lookup
|
||||
</a>
|
||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||
{% if auth is defined and auth.isAdmin|default(false) %}
|
||||
<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; ?>
|
||||
{% 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)): ?>
|
||||
{% if error_id is defined and error_id %}
|
||||
and provide the error reference ID above.
|
||||
<?php else: ?>
|
||||
{% else %}
|
||||
.
|
||||
<?php endif; ?>
|
||||
{% 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 © <?= date('Y') ?></span>
|
||||
<span class="ml-2">Domain Monitor © {{ "now"|date("Y") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyErrorId() {
|
||||
const errorId = '<?= htmlspecialchars($error_id ?? '') ?>';
|
||||
const errorId = {{ (error_id|default(''))|json_encode|raw }};
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(errorId).then(() => {
|
||||
@@ -184,4 +174,3 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,572 +0,0 @@
|
||||
<?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'];
|
||||
?>
|
||||
|
||||
<!-- Back Navigation -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<a href="/errors" class="inline-flex items-center px-3 py-2 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Back to Error Logs
|
||||
</a>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<button onclick="copyErrorReport()" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
||||
<i class="fas fa-clipboard mr-2"></i>
|
||||
Copy Error Report
|
||||
</button>
|
||||
<?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><?= $error['occurrences'] ?? 1 ?> occurrence<?= ($error['occurrences'] ?? 1) != 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['last_occurred_at'] ?? $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 (!empty($error['notes'])): ?>
|
||||
<p><strong>Notes:</strong> <?= htmlspecialchars($error['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>
|
||||
Occurrence Details (<?= $error['occurrences'] ?? 1 ?>)
|
||||
</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-4">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-info-circle text-blue-600 mt-0.5 mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-blue-900 mb-1">Error Occurrence Tracking</p>
|
||||
<p class="text-sm text-blue-800">
|
||||
This error has occurred <strong><?= $error['occurrences'] ?? 1 ?> time<?= ($error['occurrences'] ?? 1) != 1 ? 's' : '' ?></strong>.
|
||||
Similar errors are automatically grouped together and the occurrence count is incremented.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-3">Occurrence Information</h3>
|
||||
<div class="space-y-3">
|
||||
<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($error['occurred_at'])) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Last Occurred</p>
|
||||
<p class="text-sm text-gray-900"><?= date('M d, Y H:i:s', strtotime($error['last_occurred_at'] ?? $error['occurred_at'])) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Total Occurrences</p>
|
||||
<p class="text-sm text-gray-900"><?= $error['occurrences'] ?? 1 ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Request Details</p>
|
||||
<p class="text-xs text-gray-600">
|
||||
<?= htmlspecialchars($error['request_method']) ?>
|
||||
<?= htmlspecialchars($error['request_uri']) ?>
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
IP: <?= htmlspecialchars($error['ip_address']) ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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($error['occurred_at'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolution Notes Modal -->
|
||||
<div id="resolutionModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-lg bg-white">
|
||||
<div class="mt-3">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
||||
Mark Error as Resolved
|
||||
</h3>
|
||||
<button onclick="closeResolutionModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="mb-4">
|
||||
<label for="resolutionNotes" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Resolution Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="resolutionNotes"
|
||||
rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
||||
placeholder="Describe how you resolved this error or any relevant notes..."
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Add any details about the fix or resolution for future reference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
onclick="closeResolutionModal()"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick="submitResolution()"
|
||||
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>
|
||||
</div>
|
||||
</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();
|
||||
}).catch(() => {
|
||||
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 copyErrorReport() {
|
||||
const errorType = <?= json_encode($error['error_type'] ?? 'Error') ?>;
|
||||
const errorMessage = <?= json_encode($error['error_message'] ?? 'Unknown error') ?>;
|
||||
const errorFile = <?= json_encode($error['error_file'] ?? 'Unknown') ?>;
|
||||
const errorLine = <?= json_encode($error['error_line'] ?? '?') ?>;
|
||||
const errorId = <?= json_encode($error['error_id'] ?? 'N/A') ?>;
|
||||
const phpVersion = <?= json_encode($error['php_version'] ?? 'Unknown') ?>;
|
||||
const memoryUsage = <?= json_encode($error['memory_usage'] ?? 'Unknown') ?>;
|
||||
const requestMethod = <?= json_encode($error['request_method'] ?? 'GET') ?>;
|
||||
const requestUri = <?= json_encode($error['request_uri'] ?? '/') ?>;
|
||||
const userAgent = <?= json_encode($error['user_agent'] ?? 'Unknown') ?>;
|
||||
const ipAddress = <?= json_encode($error['ip_address'] ?? 'Unknown') ?>;
|
||||
const occurredAt = <?= json_encode(date('Y-m-d H:i:s', strtotime($error['occurred_at']))) ?>;
|
||||
const lastOccurredAt = <?= json_encode(date('Y-m-d H:i:s', strtotime($error['last_occurred_at'] ?? $error['occurred_at']))) ?>;
|
||||
const occurrences = <?= json_encode($error['occurrences'] ?? 1) ?>;
|
||||
const isResolved = <?= json_encode($isResolved) ?>;
|
||||
const requestData = <?= json_encode($error['request_data'] ?? null) ?>;
|
||||
const sessionData = <?= json_encode($error['session_data'] ?? null) ?>;
|
||||
|
||||
// Get stack trace from the rendered elements
|
||||
const traceFrames = document.querySelectorAll('#content-stack-trace .bg-gray-50');
|
||||
let stackTrace = 'Not available';
|
||||
if (traceFrames.length > 0) {
|
||||
let traceLines = [];
|
||||
traceFrames.forEach((frame, i) => {
|
||||
const fileLine = frame.querySelector('.font-mono.text-xs');
|
||||
const funcLine = frame.querySelector('.font-mono.text-sm');
|
||||
let line = '#' + i + ' ';
|
||||
if (fileLine) line += fileLine.textContent.trim().replace(/\s+/g, ' ');
|
||||
if (funcLine) line += ' ' + funcLine.textContent.trim().replace(/\s+/g, '');
|
||||
traceLines.push(line);
|
||||
});
|
||||
stackTrace = traceLines.join('\n');
|
||||
}
|
||||
|
||||
// Format request data sections
|
||||
let requestDataText = 'Not available';
|
||||
if (requestData && typeof requestData === 'object' && Object.keys(requestData).length > 0) {
|
||||
let sections = [];
|
||||
for (const [key, value] of Object.entries(requestData)) {
|
||||
sections.push(` [${key.toUpperCase()}]\n ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`);
|
||||
}
|
||||
requestDataText = sections.join('\n\n');
|
||||
}
|
||||
|
||||
// Format session data
|
||||
let sessionDataText = 'Not available';
|
||||
if (sessionData && typeof sessionData === 'object' && Object.keys(sessionData).length > 0) {
|
||||
sessionDataText = ' ' + JSON.stringify(sessionData, null, 2).split('\n').join('\n ');
|
||||
}
|
||||
|
||||
const errorReport = `=== DOMAIN MONITOR ERROR REPORT ===
|
||||
|
||||
ERROR INFORMATION:
|
||||
- Error ID: ${errorId}
|
||||
- Type: ${errorType}
|
||||
- Message: ${errorMessage}
|
||||
- Status: ${isResolved ? 'Resolved' : 'Unresolved'}
|
||||
- Occurrences: ${occurrences}
|
||||
|
||||
LOCATION:
|
||||
- File: ${errorFile}
|
||||
- Line: ${errorLine}
|
||||
|
||||
REQUEST DETAILS:
|
||||
- Method: ${requestMethod}
|
||||
- URI: ${requestUri}
|
||||
- IP Address: ${ipAddress}
|
||||
- User Agent: ${userAgent}
|
||||
- First Occurred: ${occurredAt}
|
||||
- Last Occurred: ${lastOccurredAt}
|
||||
|
||||
REQUEST DATA:
|
||||
${requestDataText}
|
||||
|
||||
SESSION DATA:
|
||||
${sessionDataText}
|
||||
|
||||
SYSTEM INFORMATION:
|
||||
- PHP Version: ${phpVersion}
|
||||
- Memory Usage: ${memoryUsage}
|
||||
|
||||
STACK TRACE:
|
||||
${stackTrace}
|
||||
|
||||
=== END OF ERROR REPORT ===
|
||||
|
||||
Reference ID: ${errorId}
|
||||
Please include this report when reporting bugs.`;
|
||||
|
||||
copyToClipboard(errorReport);
|
||||
}
|
||||
|
||||
function showCopySuccess() {
|
||||
// Use the existing toast container from messages.php
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast bg-white border-l-4 border-green-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in';
|
||||
toast.innerHTML = `
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Success</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5">Copied to clipboard!</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function markResolved() {
|
||||
document.getElementById('resolutionModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeResolutionModal() {
|
||||
document.getElementById('resolutionModal').classList.add('hidden');
|
||||
document.getElementById('resolutionNotes').value = '';
|
||||
}
|
||||
|
||||
function submitResolution() {
|
||||
const notes = document.getElementById('resolutionNotes').value;
|
||||
|
||||
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';
|
||||
?>
|
||||
|
||||
482
app/Views/errors/admin-detail.twig
Normal file
482
app/Views/errors/admin-detail.twig
Normal file
@@ -0,0 +1,482 @@
|
||||
{% extends 'layout/base.twig' %}
|
||||
|
||||
{% set title = 'Error Details' %}
|
||||
{% set pageTitle = 'Error Details' %}
|
||||
{% set pageDescription = 'Detailed information about this error' %}
|
||||
{% set pageIcon = 'fas fa-bug' %}
|
||||
|
||||
{% set isResolved = error.is_resolved %}
|
||||
{% set errorTypeShort = error.error_type|split('\\')|last %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Back Navigation -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<a href="/errors" class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 text-sm rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Back to Error Logs
|
||||
</a>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<button onclick="copyErrorReport()" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
|
||||
<i class="fas fa-clipboard mr-2"></i>
|
||||
Copy Error Report
|
||||
</button>
|
||||
{% if isResolved %}
|
||||
<form method="POST" action="/errors/{{ error.error_id }}/unresolve" class="inline">
|
||||
{{ csrf_field() }}
|
||||
<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>
|
||||
{% 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>
|
||||
{% 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 dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 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 dark:bg-red-500/10 rounded-lg flex items-center justify-center mr-4">
|
||||
<i class="fas fa-bug text-red-600 dark:text-red-400 text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white">{{ errorTypeShort }}</h2>
|
||||
{% if isResolved %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400 border border-green-200 dark:border-green-500/20">
|
||||
<i class="fas fa-check-circle mr-1"></i>Resolved
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 dark:bg-orange-500/10 text-orange-800 dark:text-orange-400 border border-orange-200 dark:border-orange-500/20">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>Unresolved
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-slate-400 mb-3">{{ error.error_message }}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500 dark:text-slate-400">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-hashtag mr-1.5"></i>
|
||||
<span class="font-mono font-semibold text-primary">{{ error.error_id }}</span>
|
||||
<button onclick="copyToClipboard('{{ error.error_id }}')" class="ml-2 text-gray-400 dark:text-slate-500 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>{{ error.occurrences|default(1) }} occurrence{{ error.occurrences|default(1) != 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-clock mr-1.5"></i>
|
||||
<span>Last: {{ (error.last_occurred_at|default(error.occurred_at))|date("M d, Y H:i:s") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4 border border-gray-200 dark:border-slate-700">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">File</p>
|
||||
<p class="font-mono text-sm text-gray-900 dark:text-white break-all">{{ error.error_file }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">Line</p>
|
||||
<p class="font-mono text-sm text-gray-900 dark:text-white">{{ error.error_line }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if isResolved and error.resolved_at %}
|
||||
<div class="bg-green-50 dark:bg-green-500/10 border border-green-200 dark:border-green-500/20 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-0.5 mr-3"></i>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-green-900 dark:text-green-400 mb-2">Resolved</h3>
|
||||
<div class="text-sm text-green-800 dark:text-green-300 space-y-1">
|
||||
<p><strong>Date:</strong> {{ error.resolved_at|date("M d, Y H:i:s") }}</p>
|
||||
{% if error.notes %}
|
||||
<p><strong>Notes:</strong> {{ error.notes }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||
<div class="border-b border-gray-200 dark:border-slate-700">
|
||||
<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 dark:text-slate-400 hover:text-gray-700 dark:hover:text-slate-300 hover:border-gray-300 dark:hover:border-slate-600">
|
||||
<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 dark:text-slate-400 hover:text-gray-700 dark:hover:text-slate-300 hover:border-gray-300 dark:hover:border-slate-600">
|
||||
<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 dark:text-slate-400 hover:text-gray-700 dark:hover:text-slate-300 hover:border-gray-300 dark:hover:border-slate-600">
|
||||
<i class="fas fa-history mr-2"></i>Occurrence Details ({{ error.occurrences|default(1) }})
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Stack Trace Tab -->
|
||||
<div id="content-stack-trace" class="tab-content">
|
||||
{% if error.stack_trace_array is defined and error.stack_trace_array %}
|
||||
<div class="space-y-2">
|
||||
{% for index, trace in error.stack_trace_array %}
|
||||
<div class="bg-gray-50 dark:bg-slate-900 border border-gray-200 dark:border-slate-700 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">
|
||||
{% if trace.file is defined %}
|
||||
<p class="font-mono text-xs text-gray-600 dark:text-slate-400 break-all mb-1">
|
||||
{{ trace.file }}
|
||||
<span class="text-primary font-semibold">line {{ trace.line|default('?') }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if trace.function is defined %}
|
||||
<p class="font-mono text-sm text-gray-900 dark:text-white">
|
||||
{% if trace.class is defined %}
|
||||
<span class="text-blue-600 dark:text-blue-400">{{ trace.class }}</span>{{ trace.type }}
|
||||
{% endif %}
|
||||
<span class="text-indigo-600 dark:text-indigo-400">{{ trace.function }}</span>()
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500 dark:text-slate-400 text-center py-8">No stack trace available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Request Data Tab -->
|
||||
<div id="content-request" class="tab-content hidden">
|
||||
{% if error.request_data is defined and error.request_data %}
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-slate-300 mb-2">Request Info</h3>
|
||||
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4 font-mono text-xs">
|
||||
<p><strong>Method:</strong> {{ error.request_method }}</p>
|
||||
<p><strong>URI:</strong> {{ error.request_uri }}</p>
|
||||
<p><strong>IP:</strong> {{ error.ip_address }}</p>
|
||||
<p><strong>User Agent:</strong> {{ error.user_agent }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% for key, value in error.request_data %}
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-slate-300 mb-2">{{ key|upper }}</h3>
|
||||
<pre class="bg-gray-900 text-green-400 p-4 rounded-lg overflow-x-auto text-xs">{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500 dark:text-slate-400 text-center py-8">No request data available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Session Data Tab -->
|
||||
<div id="content-session" class="tab-content hidden">
|
||||
{% if error.session_data is defined and error.session_data %}
|
||||
<pre class="bg-gray-900 text-green-400 p-4 rounded-lg overflow-x-auto text-xs">{{ error.session_data|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
|
||||
{% else %}
|
||||
<p class="text-gray-500 dark:text-slate-400 text-center py-8">No session data available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Occurrences Tab -->
|
||||
<div id="content-occurrences" class="tab-content hidden">
|
||||
<div class="space-y-4">
|
||||
<div class="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/20 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-info-circle text-blue-600 dark:text-blue-400 mt-0.5 mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-blue-900 dark:text-blue-400 mb-1">Error Occurrence Tracking</p>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
This error has occurred <strong>{{ error.occurrences|default(1) }} time{{ error.occurrences|default(1) != 1 ? 's' : '' }}</strong>.
|
||||
Similar errors are automatically grouped together and the occurrence count is incremented.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-slate-900 border border-gray-200 dark:border-slate-700 rounded-lg p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">Occurrence Information</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">First Occurred</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white">{{ error.occurred_at|date("M d, Y H:i:s") }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">Last Occurred</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white">{{ (error.last_occurred_at|default(error.occurred_at))|date("M d, Y H:i:s") }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">Total Occurrences</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white">{{ error.occurrences|default(1) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">Request Details</p>
|
||||
<p class="text-xs text-gray-600 dark:text-slate-400">{{ error.request_method }} {{ error.request_uri }}</p>
|
||||
<p class="text-xs text-gray-600 dark:text-slate-400 mt-1">IP: {{ error.ip_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Information -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white 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 dark:text-slate-400 uppercase tracking-wide mb-1">PHP Version</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white">{{ error.php_version }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">Memory Usage</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white">{{ error.memory_usage }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide mb-1">First Occurred</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white">{{ error.occurred_at|date("M d, Y H:i:s") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolution Notes Modal -->
|
||||
<div id="resolutionModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-lg bg-white">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-check-circle text-green-600 mr-2"></i>Mark Error as Resolved
|
||||
</h3>
|
||||
<button onclick="closeResolutionModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="resolutionNotes" class="block text-sm font-medium text-gray-700 mb-2">Resolution Notes (Optional)</label>
|
||||
<textarea id="resolutionNotes" rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
||||
placeholder="Describe how you resolved this error or any relevant notes..."></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">Add any details about the fix or resolution for future reference.</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button onclick="closeResolutionModal()" class="px-4 py-2 bg-gray-200 dark:bg-slate-700 text-gray-800 dark:text-slate-200 rounded-lg hover:bg-gray-300 dark:hover:bg-slate-600 transition-colors text-sm font-medium">Cancel</button>
|
||||
<button onclick="submitResolution()" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-content').forEach(content => { content.classList.add('hidden'); });
|
||||
document.querySelectorAll('.tab-button').forEach(button => {
|
||||
button.classList.remove('active', 'border-primary', 'text-primary');
|
||||
button.classList.add('border-transparent', 'text-gray-500', 'dark:text-slate-400');
|
||||
});
|
||||
document.getElementById('content-' + tabName).classList.remove('hidden');
|
||||
const activeTab = document.getElementById('tab-' + tabName);
|
||||
activeTab.classList.add('active', 'border-primary', 'text-primary');
|
||||
activeTab.classList.remove('border-transparent', 'text-gray-500', 'dark:text-slate-400');
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => { showCopySuccess(); }).catch(() => { 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 copyErrorReport() {
|
||||
const errorType = {{ error.error_type|default('Error')|json_encode|raw }};
|
||||
const errorMessage = {{ error.error_message|default('Unknown error')|json_encode|raw }};
|
||||
const errorFile = {{ error.error_file|default('Unknown')|json_encode|raw }};
|
||||
const errorLine = {{ error.error_line|default('?')|json_encode|raw }};
|
||||
const errorId = {{ error.error_id|default('N/A')|json_encode|raw }};
|
||||
const phpVersion = {{ error.php_version|default('Unknown')|json_encode|raw }};
|
||||
const memoryUsage = {{ error.memory_usage|default('Unknown')|json_encode|raw }};
|
||||
const requestMethod = {{ error.request_method|default('GET')|json_encode|raw }};
|
||||
const requestUri = {{ error.request_uri|default('/')|json_encode|raw }};
|
||||
const userAgent = {{ error.user_agent|default('Unknown')|json_encode|raw }};
|
||||
const ipAddress = {{ error.ip_address|default('Unknown')|json_encode|raw }};
|
||||
const occurredAt = {{ error.occurred_at|date("Y-m-d H:i:s")|json_encode|raw }};
|
||||
const lastOccurredAt = {{ (error.last_occurred_at|default(error.occurred_at))|date("Y-m-d H:i:s")|json_encode|raw }};
|
||||
const occurrences = {{ error.occurrences|default(1)|json_encode|raw }};
|
||||
const isResolved = {{ isResolved|json_encode|raw }};
|
||||
const requestData = {{ error.request_data|default(null)|json_encode|raw }};
|
||||
const sessionData = {{ error.session_data|default(null)|json_encode|raw }};
|
||||
|
||||
const traceFrames = document.querySelectorAll('#content-stack-trace .bg-gray-50');
|
||||
let stackTrace = 'Not available';
|
||||
if (traceFrames.length > 0) {
|
||||
let traceLines = [];
|
||||
traceFrames.forEach((frame, i) => {
|
||||
const fileLine = frame.querySelector('.font-mono.text-xs');
|
||||
const funcLine = frame.querySelector('.font-mono.text-sm');
|
||||
let line = '#' + i + ' ';
|
||||
if (fileLine) line += fileLine.textContent.trim().replace(/\s+/g, ' ');
|
||||
if (funcLine) line += ' ' + funcLine.textContent.trim().replace(/\s+/g, '');
|
||||
traceLines.push(line);
|
||||
});
|
||||
stackTrace = traceLines.join('\n');
|
||||
}
|
||||
|
||||
let requestDataText = 'Not available';
|
||||
if (requestData && typeof requestData === 'object' && Object.keys(requestData).length > 0) {
|
||||
let sections = [];
|
||||
for (const [key, value] of Object.entries(requestData)) {
|
||||
sections.push(` [${key.toUpperCase()}]\n ${JSON.stringify(value, null, 2).split('\n').join('\n ')}`);
|
||||
}
|
||||
requestDataText = sections.join('\n\n');
|
||||
}
|
||||
|
||||
let sessionDataText = 'Not available';
|
||||
if (sessionData && typeof sessionData === 'object' && Object.keys(sessionData).length > 0) {
|
||||
sessionDataText = ' ' + JSON.stringify(sessionData, null, 2).split('\n').join('\n ');
|
||||
}
|
||||
|
||||
const errorReport = `=== DOMAIN MONITOR ERROR REPORT ===
|
||||
|
||||
ERROR INFORMATION:
|
||||
- Error ID: ${errorId}
|
||||
- Type: ${errorType}
|
||||
- Message: ${errorMessage}
|
||||
- Status: ${isResolved ? 'Resolved' : 'Unresolved'}
|
||||
- Occurrences: ${occurrences}
|
||||
|
||||
LOCATION:
|
||||
- File: ${errorFile}
|
||||
- Line: ${errorLine}
|
||||
|
||||
REQUEST DETAILS:
|
||||
- Method: ${requestMethod}
|
||||
- URI: ${requestUri}
|
||||
- IP Address: ${ipAddress}
|
||||
- User Agent: ${userAgent}
|
||||
- First Occurred: ${occurredAt}
|
||||
- Last Occurred: ${lastOccurredAt}
|
||||
|
||||
REQUEST DATA:
|
||||
${requestDataText}
|
||||
|
||||
SESSION DATA:
|
||||
${sessionDataText}
|
||||
|
||||
SYSTEM INFORMATION:
|
||||
- PHP Version: ${phpVersion}
|
||||
- Memory Usage: ${memoryUsage}
|
||||
|
||||
STACK TRACE:
|
||||
${stackTrace}
|
||||
|
||||
=== END OF ERROR REPORT ===
|
||||
|
||||
Reference ID: ${errorId}
|
||||
Please include this report when reporting bugs.`;
|
||||
|
||||
copyToClipboard(errorReport);
|
||||
}
|
||||
|
||||
function showCopySuccess() {
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast bg-white dark:bg-slate-800 border-l-4 border-green-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in';
|
||||
toast.innerHTML = '<div class="flex-shrink-0"><div class="w-8 h-8 bg-green-100 dark:bg-green-500/10 rounded-full flex items-center justify-center"><i class="fas fa-check text-green-600 dark:text-green-400 text-sm"></i></div></div><div class="ml-3 flex-1"><p class="text-sm font-medium text-gray-900 dark:text-white">Success</p><p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">Copied to clipboard!</p></div><button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition-colors"><i class="fas fa-times text-sm"></i></button>';
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => { toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out'; toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; setTimeout(() => toast.remove(), 300); }, 3000);
|
||||
}
|
||||
|
||||
function markResolved() {
|
||||
document.getElementById('resolutionModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeResolutionModal() {
|
||||
document.getElementById('resolutionModal').classList.add('hidden');
|
||||
document.getElementById('resolutionNotes').value = '';
|
||||
}
|
||||
|
||||
function submitResolution() {
|
||||
const notes = document.getElementById('resolutionNotes').value;
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/errors/{{ 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/{{ 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>
|
||||
{% endblock %}
|
||||
@@ -1,605 +0,0 @@
|
||||
<?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"><?= $errorStats['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"><?= $errorStats['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"><?= $errorStats['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"><?= $errorStats['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>
|
||||
|
||||
<!-- 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">
|
||||
<!-- Bulk Actions Bar (shown when errors are selected) -->
|
||||
<div id="bulk-actions" class="hidden px-6 py-3 bg-blue-50 border-b border-blue-200 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="selected-count" class="text-sm font-medium text-gray-700"></span>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium">
|
||||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="clearSelection()" class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 hover:bg-blue-100 px-3 py-1.5 rounded-lg transition-colors">
|
||||
<i class="fas fa-times mr-1.5"></i> Clear Selection
|
||||
</button>
|
||||
</div>
|
||||
<?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; ?>
|
||||
|
||||
<!-- Resolution Notes Modal -->
|
||||
<div id="resolutionModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-lg bg-white">
|
||||
<div class="mt-3">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
||||
Mark Error as Resolved
|
||||
</h3>
|
||||
<button onclick="closeResolutionModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="mb-4">
|
||||
<label for="resolutionNotes" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Resolution Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="resolutionNotes"
|
||||
rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
||||
placeholder="Describe how you resolved this error or any relevant notes..."
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Add any details about the fix or resolution for future reference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
onclick="closeResolutionModal()"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick="submitResolution()"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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() {
|
||||
// Use the existing toast container from messages.php
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast bg-white border-l-4 border-green-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in';
|
||||
toast.innerHTML = `
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Success</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5">Copied to clipboard!</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
let currentErrorId = null;
|
||||
|
||||
function markResolved(errorId) {
|
||||
currentErrorId = errorId;
|
||||
document.getElementById('resolutionModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeResolutionModal() {
|
||||
document.getElementById('resolutionModal').classList.add('hidden');
|
||||
document.getElementById('resolutionNotes').value = '';
|
||||
currentErrorId = null;
|
||||
}
|
||||
|
||||
function submitResolution() {
|
||||
if (!currentErrorId) return;
|
||||
|
||||
const notes = document.getElementById('resolutionNotes').value;
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/errors/' + currentErrorId + '/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');
|
||||
selectedCount.textContent = checkboxes.length + ' error(s) selected';
|
||||
} else {
|
||||
bulkActions.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 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';
|
||||
?>
|
||||
|
||||
481
app/Views/errors/admin-index.twig
Normal file
481
app/Views/errors/admin-index.twig
Normal file
@@ -0,0 +1,481 @@
|
||||
{% extends 'layout/base.twig' %}
|
||||
|
||||
{% set title = 'Error Logs' %}
|
||||
{% set pageTitle = 'Error Logs' %}
|
||||
{% set pageDescription = 'Monitor and manage application errors' %}
|
||||
{% set pageIcon = 'fas fa-bug' %}
|
||||
|
||||
{% set currentFilters = filters|default({resolved: '', type: '', sort: 'last_occurred_at', order: 'desc'}) %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 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 dark:text-slate-400 uppercase tracking-wide">Total Errors</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ errorStats.total_errors|default(0) }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-red-50 dark:bg-red-500/10 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-red-600 dark:text-red-400 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 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 dark:text-slate-400 uppercase tracking-wide">Unresolved</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ errorStats.unresolved|default(0) }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-50 dark:bg-orange-500/10 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-circle text-orange-600 dark:text-orange-400 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 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 dark:text-slate-400 uppercase tracking-wide">Last 24h</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ errorStats.last_24h|default(0) }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-50 dark:bg-blue-500/10 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-clock text-blue-600 dark:text-blue-400 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 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 dark:text-slate-400 uppercase tracking-wide">Occurrences</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ errorStats.total_occurrences|default(0) }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-layer-group text-indigo-600 dark:text-indigo-400 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 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 dark:text-slate-300 mb-1.5">Status</label>
|
||||
<select name="resolved" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
||||
<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 dark:text-slate-300 mb-1.5">Error Type</label>
|
||||
<input type="text" name="type" value="{{ currentFilters.type }}" placeholder="e.g., PDOException" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-slate-300 mb-1.5">Sort By</label>
|
||||
<select name="sort" class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
||||
<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 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-sm font-medium">
|
||||
<i class="fas fa-times mr-2"></i>
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="order" value="{{ currentFilters.order }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Info -->
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-600 dark:text-slate-400">
|
||||
Showing <span class="font-semibold text-gray-900 dark:text-white">{{ pagination.showing_from }}</span> to
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ pagination.showing_to }}</span> of
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ pagination.total }}</span> error(s)
|
||||
</div>
|
||||
|
||||
<form method="GET" action="/errors" class="flex items-center gap-2">
|
||||
<input type="hidden" name="resolved" value="{{ currentFilters.resolved }}">
|
||||
<input type="hidden" name="type" value="{{ currentFilters.type }}">
|
||||
<input type="hidden" name="sort" value="{{ currentFilters.sort }}">
|
||||
<input type="hidden" name="order" value="{{ currentFilters.order }}">
|
||||
|
||||
<label for="per_page" class="text-sm text-gray-600 dark:text-slate-400">Show:</label>
|
||||
<select name="per_page" id="per_page" onchange="this.form.submit()" class="px-3 py-1.5 border border-gray-300 dark:border-slate-600 rounded-lg text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-white">
|
||||
<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 dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
|
||||
<div id="bulk-actions" class="hidden px-6 py-3 bg-blue-50 dark:bg-blue-500/10 border-b border-blue-200 dark:border-blue-500/20 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<span id="selected-count" class="text-sm font-medium text-gray-700 dark:text-slate-300"></span>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium">
|
||||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="clearSelection()" class="inline-flex items-center text-sm font-medium text-gray-600 dark:text-slate-400 hover:text-gray-900 dark:hover:text-white hover:bg-blue-100 dark:hover:bg-blue-500/20 px-3 py-1.5 rounded-lg transition-colors">
|
||||
<i class="fas fa-times mr-1.5"></i> Clear Selection
|
||||
</button>
|
||||
</div>
|
||||
{% if errors is defined and errors is not empty %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<thead class="bg-gray-50 dark:bg-slate-900">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left">
|
||||
<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)" class="rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary">
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Error</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Location</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Occurrences</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Last Occurred</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||
{% for error in errors %}
|
||||
{% set errorTypeShort = error.error_type|split('\\')|last %}
|
||||
{% set isResolved = error.is_resolved %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150">
|
||||
<td class="px-6 py-4">
|
||||
<input type="checkbox" class="error-checkbox rounded border-gray-300 dark:border-slate-600 text-primary focus:ring-primary" value="{{ 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 dark:bg-red-500/10 rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-bug text-red-600 dark:text-red-400"></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">{{ error.error_id }}</span>
|
||||
<button onclick="copyToClipboard('{{ error.error_id }}')" class="text-gray-400 dark:text-slate-500 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 dark:text-white">{{ errorTypeShort }}</p>
|
||||
<p class="text-xs text-gray-600 dark:text-slate-400 mt-0.5 truncate" style="max-width: 300px;" title="{{ error.error_message }}">
|
||||
{{ error.error_message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-xs">
|
||||
<p class="font-mono text-gray-600 dark:text-slate-400 truncate" style="max-width: 200px;" title="{{ error.error_file }}">
|
||||
{{ error.error_file|split('/')|last|split('\\')|last }}
|
||||
</p>
|
||||
<p class="text-gray-500 dark:text-slate-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 dark:bg-red-500/10 text-red-800 dark:text-red-400' : 'bg-gray-100 dark:bg-slate-700 text-gray-800 dark:text-slate-300' }}">
|
||||
<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 dark:text-slate-400">
|
||||
<div class="flex items-center">
|
||||
<i class="far fa-clock mr-2"></i>
|
||||
{{ error.last_occurred_at|date("M d, H:i") }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if isResolved %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400 border border-green-200 dark:border-green-500/20">
|
||||
<i class="fas fa-check-circle mr-1"></i>
|
||||
Resolved
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 dark:bg-orange-500/10 text-orange-800 dark:text-orange-400 border border-orange-200 dark:border-orange-500/20">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
Unresolved
|
||||
</span>
|
||||
{% 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/{{ error.error_id }}" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if not isResolved %}
|
||||
<button onclick="markResolved('{{ error.error_id }}')" class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300" title="Mark as Resolved">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="deleteError('{{ error.error_id }}')" class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300" title="Delete Error">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12 px-6">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-check-circle text-green-500 dark:text-green-400 text-6xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-slate-300 mb-1">No Errors Found</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-slate-400 mb-4">
|
||||
{% if currentFilters.resolved or currentFilters.type %}
|
||||
No errors match your filter criteria.
|
||||
{% else %}
|
||||
Great! Your application is running smoothly.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{% 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 dark:text-slate-400">
|
||||
Page <span class="font-semibold text-gray-900 dark:text-white">{{ pagination.current_page }}</span> of
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ pagination.total_pages }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
{% if pagination.current_page > 1 %}
|
||||
<a href="{{ pagination_url(1, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">
|
||||
<i class="fas fa-angle-double-left"></i>
|
||||
</a>
|
||||
<a href="{{ pagination_url(pagination.current_page - 1, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">
|
||||
<i class="fas fa-angle-left"></i> Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% set range = 2 %}
|
||||
{% set startPage = max(1, pagination.current_page - range) %}
|
||||
{% set endPage = min(pagination.total_pages, pagination.current_page + range) %}
|
||||
|
||||
{% if startPage > 1 %}
|
||||
<a href="{{ pagination_url(1, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">1</a>
|
||||
{% if startPage > 2 %}
|
||||
<span class="px-2 text-gray-500 dark:text-slate-400">...</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for i in startPage..endPage %}
|
||||
{% if i == pagination.current_page %}
|
||||
<span class="px-3 py-2 text-sm bg-primary text-white rounded-lg font-semibold">{{ i }}</span>
|
||||
{% else %}
|
||||
<a href="{{ pagination_url(i, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">{{ i }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if endPage < pagination.total_pages %}
|
||||
{% if endPage < pagination.total_pages - 1 %}
|
||||
<span class="px-2 text-gray-500 dark:text-slate-400">...</span>
|
||||
{% endif %}
|
||||
<a href="{{ pagination_url(pagination.total_pages, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">{{ pagination.total_pages }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if pagination.current_page < pagination.total_pages %}
|
||||
<a href="{{ pagination_url(pagination.current_page + 1, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">
|
||||
Next <i class="fas fa-angle-right"></i>
|
||||
</a>
|
||||
<a href="{{ pagination_url(pagination.total_pages, currentFilters, pagination.per_page) }}" class="px-3 py-2 text-sm border border-gray-300 dark:border-slate-600 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 transition-colors">
|
||||
<i class="fas fa-angle-double-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Resolution Notes Modal -->
|
||||
<div id="resolutionModal" class="hidden fixed inset-0 bg-gray-600/50 dark:bg-black/50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border border-gray-200 dark:border-slate-700 w-full max-w-md shadow-lg rounded-lg bg-white dark:bg-slate-800">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mr-2"></i>
|
||||
Mark Error as Resolved
|
||||
</h3>
|
||||
<button onclick="closeResolutionModal()" class="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="resolutionNotes" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-2">
|
||||
Resolution Notes (Optional)
|
||||
</label>
|
||||
<textarea id="resolutionNotes" rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none bg-white dark:bg-slate-900 text-gray-900 dark:text-white"
|
||||
placeholder="Describe how you resolved this error or any relevant notes..."></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-slate-400">Add any details about the fix or resolution for future reference.</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button onclick="closeResolutionModal()" class="px-4 py-2 bg-gray-200 dark:bg-slate-700 text-gray-800 dark:text-slate-200 rounded-lg hover:bg-gray-300 dark:hover:bg-slate-600 transition-colors text-sm font-medium">Cancel</button>
|
||||
<button onclick="submitResolution()" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<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() {
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast bg-white dark:bg-slate-800 border-l-4 border-green-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in';
|
||||
toast.innerHTML = '<div class="flex-shrink-0"><div class="w-8 h-8 bg-green-100 dark:bg-green-500/10 rounded-full flex items-center justify-center"><i class="fas fa-check text-green-600 dark:text-green-400 text-sm"></i></div></div><div class="ml-3 flex-1"><p class="text-sm font-medium text-gray-900 dark:text-white">Success</p><p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">Copied to clipboard!</p></div><button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300 transition-colors"><i class="fas fa-times text-sm"></i></button>';
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => { toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out'; toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; setTimeout(() => toast.remove(), 300); }, 3000);
|
||||
}
|
||||
|
||||
let currentErrorId = null;
|
||||
|
||||
function markResolved(errorId) {
|
||||
currentErrorId = errorId;
|
||||
document.getElementById('resolutionModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeResolutionModal() {
|
||||
document.getElementById('resolutionModal').classList.add('hidden');
|
||||
document.getElementById('resolutionNotes').value = '';
|
||||
currentErrorId = null;
|
||||
}
|
||||
|
||||
function submitResolution() {
|
||||
if (!currentErrorId) return;
|
||||
const notes = document.getElementById('resolutionNotes').value;
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/errors/' + currentErrorId + '/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();
|
||||
}
|
||||
|
||||
function toggleSelectAll(checkbox) {
|
||||
document.querySelectorAll('.error-checkbox').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');
|
||||
selectedCount.textContent = checkboxes.length + ' error(s) selected';
|
||||
} else {
|
||||
bulkActions.classList.add('hidden');
|
||||
}
|
||||
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() {
|
||||
return Array.from(document.querySelectorAll('.error-checkbox:checked')).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
document.querySelectorAll('.error-checkbox').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>
|
||||
{% endblock %}
|
||||
@@ -3,12 +3,9 @@
|
||||
<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>
|
||||
<title>Debug Error - {{ error_type|default('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>
|
||||
@@ -28,28 +25,15 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
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;
|
||||
}
|
||||
.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">
|
||||
@@ -84,18 +68,16 @@
|
||||
<!-- 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') ?>
|
||||
{{ error_type|default('Error') }}
|
||||
</h2>
|
||||
<p class="text-lg text-gray-700 mb-4"><?= htmlspecialchars($error_message ?? 'An error occurred') ?></p>
|
||||
<p class="text-lg text-gray-700 mb-4">{{ error_message|default('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>
|
||||
@@ -105,12 +87,12 @@
|
||||
<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') ?>
|
||||
{{ error_file|default('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>
|
||||
<span class="font-mono text-red-600 font-bold text-lg">{{ error_line|default('?') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,67 +101,63 @@
|
||||
|
||||
<!-- 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') ?>')"
|
||||
<button onclick="copyToClipboard('{{ error_id|default('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>
|
||||
<code class="text-sm font-mono font-bold text-primary">{{ error_id|default('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>
|
||||
<span class="font-mono font-bold text-gray-900">{{ request_method|default('GET') }}</span>
|
||||
</p>
|
||||
<code class="text-xs text-gray-600 font-mono block truncate" title="<?= htmlspecialchars($request_uri ?? '/') ?>">
|
||||
<?= htmlspecialchars($request_uri ?? '/') ?>
|
||||
<code class="text-xs text-gray-600 font-mono block truncate" title="{{ request_uri|default('/') }}">
|
||||
{{ request_uri|default('/') }}
|
||||
</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>
|
||||
{% if user_info %}
|
||||
<p class="text-sm font-semibold text-gray-900">{{ user_info.username }}</p>
|
||||
<p class="text-xs text-gray-600">
|
||||
<i class="fas fa-user mr-1"></i>
|
||||
<?= htmlspecialchars($user_info['role']) ?>
|
||||
{{ user_info.role }}
|
||||
</p>
|
||||
<?php else: ?>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">
|
||||
<i class="fas fa-user-slash mr-1"></i>
|
||||
Guest (Not logged in)
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
{% 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) ?>
|
||||
PHP {{ php_version|default('unknown') }}
|
||||
</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
|
||||
{{ memory_usage_mb|default(0) }}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')) ?>
|
||||
{{ occurred_at|default('now')|date("H:i:s") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,8 +171,7 @@
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Stack Trace -->
|
||||
<?php if (!empty($stack_trace)): ?>
|
||||
{% if stack_trace is defined and 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">
|
||||
@@ -211,24 +188,20 @@
|
||||
</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>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
{% for line in stack_trace|split("\n") %}
|
||||
{% if line|trim %}
|
||||
<div class="flex font-mono text-sm">
|
||||
<span class="line-number mr-4 text-right" style="min-width: 2rem">{{ '%02d'|format(loop.index0) }}</span>
|
||||
<span class="flex-1 text-green-400">{{ line }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
{% endif %}
|
||||
|
||||
<!-- Request Data -->
|
||||
<?php if (!empty($request_data)): ?>
|
||||
{% if request_data is defined and 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">
|
||||
@@ -237,7 +210,7 @@
|
||||
<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) ?>
|
||||
{{ request_data|length }}
|
||||
</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform" id="request-data-chevron"></i>
|
||||
@@ -245,20 +218,24 @@
|
||||
</button>
|
||||
<div id="request-data" class="hidden p-6">
|
||||
<div class="space-y-3">
|
||||
<?php foreach ($request_data as $key => $value): ?>
|
||||
{% for key, value in request_data %}
|
||||
<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) ?>
|
||||
{{ 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) ?>
|
||||
{% if value is iterable %}
|
||||
{{ value|json_encode(constant('JSON_PRETTY_PRINT')) }}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
</code>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -277,24 +254,24 @@
|
||||
<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>
|
||||
<span class="font-mono font-bold text-gray-900">{{ request_method|default('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 ?? '/') ?>
|
||||
{{ request_uri|default('/') }}
|
||||
</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') ?>
|
||||
{{ ip_address|default('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') ?>
|
||||
{{ user_agent|default('Unknown') }}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,26 +290,25 @@
|
||||
<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>
|
||||
<span class="font-mono text-gray-900">{{ php_version|default('unknown') }}</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>
|
||||
<span class="font-mono text-gray-900">{{ memory_usage_mb|default(0) }}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>
|
||||
<span class="font-mono text-gray-900">{{ peak_memory_mb|default(0) }}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>
|
||||
<span class="font-mono text-gray-900 text-xs">{{ occurred_at|default('now')|date("Y-m-d H:i:s T") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Data -->
|
||||
<?php if (!empty($session_data)): ?>
|
||||
{% if session_data is defined and 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">
|
||||
@@ -341,7 +317,7 @@
|
||||
<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) ?>
|
||||
{{ session_data|length }}
|
||||
</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 text-xs transition-transform" id="session-data-chevron"></i>
|
||||
@@ -357,20 +333,24 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<?php foreach ($session_data as $key => $value): ?>
|
||||
{% for key, value in session_data %}
|
||||
<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-700 align-top">{{ key }}</td>
|
||||
<td class="px-3 py-2 font-mono text-gray-600 break-all">
|
||||
<?= htmlspecialchars(is_array($value) ? json_encode($value) : $value) ?>
|
||||
{% if value is iterable %}
|
||||
{{ value|json_encode }}
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -409,12 +389,11 @@
|
||||
<div class="text-center text-sm text-gray-500">
|
||||
<p>
|
||||
<i class="fas fa-globe text-primary mr-1"></i>
|
||||
Domain Monitor © <?= date('Y') ?> • Development Mode
|
||||
Domain Monitor © {{ "now"|date("Y") }} • Development Mode
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
function toggleSection(sectionId) {
|
||||
const section = document.getElementById(sectionId);
|
||||
@@ -422,14 +401,10 @@
|
||||
|
||||
if (section.classList.contains('hidden')) {
|
||||
section.classList.remove('hidden');
|
||||
if (chevron) {
|
||||
chevron.style.transform = 'rotate(180deg)';
|
||||
}
|
||||
if (chevron) chevron.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
section.classList.add('hidden');
|
||||
if (chevron) {
|
||||
chevron.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
if (chevron) chevron.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,14 +427,12 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -467,34 +440,29 @@
|
||||
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';
|
||||
}
|
||||
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 errorType = {{ (error_type|default('Error'))|json_encode|raw }};
|
||||
const errorMessage = {{ (error_message|default('Unknown error'))|json_encode|raw }};
|
||||
const errorFile = {{ (error_file|default('Unknown'))|json_encode|raw }};
|
||||
const errorLine = {{ (error_line|default('?'))|json_encode|raw }};
|
||||
const errorId = {{ (error_id|default('N/A'))|json_encode|raw }};
|
||||
const phpVersion = {{ (php_version|default('unknown'))|json_encode|raw }};
|
||||
const requestMethod = {{ (request_method|default('GET'))|json_encode|raw }};
|
||||
const requestUri = {{ (request_uri|default('/'))|json_encode|raw }};
|
||||
const userAgent = {{ (user_agent|default('Unknown'))|json_encode|raw }};
|
||||
const ipAddress = {{ (ip_address|default('Unknown'))|json_encode|raw }};
|
||||
const timestamp = {{ (occurred_at|default('now'))|json_encode|raw }};
|
||||
|
||||
const userInfo = <?= json_encode($user_info ?? null) ?>;
|
||||
const userInfo = {{ (user_info|default(null))|json_encode|raw }};
|
||||
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) {
|
||||
@@ -502,9 +470,7 @@
|
||||
let stackText = '';
|
||||
lines.forEach(line => {
|
||||
const textSpan = line.querySelector('span:last-child');
|
||||
if (textSpan) {
|
||||
stackText += textSpan.textContent + '\n';
|
||||
}
|
||||
if (textSpan) stackText += textSpan.textContent + '\n';
|
||||
});
|
||||
stackTrace = stackText.trim();
|
||||
}
|
||||
@@ -532,8 +498,8 @@ USER CONTEXT:
|
||||
|
||||
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
|
||||
- Memory Usage: {{ memory_usage_mb|default(0) }}MB
|
||||
- Peak Memory: {{ peak_memory_mb|default(0) }}MB
|
||||
|
||||
STACK TRACE:
|
||||
${stackTrace}
|
||||
@@ -562,4 +528,3 @@ Please include this report when reporting bugs.`;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user