Add error log management and bulk admin actions

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

View File

@@ -161,11 +161,11 @@ ob_start();
</div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-green-700">Create Group</span>
</a>
<a href="/debug/whois" class="flex items-center p-3 border border-gray-200 hover:border-purple-500 hover:bg-purple-50 rounded-lg transition-all duration-200 group">
<div class="w-9 h-9 bg-purple-50 group-hover:bg-purple-500 rounded-lg flex items-center justify-center group-hover:text-white text-purple-600 transition-colors duration-200">
<a href="/debug/whois" class="flex items-center p-3 border border-gray-200 hover:border-indigo-500 hover:bg-indigo-50 rounded-lg transition-all duration-200 group">
<div class="w-9 h-9 bg-indigo-50 group-hover:bg-indigo-500 rounded-lg flex items-center justify-center group-hover:text-white text-indigo-600 transition-colors duration-200">
<i class="fas fa-search text-sm"></i>
</div>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-purple-700">WHOIS Lookup</span>
<span class="ml-3 text-sm font-medium text-gray-700 group-hover:text-indigo-700">WHOIS Lookup</span>
</a>
</div>
</div>

View File

@@ -28,81 +28,27 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
?>
<!-- Action Buttons -->
<div class="mb-4 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<!-- Bulk Actions Toolbar (Hidden by default, shown when domains are selected) -->
<div id="bulk-actions" class="hidden items-center gap-2">
<span id="selected-count" class="text-sm font-medium text-gray-700"></span>
<button type="button" onclick="bulkRefresh()" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-2"></i>
Refresh
</button>
<div class="relative inline-block">
<button type="button" onclick="toggleAssignGroupDropdown()" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-bell mr-2"></i>
Assign Group
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div id="assign-group-dropdown" class="hidden absolute left-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 z-10">
<form method="POST" action="/domains/bulk-assign-group" id="bulk-assign-form">
<?= csrf_field() ?>
<input type="hidden" name="domain_ids" id="bulk-assign-ids">
<div class="p-3">
<select name="group_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<option value="">-- No Group --</option>
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="border-t border-gray-200 p-2 flex gap-2">
<button type="submit" class="flex-1 px-3 py-1.5 bg-primary text-white text-xs rounded hover:bg-primary-dark">
Assign
</button>
<button type="button" onclick="toggleAssignGroupDropdown()" class="flex-1 px-3 py-1.5 bg-gray-200 text-gray-700 text-xs rounded hover:bg-gray-300">
Cancel
</button>
</div>
</form>
</div>
</div>
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>
Delete
</button>
<button type="button" onclick="clearSelection()" class="inline-flex items-center px-4 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-times mr-2"></i>
Clear
</button>
</div>
</div>
<div class="flex gap-2">
<?php if (!empty($domains)): ?>
<form method="POST" action="/domains/bulk-refresh" id="refresh-all-form">
<?= csrf_field() ?>
<?php foreach ($domains as $domain): ?>
<input type="hidden" name="domain_ids[]" value="<?= $domain['id'] ?>">
<?php endforeach; ?>
<button type="submit" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium" title="Refresh all domains on this page">
<i class="fas fa-sync-alt mr-2"></i>
Refresh Page (<?= count($domains) ?>)
</button>
</form>
<?php endif; ?>
<a href="/domains/bulk-add" class="inline-flex items-center px-4 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 transition-colors font-medium">
<i class="fas fa-layer-group mr-2"></i>
Bulk Add
</a>
<a href="/domains/create" class="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-plus mr-2"></i>
Add Domain
</a>
</div>
<div class="mb-4 flex gap-2 justify-end">
<?php if (!empty($domains)): ?>
<form method="POST" action="/domains/bulk-refresh" id="refresh-all-form">
<?= csrf_field() ?>
<?php foreach ($domains as $domain): ?>
<input type="hidden" name="domain_ids[]" value="<?= $domain['id'] ?>">
<?php endforeach; ?>
<button type="submit" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium" title="Refresh all domains on this page">
<i class="fas fa-sync-alt mr-2"></i>
Refresh Page (<?= count($domains) ?>)
</button>
</form>
<?php endif; ?>
<a href="/domains/bulk-add" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700 transition-colors font-medium">
<i class="fas fa-layer-group mr-2"></i>
Bulk Add
</a>
<a href="/domains/create" class="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors text-sm font-medium">
<i class="fas fa-plus mr-2"></i>
Add Domain
</a>
</div>
<!-- Filters & Search -->
@@ -150,6 +96,59 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
</form>
</div>
<!-- Bulk Actions Toolbar (Hidden by default, shown when domains are selected) -->
<div id="bulk-actions" class="hidden mb-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span id="selected-count" class="text-sm font-medium text-blue-900"></span>
<button type="button" onclick="bulkRefresh()" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-sync-alt mr-2"></i>
Refresh Selected
</button>
<div class="relative inline-block">
<button type="button" onclick="toggleAssignGroupDropdown()" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
<i class="fas fa-bell mr-2"></i>
Assign Group
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div id="assign-group-dropdown" class="hidden absolute left-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 z-10">
<form method="POST" action="/domains/bulk-assign-group" id="bulk-assign-form">
<?= csrf_field() ?>
<div class="p-3">
<select name="group_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<option value="">-- No Group --</option>
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="border-t border-gray-200 p-2 flex gap-2">
<button type="submit" class="flex-1 px-3 py-1.5 bg-primary text-white text-xs rounded hover:bg-primary-dark">
Assign
</button>
<button type="button" onclick="toggleAssignGroupDropdown()" class="flex-1 px-3 py-1.5 bg-gray-200 text-gray-700 text-xs rounded hover:bg-gray-300">
Cancel
</button>
</div>
</form>
</div>
</div>
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>
Delete Selected
</button>
<button type="button" onclick="clearSelection()" class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-times mr-2"></i>
Clear Selection
</button>
</div>
</div>
</div>
<!-- Pagination Info & Per Page Selector -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600">
@@ -184,8 +183,8 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left w-12">
<input type="checkbox" id="select-all" onclick="toggleSelectAll(this)" class="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer">
<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">
<a href="<?= sortUrl('domain_name', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
@@ -231,8 +230,8 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
$statusIcon = $domain['statusIcon'];
?>
<tr class="hover:bg-gray-50 transition-colors duration-150 domain-row">
<td class="px-4 py-4">
<input type="checkbox" class="domain-checkbox w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer" value="<?= $domain['id'] ?>">
<td class="px-6 py-4">
<input type="checkbox" class="domain-checkbox rounded border-gray-300 text-primary focus:ring-primary" value="<?= $domain['id'] ?>" onchange="updateBulkActions()">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
@@ -328,7 +327,7 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
<?php foreach ($domains as $domain): ?>
<div class="p-6 hover:bg-gray-50 transition-colors duration-150">
<div class="flex items-center mb-3">
<input type="checkbox" class="domain-checkbox-mobile w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary cursor-pointer mr-3" value="<?= $domain['id'] ?>">
<input type="checkbox" class="domain-checkbox-mobile rounded border-gray-300 text-primary focus:ring-primary mr-3" value="<?= $domain['id'] ?>" onchange="updateBulkActions()">
<a href="/domains/<?= $domain['id'] ?>" class="text-lg font-semibold text-gray-900 hover:text-primary"><?= htmlspecialchars($domain['domain_name']) ?></a>
</div>
<!-- Add mobile view content here if needed -->
@@ -440,76 +439,47 @@ $currentFilters = $filters ?? ['search' => '', 'status' => '', 'group' => '', 's
<script>
// Multi-select functionality
function toggleSelectAll(checkbox) {
// Only select checkboxes that are currently visible
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox');
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile');
// Check if desktop view is visible (lg:block class)
const desktopTable = document.querySelector('.hidden.lg\\:block');
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
if (isDesktopVisible) {
// Desktop view is visible, select desktop checkboxes
desktopCheckboxes.forEach(cb => cb.checked = checkbox.checked);
} else {
// Mobile view is visible, select mobile checkboxes
mobileCheckboxes.forEach(cb => cb.checked = checkbox.checked);
}
const checkboxes = document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
// Only count checkboxes that are currently visible
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox:checked');
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile:checked');
// Check if desktop view is visible
const desktopTable = document.querySelector('.hidden.lg\\:block');
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
const checkboxes = isDesktopVisible ? desktopCheckboxes : mobileCheckboxes;
const checkboxes = document.querySelectorAll('.domain-checkbox:checked, .domain-checkbox-mobile:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (checkboxes.length > 0) {
bulkActions.classList.remove('hidden');
bulkActions.classList.add('flex');
selectedCount.textContent = `${checkboxes.length} selected`;
selectedCount.textContent = `${checkboxes.length} domain(s) selected`;
} else {
bulkActions.classList.add('hidden');
bulkActions.classList.remove('flex');
}
// Update select all checkbox state
const allCheckboxes = document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
selectAllCheckbox.indeterminate = checkboxes.length > 0 && checkboxes.length < allCheckboxes.length;
}
}
function clearSelection() {
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox');
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile');
// Check if desktop view is visible
const desktopTable = document.querySelector('.hidden.lg\\:block');
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
if (isDesktopVisible) {
desktopCheckboxes.forEach(cb => cb.checked = false);
} else {
mobileCheckboxes.forEach(cb => cb.checked = false);
}
const checkboxes = document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile');
checkboxes.forEach(cb => {
cb.checked = false;
});
document.getElementById('select-all').checked = false;
updateBulkActions();
}
function getSelectedIds() {
// Only get IDs from currently visible checkboxes
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox:checked');
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile:checked');
// Check if desktop view is visible
const desktopTable = document.querySelector('.hidden.lg\\:block');
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
const checkboxes = isDesktopVisible ? desktopCheckboxes : mobileCheckboxes;
const checkboxes = document.querySelectorAll('.domain-checkbox:checked, .domain-checkbox-mobile:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
@@ -580,37 +550,6 @@ document.getElementById('bulk-assign-form')?.addEventListener('submit', function
});
});
// Listen to checkbox changes
document.querySelectorAll('.domain-checkbox, .domain-checkbox-mobile').forEach(checkbox => {
checkbox.addEventListener('change', function() {
// Update the select-all checkbox state
const desktopCheckboxes = document.querySelectorAll('.domain-checkbox');
const mobileCheckboxes = document.querySelectorAll('.domain-checkbox-mobile');
// Check if desktop view is visible
const desktopTable = document.querySelector('.hidden.lg\\:block');
const isDesktopVisible = desktopTable && !desktopTable.classList.contains('hidden');
const checkboxes = isDesktopVisible ? desktopCheckboxes : mobileCheckboxes;
const checkedBoxes = isDesktopVisible ?
document.querySelectorAll('.domain-checkbox:checked') :
document.querySelectorAll('.domain-checkbox-mobile:checked');
const selectAllCheckbox = document.getElementById('select-all');
if (checkedBoxes.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedBoxes.length === checkboxes.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
updateBulkActions();
});
});
// Close dropdown when clicking outside
document.addEventListener('click', function(event) {
@@ -622,11 +561,6 @@ document.addEventListener('click', function(event) {
}
});
// Handle window resize to sync checkboxes when switching between desktop/mobile views
window.addEventListener('resize', function() {
// Small delay to allow CSS classes to update
setTimeout(updateBulkActions, 100);
});
</script>
<?php

View File

@@ -32,7 +32,7 @@ ob_start();
<?= $daysLeft !== null ? $daysLeft . ' days left' : 'No expiry date' ?>
</span>
<?php endif; ?>
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-purple-100 text-purple-800 border border-purple-200">
<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-semibold bg-indigo-100 text-indigo-800 border border-indigo-200">
<i class="fas fa-<?= $domain['is_active'] ? 'check-circle' : 'pause-circle' ?> mr-1.5"></i>
<?= $domain['is_active'] ? 'Monitoring Active' : 'Monitoring Paused' ?>
</span>
@@ -167,8 +167,8 @@ ob_start();
</div>
<?php endif; ?>
<div class="flex items-center p-2 bg-purple-50 rounded border border-purple-200">
<div class="w-7 h-7 bg-purple-500 rounded flex items-center justify-center mr-2">
<div class="flex items-center p-2 bg-indigo-50 rounded border border-indigo-200">
<div class="w-7 h-7 bg-indigo-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-sync text-white text-xs"></i>
</div>
<div>

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

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

View File

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

View File

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

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

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

View File

@@ -79,14 +79,16 @@ ob_start();
<?php foreach ($group['channels'] as $channel):
$config = json_decode($channel['channel_config'], true);
$icons = ['email' => 'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack'];
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'purple'];
$iconClasses = ['email' => 'fas', 'telegram' => 'fab', 'discord' => 'fab', 'slack' => 'fab'];
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal'];
$icon = $icons[$channel['channel_type']] ?? 'fa-bell';
$iconClass = $iconClasses[$channel['channel_type']] ?? 'fas';
$color = $colors[$channel['channel_type']] ?? 'gray';
?>
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow duration-200">
<div class="flex items-start justify-between mb-4">
<div class="w-12 h-12 bg-<?= $color ?>-100 rounded-lg flex items-center justify-center">
<i class="fab <?= $icon ?> text-<?= $color ?>-600 text-xl"></i>
<i class="<?= $iconClass ?> <?= $icon ?> text-<?= $color ?>-600 text-xl"></i>
</div>
<span class="px-3 py-1 rounded-full text-xs font-semibold <?= $channel['is_active'] ? 'bg-green-100 text-green-800' : 'bg-gray-200 text-gray-600' ?>">
<?= $channel['is_active'] ? 'Active' : 'Disabled' ?>

View File

@@ -31,6 +31,25 @@ ob_start();
</div>
</div>
<!-- Bulk Actions Toolbar (Hidden by default, shown when groups are selected) -->
<div id="bulk-actions" class="hidden mb-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span id="selected-count" class="text-sm font-medium text-blue-900"></span>
<button type="button" onclick="bulkDelete()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>
Delete Selected
</button>
<button type="button" onclick="clearSelection()" class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-times mr-2"></i>
Clear Selection
</button>
</div>
</div>
</div>
<!-- Groups List -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<?php if (!empty($groups)): ?>
@@ -39,6 +58,9 @@ ob_start();
<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-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Group Name</th>
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Description</th>
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Channels</th>
@@ -49,6 +71,9 @@ ob_start();
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($groups as $group): ?>
<tr class="hover:bg-gray-50 transition-colors duration-150">
<td class="px-6 py-4">
<input type="checkbox" class="group-checkbox rounded border-gray-300 text-primary focus:ring-primary" value="<?= $group['id'] ?>" onchange="updateBulkActions()">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
@@ -150,6 +175,85 @@ ob_start();
<?php endif; ?>
</div>
<script>
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.group-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const checkboxes = document.querySelectorAll('.group-checkbox:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (checkboxes.length > 0) {
bulkActions.classList.remove('hidden');
bulkActions.classList.add('flex');
selectedCount.textContent = checkboxes.length + ' group(s) selected';
} else {
bulkActions.classList.add('hidden');
bulkActions.classList.remove('flex');
}
// Update select all checkbox state
const allCheckboxes = document.querySelectorAll('.group-checkbox');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
selectAllCheckbox.indeterminate = checkboxes.length > 0 && checkboxes.length < allCheckboxes.length;
}
}
function clearSelection() {
const checkboxes = document.querySelectorAll('.group-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
document.getElementById('select-all').checked = false;
updateBulkActions();
}
function getSelectedGroupIds() {
const checkboxes = document.querySelectorAll('.group-checkbox:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
function bulkDelete() {
const groupIds = getSelectedGroupIds();
if (groupIds.length === 0) {
alert('Please select at least one group to delete');
return;
}
if (!confirm(`Are you sure you want to delete ${groupIds.length} group(s)? Domains will be unassigned from these groups.`)) {
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/groups/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 = 'group_ids';
idsInput.value = JSON.stringify(groupIds);
form.appendChild(idsInput);
document.body.appendChild(form);
form.submit();
}
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';

View File

@@ -43,7 +43,7 @@
<div class="mt-4 pt-3 border-t border-gray-800">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-1">Tools</p>
<div class="space-y-0.5">
<a href="/debug/whois" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150">
<a href="/debug/whois" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/debug/whois') !== false ? 'bg-primary text-white' : '' ?>">
<i class="fas fa-search text-xs mr-3 w-4"></i>
<span class="text-sm">WHOIS Lookup</span>
</a>
@@ -63,6 +63,10 @@
<i class="fas fa-users text-xs mr-3 w-4"></i>
<span class="text-sm">Users</span>
</a>
<a href="/errors" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/errors') !== false ? 'bg-primary text-white' : '' ?>">
<i class="fas fa-bug text-xs mr-3 w-4"></i>
<span class="text-sm">Error Logs</span>
</a>
</div>
</div>
<?php endif; ?>

View File

@@ -129,9 +129,7 @@
</div>
<div class="hidden lg:block text-left">
<p class="text-sm font-medium text-gray-700"><?= htmlspecialchars($_SESSION['username'] ?? 'User') ?></p>
<p class="text-xs text-gray-500">
<?= ucfirst($_SESSION['role'] ?? 'user') ?>
</p>
<p class="text-xs text-gray-500"><?= htmlspecialchars($_SESSION['email'] ?? '') ?></p>
</div>
<i class="fas fa-chevron-down text-gray-400 text-xs hidden md:block"></i>
</button>
@@ -141,9 +139,16 @@
<div class="px-4 py-3 border-b border-gray-200">
<p class="text-sm font-medium text-gray-900"><?= htmlspecialchars($_SESSION['full_name'] ?? $_SESSION['username'] ?? 'User') ?></p>
<p class="text-xs text-gray-500 mt-1"><?= htmlspecialchars($_SESSION['email'] ?? 'user@example.com') ?></p>
<span class="inline-block mt-2 px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded">
<i class="fas fa-circle text-xs mr-1"></i>Online
</span>
<div class="flex items-center gap-2 mt-2">
<span class="inline-flex items-center px-2.5 py-1 bg-<?= $_SESSION['role'] === 'admin' ? 'amber' : 'blue' ?>-100 text-<?= $_SESSION['role'] === 'admin' ? 'amber' : 'blue' ?>-800 text-xs font-semibold rounded border border-<?= $_SESSION['role'] === 'admin' ? 'amber' : 'blue' ?>-200">
<i class="fas fa-<?= $_SESSION['role'] === 'admin' ? 'crown' : 'user' ?> mr-1.5"></i>
<?= ucfirst($_SESSION['role'] ?? 'user') ?>
</span>
<span class="inline-flex items-center px-2.5 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded border border-green-200">
<i class="fas fa-circle text-xs mr-1"></i>
Online
</span>
</div>
</div>
<a href="/profile#profile" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">

View File

@@ -21,7 +21,7 @@ ob_start();
<p class="text-sm text-gray-500 mt-1">@<?= htmlspecialchars($user['username'] ?? '') ?></p>
<!-- Role Badge -->
<span class="inline-flex items-center mt-3 px-2.5 py-1 bg-<?= $user['role'] === 'admin' ? 'indigo' : 'blue' ?>-100 text-<?= $user['role'] === 'admin' ? 'indigo' : 'blue' ?>-800 text-xs font-semibold rounded">
<span class="inline-flex items-center mt-3 px-2.5 py-1 bg-<?= $user['role'] === 'admin' ? 'amber' : 'blue' ?>-100 text-<?= $user['role'] === 'admin' ? 'amber' : 'blue' ?>-800 text-xs font-semibold rounded border border-<?= $user['role'] === 'admin' ? 'amber' : 'blue' ?>-200">
<i class="fas fa-<?= $user['role'] === 'admin' ? 'crown' : 'user' ?> mr-1.5"></i>
<?= ucfirst($user['role'] ?? 'user') ?>
</span>

View File

@@ -74,8 +74,8 @@ ob_start();
<?php endif; ?>
</p>
</div>
<div class="w-12 h-12 bg-purple-50 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-purple-600 text-lg"></i>
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-indigo-600 text-lg"></i>
</div>
</div>
</div>

View File

@@ -28,45 +28,39 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
?>
<!-- Action Buttons -->
<div class="mb-4">
<div class="flex flex-wrap gap-2 justify-between items-center">
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="flex flex-wrap gap-2">
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="complete_workflow">
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
<i class="fas fa-rocket mr-2"></i>
Import TLDs
</button>
</form>
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="check_updates">
<button type="submit" <?= $stats['total'] == 0 ? 'disabled' : '' ?> class="inline-flex items-center px-4 py-2.5 <?= $stats['total'] == 0 ? 'bg-gray-400 cursor-not-allowed' : 'bg-purple-600 hover:bg-purple-700' ?> text-white text-sm rounded-lg transition-colors font-medium" title="<?= $stats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
<i class="fas fa-sync-alt mr-2"></i>
Check Updates
</button>
</form>
<a href="/tld-registry/import-logs" class="inline-flex items-center px-4 py-2.5 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-history mr-2"></i>
Import Logs
</a>
</div>
<?php else: ?>
<div>
<p class="text-sm text-gray-600">
<i class="fas fa-info-circle mr-1"></i>
View-only mode. Contact admin to import or modify TLD data.
</p>
</div>
<?php endif; ?>
<div class="flex gap-2">
<!-- Search and filters will stay visible for all users -->
</div>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="mb-4 flex justify-end gap-2">
<a href="/tld-registry/import-logs" class="inline-flex items-center px-4 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-history mr-2"></i>
Import Logs
</a>
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="check_updates">
<button type="submit" <?= $stats['total'] == 0 ? 'disabled' : '' ?> class="inline-flex items-center px-4 py-2 <?= $stats['total'] == 0 ? 'bg-gray-400 cursor-not-allowed' : 'bg-indigo-600 hover:bg-indigo-700' ?> text-white text-sm rounded-lg transition-colors font-medium" title="<?= $stats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
<i class="fas fa-sync-alt mr-2"></i>
Check Updates
</button>
</form>
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
<?= csrf_field() ?>
<input type="hidden" name="import_type" value="complete_workflow">
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
<i class="fas fa-rocket mr-2"></i>
Import TLDs
</button>
</form>
</div>
<?php else: ?>
<div class="mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div class="flex items-center">
<i class="fas fa-info-circle text-yellow-600 mr-2"></i>
<p class="text-sm text-yellow-800">
View-only mode. Contact admin to import or modify TLD data.
</p>
</div>
</div>
<?php endif; ?>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
@@ -103,8 +97,8 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">With RDAP</p>
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['with_rdap'] ?? 0 ?></p>
</div>
<div class="w-12 h-12 bg-purple-50 rounded-lg flex items-center justify-center">
<i class="fas fa-database text-purple-600 text-lg"></i>
<div class="w-12 h-12 bg-indigo-50 rounded-lg flex items-center justify-center">
<i class="fas fa-database text-indigo-600 text-lg"></i>
</div>
</div>
</div>
@@ -200,22 +194,25 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
</form>
</div>
<!-- Bulk Actions (Admin Only) -->
<?php if (!empty($tlds) && isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-4">
<!-- Bulk Actions Toolbar (Admin Only - Hidden by default, shown when TLDs are selected) -->
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<div id="bulk-actions" class="hidden mb-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-600">Bulk Actions:</span>
<div class="flex items-center gap-4">
<span id="selected-count" class="text-sm font-medium text-blue-900"></span>
<form method="POST" action="/tld-registry/bulk-delete" id="bulk-delete-form" class="inline">
<?= csrf_field() ?>
<button type="button" onclick="confirmBulkDelete()" class="inline-flex items-center px-3 py-2 border border-red-300 text-red-700 text-sm rounded-lg hover:bg-red-50 transition-colors font-medium">
<button type="button" onclick="confirmBulkDelete()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>
Delete Selected
</button>
</form>
</div>
<div class="text-sm text-gray-500">
<span id="selected-count">0</span> selected
<button type="button" onclick="clearSelection()" class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-times mr-2"></i>
Clear Selection
</button>
</div>
</div>
</div>
@@ -546,22 +543,46 @@ function toggleAllCheckboxes(selectAllCheckbox) {
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.tld-checkbox:checked');
const count = checkboxes.length;
document.getElementById('selected-count').textContent = count;
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (count > 0) {
bulkActions.classList.remove('hidden');
bulkActions.classList.add('flex');
selectedCount.textContent = `${count} TLD(s) selected`;
} else {
bulkActions.classList.add('hidden');
bulkActions.classList.remove('flex');
}
// Update select all checkbox state
const selectAllCheckbox = document.getElementById('select-all');
const allCheckboxes = document.querySelectorAll('.tld-checkbox');
if (count === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (count === allCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
if (selectAllCheckbox) {
if (count === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (count === allCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
}
}
}
function clearSelection() {
const checkboxes = document.querySelectorAll('.tld-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
updateSelectedCount();
}
function confirmBulkDelete() {
const checkboxes = document.querySelectorAll('.tld-checkbox:checked');
if (checkboxes.length === 0) {

View File

@@ -98,7 +98,7 @@ ob_start();
<div class="space-y-1.5">
<?php foreach ($rdapServers as $index => $server): ?>
<div class="flex items-center p-2 bg-gray-50 rounded hover:bg-gray-100 transition-colors">
<div class="w-6 h-6 bg-purple-500 rounded flex items-center justify-center text-white font-bold text-xs mr-2">
<div class="w-6 h-6 bg-indigo-500 rounded flex items-center justify-center text-white font-bold text-xs mr-2">
<?= $index + 1 ?>
</div>
<p class="font-mono text-xs text-gray-800"><?= htmlspecialchars($server) ?></p>
@@ -168,8 +168,8 @@ ob_start();
<?php endif; ?>
<?php if ($tld['iana_publication_date']): ?>
<div class="flex items-center p-2 bg-purple-50 rounded border border-purple-200">
<div class="w-7 h-7 bg-purple-500 rounded flex items-center justify-center mr-2">
<div class="flex items-center p-2 bg-indigo-50 rounded border border-indigo-200">
<div class="w-7 h-7 bg-indigo-500 rounded flex items-center justify-center mr-2">
<i class="fas fa-calendar text-white text-xs"></i>
</div>
<div>

View File

@@ -38,17 +38,11 @@ $pagination = $pagination ?? [
?>
<!-- Action Buttons -->
<div class="mb-4 flex flex-wrap gap-2 justify-between items-center">
<div class="flex gap-2">
<!-- Placeholder for future bulk actions -->
</div>
<div class="flex gap-2">
<a href="/users/create" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-user-plus mr-2"></i>
Add User
</a>
</div>
<div class="mb-4 flex justify-end">
<a href="/users/create" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-user-plus mr-2"></i>
Add User
</a>
</div>
<!-- Filters & Search -->
@@ -101,6 +95,35 @@ $pagination = $pagination ?? [
</form>
</div>
<!-- Bulk Actions Toolbar (Hidden by default, shown when users are selected) -->
<div id="bulk-actions" class="hidden mb-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span id="selected-count" class="text-sm font-medium text-blue-900"></span>
<button type="button" onclick="bulkToggleStatus('active')" class="inline-flex items-center px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors font-medium">
<i class="fas fa-user-check mr-2"></i>
Activate Selected
</button>
<button type="button" onclick="bulkToggleStatus('inactive')" class="inline-flex items-center px-4 py-2 bg-orange-600 text-white text-sm rounded-lg hover:bg-orange-700 transition-colors font-medium">
<i class="fas fa-user-slash mr-2"></i>
Deactivate Selected
</button>
<button type="button" onclick="bulkDeleteUsers()" class="inline-flex items-center px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
<i class="fas fa-trash mr-2"></i>
Delete Selected
</button>
<button type="button" onclick="clearSelection()" class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
<i class="fas fa-times mr-2"></i>
Clear Selection
</button>
</div>
</div>
</div>
<!-- Pagination Info & Per Page Selector -->
<div class="mb-4 flex justify-between items-center">
<div class="text-sm text-gray-600">
@@ -134,6 +157,9 @@ $pagination = $pagination ?? [
<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">
<a href="<?= sortUrl('full_name', $currentFilters['sort'], $currentFilters['order']) ?>" class="hover:text-primary flex items-center">
User <?= sortIcon('full_name', $currentFilters['sort'], $currentFilters['order']) ?>
@@ -170,6 +196,15 @@ $pagination = $pagination ?? [
<tbody class="bg-white divide-y divide-gray-200">
<?php foreach ($users as $user): ?>
<tr class="hover:bg-gray-50 transition-colors duration-150">
<td class="px-6 py-4">
<?php if ($user['id'] != $_SESSION['user_id']): ?>
<input type="checkbox" class="user-checkbox rounded border-gray-300 text-primary focus:ring-primary" value="<?= $user['id'] ?>" onchange="updateBulkActions()">
<?php else: ?>
<span class="text-gray-300" title="Cannot select your own account">
<i class="fas fa-lock text-xs"></i>
</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
@@ -188,7 +223,7 @@ $pagination = $pagination ?? [
</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 border
<?= $user['role'] === 'admin' ? 'bg-purple-100 text-purple-700 border-purple-200' : 'bg-blue-100 text-blue-700 border-blue-200' ?>">
<?= $user['role'] === 'admin' ? 'bg-amber-100 text-amber-700 border-amber-200' : 'bg-blue-100 text-blue-700 border-blue-200' ?>">
<i class="fas fa-<?= $user['role'] === 'admin' ? 'crown' : 'user' ?> mr-1"></i>
<?= ucfirst($user['role']) ?>
</span>
@@ -348,6 +383,124 @@ $pagination = $pagination ?? [
</div>
<?php endif; ?>
<script>
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.user-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const checkboxes = document.querySelectorAll('.user-checkbox:checked');
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
const selectAllCheckbox = document.getElementById('select-all');
if (checkboxes.length > 0) {
bulkActions.classList.remove('hidden');
bulkActions.classList.add('flex');
selectedCount.textContent = checkboxes.length + ' user(s) selected';
} else {
bulkActions.classList.add('hidden');
bulkActions.classList.remove('flex');
}
// Update select all checkbox state
const allCheckboxes = document.querySelectorAll('.user-checkbox');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
selectAllCheckbox.indeterminate = checkboxes.length > 0 && checkboxes.length < allCheckboxes.length;
}
}
function clearSelection() {
const checkboxes = document.querySelectorAll('.user-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
document.getElementById('select-all').checked = false;
updateBulkActions();
}
function getSelectedUserIds() {
const checkboxes = document.querySelectorAll('.user-checkbox:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
function bulkToggleStatus(action) {
const userIds = getSelectedUserIds();
if (userIds.length === 0) {
alert('Please select at least one user');
return;
}
const actionText = action === 'active' ? 'activate' : 'deactivate';
if (!confirm(`Are you sure you want to ${actionText} ${userIds.length} user(s)?`)) {
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/users/bulk-toggle-status';
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 = 'user_ids';
idsInput.value = JSON.stringify(userIds);
form.appendChild(idsInput);
const actionInput = document.createElement('input');
actionInput.type = 'hidden';
actionInput.name = 'action';
actionInput.value = action;
form.appendChild(actionInput);
document.body.appendChild(form);
form.submit();
}
function bulkDeleteUsers() {
const userIds = getSelectedUserIds();
if (userIds.length === 0) {
alert('Please select at least one user to delete');
return;
}
if (!confirm(`Are you sure you want to delete ${userIds.length} user(s)? This action cannot be undone.`)) {
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/users/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 = 'user_ids';
idsInput.value = JSON.stringify(userIds);
form.appendChild(idsInput);
document.body.appendChild(form);
form.submit();
}
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/../layout/base.php';