Enhance error resolution workflow and notification service
Refactored error log model and views to use a unified 'notes' field instead of 'resolution_notes'. Added a modal dialog for entering resolution notes when marking errors as resolved in admin views. Improved stack trace handling in ErrorHandler by storing as JSON and formatting for display. Expanded NotificationService to support multi-channel notifications (email, Telegram, Discord, Slack), group notifications, and improved domain expiration alerts.
This commit is contained in:
@@ -350,7 +350,7 @@ class ErrorLog extends Model
|
|||||||
SET is_resolved = 1,
|
SET is_resolved = 1,
|
||||||
resolved_at = NOW(),
|
resolved_at = NOW(),
|
||||||
resolved_by = ?,
|
resolved_by = ?,
|
||||||
resolution_notes = ?
|
notes = ?
|
||||||
WHERE error_id = ?
|
WHERE error_id = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ class ErrorLog extends Model
|
|||||||
SET is_resolved = 0,
|
SET is_resolved = 0,
|
||||||
resolved_at = NULL,
|
resolved_at = NULL,
|
||||||
resolved_by = NULL,
|
resolved_by = NULL,
|
||||||
resolution_notes = NULL
|
notes = NULL
|
||||||
WHERE error_id = ?
|
WHERE error_id = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class ErrorHandler
|
|||||||
'error_message' => $exception->getMessage(),
|
'error_message' => $exception->getMessage(),
|
||||||
'error_file' => $exception->getFile(),
|
'error_file' => $exception->getFile(),
|
||||||
'error_line' => $exception->getLine(),
|
'error_line' => $exception->getLine(),
|
||||||
'stack_trace' => $exception->getTraceAsString(),
|
'stack_trace' => json_encode($exception->getTrace()),
|
||||||
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
|
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
|
||||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'N/A',
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'N/A',
|
||||||
'request_data' => json_encode($requestData),
|
'request_data' => json_encode($requestData),
|
||||||
@@ -254,7 +254,11 @@ class ErrorHandler
|
|||||||
$error_message = $errorData['error_message'];
|
$error_message = $errorData['error_message'];
|
||||||
$error_file = $errorData['error_file'];
|
$error_file = $errorData['error_file'];
|
||||||
$error_line = $errorData['error_line'];
|
$error_line = $errorData['error_line'];
|
||||||
$stack_trace = $errorData['stack_trace'];
|
|
||||||
|
// Convert JSON stack trace back to string format for display
|
||||||
|
$traceArray = json_decode($errorData['stack_trace'], true) ?? [];
|
||||||
|
$stack_trace = $this->formatStackTraceAsString($traceArray);
|
||||||
|
|
||||||
$request_method = $errorData['request_method'];
|
$request_method = $errorData['request_method'];
|
||||||
$request_uri = $errorData['request_uri'];
|
$request_uri = $errorData['request_uri'];
|
||||||
$user_agent = $errorData['user_agent'];
|
$user_agent = $errorData['user_agent'];
|
||||||
@@ -293,6 +297,42 @@ class ErrorHandler
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format stack trace array as string (similar to getTraceAsString())
|
||||||
|
*/
|
||||||
|
private function formatStackTraceAsString(array $trace): string
|
||||||
|
{
|
||||||
|
if (empty($trace)) {
|
||||||
|
return 'No stack trace available';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($trace as $index => $frame) {
|
||||||
|
$line = "#{$index} ";
|
||||||
|
|
||||||
|
if (isset($frame['file'])) {
|
||||||
|
$line .= $frame['file'];
|
||||||
|
if (isset($frame['line'])) {
|
||||||
|
$line .= "({$frame['line']})";
|
||||||
|
}
|
||||||
|
$line .= ': ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($frame['class'])) {
|
||||||
|
$line .= $frame['class'];
|
||||||
|
$line .= $frame['type'] ?? '->';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($frame['function'])) {
|
||||||
|
$line .= $frame['function'] . '()';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static helper to register global handlers
|
* Static helper to register global handlers
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,23 +2,166 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Notification;
|
use App\Services\Channels\EmailChannel;
|
||||||
|
use App\Services\Channels\TelegramChannel;
|
||||||
|
use App\Services\Channels\DiscordChannel;
|
||||||
|
use App\Services\Channels\SlackChannel;
|
||||||
|
|
||||||
class NotificationService
|
class NotificationService
|
||||||
{
|
{
|
||||||
private Notification $notificationModel;
|
private array $channels = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->notificationModel = new Notification();
|
$this->channels = [
|
||||||
|
'email' => new EmailChannel(),
|
||||||
|
'telegram' => new TelegramChannel(),
|
||||||
|
'discord' => new DiscordChannel(),
|
||||||
|
'slack' => new SlackChannel(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a domain expiring notification
|
* Send notification to specified channel
|
||||||
|
*/
|
||||||
|
public function send(string $channelType, array $config, string $message, array $data = []): bool
|
||||||
|
{
|
||||||
|
if (!isset($this->channels[$channelType])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->channels[$channelType]->send($config, $message, $data);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Notification send failed [$channelType]: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send notification to all active channels in a group
|
||||||
|
*/
|
||||||
|
public function sendToGroup(int $groupId, string $subject, string $message, array $data = []): array
|
||||||
|
{
|
||||||
|
// Get active channels for the group
|
||||||
|
$channelModel = new \App\Models\NotificationChannel();
|
||||||
|
$channels = $channelModel->getByGroupId($groupId);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
if (!$channel['is_active']) {
|
||||||
|
continue; // Skip inactive channels
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = json_decode($channel['channel_config'], true);
|
||||||
|
|
||||||
|
// Add subject to data for channels that support it (like email)
|
||||||
|
$channelData = array_merge(['subject' => $subject], $data);
|
||||||
|
|
||||||
|
$success = $this->send(
|
||||||
|
$channel['channel_type'],
|
||||||
|
$config,
|
||||||
|
$message,
|
||||||
|
$channelData
|
||||||
|
);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'channel' => $channel['channel_type'],
|
||||||
|
'success' => $success
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send domain expiration notification
|
||||||
|
*/
|
||||||
|
public function sendDomainExpirationAlert(array $domain, array $notificationChannels): array
|
||||||
|
{
|
||||||
|
$daysLeft = $this->calculateDaysLeft($domain['expiration_date']);
|
||||||
|
$message = $this->formatExpirationMessage($domain, $daysLeft);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($notificationChannels as $channel) {
|
||||||
|
$config = json_decode($channel['channel_config'], true);
|
||||||
|
$success = $this->send(
|
||||||
|
$channel['channel_type'],
|
||||||
|
$config,
|
||||||
|
$message,
|
||||||
|
[
|
||||||
|
'domain' => $domain['domain_name'],
|
||||||
|
'domain_id' => $domain['id'],
|
||||||
|
'days_left' => $daysLeft,
|
||||||
|
'expiration_date' => $domain['expiration_date'],
|
||||||
|
'registrar' => $domain['registrar']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'channel' => $channel['channel_type'],
|
||||||
|
'success' => $success
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format expiration message
|
||||||
|
*/
|
||||||
|
private function formatExpirationMessage(array $domain, int $daysLeft): string
|
||||||
|
{
|
||||||
|
$domainName = $domain['domain_name'];
|
||||||
|
$expirationDate = date('F j, Y', strtotime($domain['expiration_date']));
|
||||||
|
$registrar = $domain['registrar'] ?? 'Unknown';
|
||||||
|
|
||||||
|
if ($daysLeft <= 0) {
|
||||||
|
return "🚨 URGENT: Domain '$domainName' has EXPIRED on $expirationDate!\n\n" .
|
||||||
|
"Registrar: $registrar\n" .
|
||||||
|
"Please renew immediately to avoid losing your domain.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($daysLeft == 1) {
|
||||||
|
return "⚠️ CRITICAL: Domain '$domainName' expires TOMORROW ($expirationDate)!\n\n" .
|
||||||
|
"Registrar: $registrar\n" .
|
||||||
|
"Please renew as soon as possible.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($daysLeft <= 7) {
|
||||||
|
return "⚠️ WARNING: Domain '$domainName' expires in $daysLeft days ($expirationDate)!\n\n" .
|
||||||
|
"Registrar: $registrar\n" .
|
||||||
|
"Please renew soon.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ℹ️ REMINDER: Domain '$domainName' expires in $daysLeft days ($expirationDate).\n\n" .
|
||||||
|
"Registrar: $registrar\n" .
|
||||||
|
"Please plan for renewal.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate days left until expiration
|
||||||
|
*/
|
||||||
|
private function calculateDaysLeft(string $expirationDate): int
|
||||||
|
{
|
||||||
|
$expiration = strtotime($expirationDate);
|
||||||
|
$now = time();
|
||||||
|
return (int)floor(($expiration - $now) / 86400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// IN-APP NOTIFICATION METHODS (Bell Icon)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a domain expiring notification (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyDomainExpiring(int $userId, string $domainName, int $daysLeft, int $domainId): void
|
public function notifyDomainExpiring(int $userId, string $domainName, int $daysLeft, int $domainId): void
|
||||||
{
|
{
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel = new \App\Models\Notification();
|
||||||
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'domain_expiring',
|
'domain_expiring',
|
||||||
'Domain Expiring Soon',
|
'Domain Expiring Soon',
|
||||||
@@ -28,11 +171,12 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a domain expired notification
|
* Create a domain expired notification (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyDomainExpired(int $userId, string $domainName, int $domainId): void
|
public function notifyDomainExpired(int $userId, string $domainName, int $domainId): void
|
||||||
{
|
{
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel = new \App\Models\Notification();
|
||||||
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'domain_expired',
|
'domain_expired',
|
||||||
'Domain Expired',
|
'Domain Expired',
|
||||||
@@ -42,15 +186,16 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a domain WHOIS updated notification
|
* Create a domain WHOIS updated notification (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyDomainUpdated(int $userId, string $domainName, int $domainId, string $changes = ''): void
|
public function notifyDomainUpdated(int $userId, string $domainName, int $domainId, string $changes = ''): void
|
||||||
{
|
{
|
||||||
|
$notificationModel = new \App\Models\Notification();
|
||||||
$message = !empty($changes) ?
|
$message = !empty($changes) ?
|
||||||
"{$domainName} - {$changes}" :
|
"{$domainName} - {$changes}" :
|
||||||
"{$domainName} WHOIS data updated";
|
"{$domainName} WHOIS data updated";
|
||||||
|
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'domain_updated',
|
'domain_updated',
|
||||||
'Domain WHOIS Updated',
|
'Domain WHOIS Updated',
|
||||||
@@ -60,15 +205,16 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a WHOIS lookup failed notification
|
* Create a WHOIS lookup failed notification (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyWhoisFailed(int $userId, string $domainName, int $domainId, string $reason = ''): void
|
public function notifyWhoisFailed(int $userId, string $domainName, int $domainId, string $reason = ''): void
|
||||||
{
|
{
|
||||||
|
$notificationModel = new \App\Models\Notification();
|
||||||
$message = !empty($reason) ?
|
$message = !empty($reason) ?
|
||||||
"Could not refresh {$domainName} - {$reason}" :
|
"Could not refresh {$domainName} - {$reason}" :
|
||||||
"Could not refresh {$domainName}";
|
"Could not refresh {$domainName}";
|
||||||
|
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'whois_failed',
|
'whois_failed',
|
||||||
'WHOIS Lookup Failed',
|
'WHOIS Lookup Failed',
|
||||||
@@ -78,11 +224,12 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new login notification
|
* Create a new login notification (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyNewLogin(int $userId, string $location, string $ipAddress): void
|
public function notifyNewLogin(int $userId, string $location, string $ipAddress): void
|
||||||
{
|
{
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel = new \App\Models\Notification();
|
||||||
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'session_new',
|
'session_new',
|
||||||
'New Login Detected',
|
'New Login Detected',
|
||||||
@@ -92,11 +239,12 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create welcome notification for new users/fresh install
|
* Create welcome notification for new users/fresh install (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyWelcome(int $userId, string $username): void
|
public function notifyWelcome(int $userId, string $username): void
|
||||||
{
|
{
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel = new \App\Models\Notification();
|
||||||
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'system_welcome',
|
'system_welcome',
|
||||||
'Welcome to Domain Monitor! 🎉',
|
'Welcome to Domain Monitor! 🎉',
|
||||||
@@ -106,11 +254,12 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create system upgrade notification for admins
|
* Create system upgrade notification for admins (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifySystemUpgrade(int $userId, string $fromVersion, string $toVersion, int $migrationsCount): void
|
public function notifySystemUpgrade(int $userId, string $fromVersion, string $toVersion, int $migrationsCount): void
|
||||||
{
|
{
|
||||||
$this->notificationModel->createNotification(
|
$notificationModel = new \App\Models\Notification();
|
||||||
|
$notificationModel->createNotification(
|
||||||
$userId,
|
$userId,
|
||||||
'system_upgrade',
|
'system_upgrade',
|
||||||
'System Upgraded Successfully',
|
'System Upgraded Successfully',
|
||||||
@@ -120,7 +269,7 @@ class NotificationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify all admins about system upgrade
|
* Notify all admins about system upgrade (in-app)
|
||||||
*/
|
*/
|
||||||
public function notifyAdminsUpgrade(string $fromVersion, string $toVersion, int $migrationsCount): void
|
public function notifyAdminsUpgrade(string $fromVersion, string $toVersion, int $migrationsCount): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro
|
|||||||
<h3 class="text-sm font-semibold text-green-900 mb-2">Resolved</h3>
|
<h3 class="text-sm font-semibold text-green-900 mb-2">Resolved</h3>
|
||||||
<div class="text-sm text-green-800 space-y-1">
|
<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>
|
<p><strong>Date:</strong> <?= date('M d, Y H:i:s', strtotime($error['resolved_at'])) ?></p>
|
||||||
<?php if ($error['resolution_notes']): ?>
|
<?php if (!empty($error['notes'])): ?>
|
||||||
<p><strong>Notes:</strong> <?= htmlspecialchars($error['resolution_notes']) ?></p>
|
<p><strong>Notes:</strong> <?= htmlspecialchars($error['notes']) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,6 +254,57 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Resolution Notes Modal -->
|
||||||
|
<div id="resolutionModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-lg bg-white">
|
||||||
|
<div class="mt-3">
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
|
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
||||||
|
Mark Error as Resolved
|
||||||
|
</h3>
|
||||||
|
<button onclick="closeResolutionModal()" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="resolutionNotes" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Resolution Notes (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="resolutionNotes"
|
||||||
|
rows="4"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
||||||
|
placeholder="Describe how you resolved this error or any relevant notes..."
|
||||||
|
></textarea>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
Add any details about the fix or resolution for future reference.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onclick="closeResolutionModal()"
|
||||||
|
class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="submitResolution()"
|
||||||
|
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check mr-2"></i>
|
||||||
|
Mark as Resolved
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function switchTab(tabName) {
|
function switchTab(tabName) {
|
||||||
// Hide all tab contents
|
// Hide all tab contents
|
||||||
@@ -308,8 +359,16 @@ function showCopySuccess() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markResolved() {
|
function markResolved() {
|
||||||
const notes = prompt('Add resolution notes (optional):');
|
document.getElementById('resolutionModal').classList.remove('hidden');
|
||||||
if (notes === null) return; // User cancelled
|
}
|
||||||
|
|
||||||
|
function closeResolutionModal() {
|
||||||
|
document.getElementById('resolutionModal').classList.add('hidden');
|
||||||
|
document.getElementById('resolutionNotes').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitResolution() {
|
||||||
|
const notes = document.getElementById('resolutionNotes').value;
|
||||||
|
|
||||||
const form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
form.method = 'POST';
|
form.method = 'POST';
|
||||||
|
|||||||
@@ -361,6 +361,57 @@ $currentFilters = $filters ?? ['resolved' => '', 'type' => '', 'sort' => 'last_o
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Resolution Notes Modal -->
|
||||||
|
<div id="resolutionModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-lg bg-white">
|
||||||
|
<div class="mt-3">
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
|
<i class="fas fa-check-circle text-green-600 mr-2"></i>
|
||||||
|
Mark Error as Resolved
|
||||||
|
</h3>
|
||||||
|
<button onclick="closeResolutionModal()" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="resolutionNotes" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Resolution Notes (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="resolutionNotes"
|
||||||
|
rows="4"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
||||||
|
placeholder="Describe how you resolved this error or any relevant notes..."
|
||||||
|
></textarea>
|
||||||
|
<p class="mt-1 text-xs text-gray-500">
|
||||||
|
Add any details about the fix or resolution for future reference.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onclick="closeResolutionModal()"
|
||||||
|
class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="submitResolution()"
|
||||||
|
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check mr-2"></i>
|
||||||
|
Mark as Resolved
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function copyToClipboard(text) {
|
function copyToClipboard(text) {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
@@ -393,13 +444,27 @@ function showCopySuccess() {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentErrorId = null;
|
||||||
|
|
||||||
function markResolved(errorId) {
|
function markResolved(errorId) {
|
||||||
const notes = prompt('Add resolution notes (optional):');
|
currentErrorId = errorId;
|
||||||
if (notes === null) return; // User cancelled
|
document.getElementById('resolutionModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResolutionModal() {
|
||||||
|
document.getElementById('resolutionModal').classList.add('hidden');
|
||||||
|
document.getElementById('resolutionNotes').value = '';
|
||||||
|
currentErrorId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitResolution() {
|
||||||
|
if (!currentErrorId) return;
|
||||||
|
|
||||||
|
const notes = document.getElementById('resolutionNotes').value;
|
||||||
|
|
||||||
const form = document.createElement('form');
|
const form = document.createElement('form');
|
||||||
form.method = 'POST';
|
form.method = 'POST';
|
||||||
form.action = '/errors/' + errorId + '/resolve';
|
form.action = '/errors/' + currentErrorId + '/resolve';
|
||||||
|
|
||||||
const csrfInput = document.createElement('input');
|
const csrfInput = document.createElement('input');
|
||||||
csrfInput.type = 'hidden';
|
csrfInput.type = 'hidden';
|
||||||
|
|||||||
Reference in New Issue
Block a user