From dcb7f685dded2bcd0df743810095a71ab72da2e8 Mon Sep 17 00:00:00 2001 From: Hosteroid Date: Sat, 11 Oct 2025 20:27:46 +0300 Subject: [PATCH] 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. --- app/Models/ErrorLog.php | 4 +- app/Services/ErrorHandler.php | 44 ++++++- app/Services/NotificationService.php | 185 ++++++++++++++++++++++++--- app/Views/errors/admin-detail.php | 67 +++++++++- app/Views/errors/admin-index.php | 71 +++++++++- 5 files changed, 342 insertions(+), 29 deletions(-) diff --git a/app/Models/ErrorLog.php b/app/Models/ErrorLog.php index f125adc..13113a0 100644 --- a/app/Models/ErrorLog.php +++ b/app/Models/ErrorLog.php @@ -350,7 +350,7 @@ class ErrorLog extends Model SET is_resolved = 1, resolved_at = NOW(), resolved_by = ?, - resolution_notes = ? + notes = ? WHERE error_id = ? "); @@ -367,7 +367,7 @@ class ErrorLog extends Model SET is_resolved = 0, resolved_at = NULL, resolved_by = NULL, - resolution_notes = NULL + notes = NULL WHERE error_id = ? "); diff --git a/app/Services/ErrorHandler.php b/app/Services/ErrorHandler.php index f06dd5c..c3809d2 100644 --- a/app/Services/ErrorHandler.php +++ b/app/Services/ErrorHandler.php @@ -114,7 +114,7 @@ class ErrorHandler 'error_message' => $exception->getMessage(), 'error_file' => $exception->getFile(), 'error_line' => $exception->getLine(), - 'stack_trace' => $exception->getTraceAsString(), + 'stack_trace' => json_encode($exception->getTrace()), 'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI', 'request_uri' => $_SERVER['REQUEST_URI'] ?? 'N/A', 'request_data' => json_encode($requestData), @@ -254,7 +254,11 @@ class ErrorHandler $error_message = $errorData['error_message']; $error_file = $errorData['error_file']; $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_uri = $errorData['request_uri']; $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 */ diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index acdc0f6..7badf6a 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -2,23 +2,166 @@ 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 { - private Notification $notificationModel; + private array $channels = []; 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 { - $this->notificationModel->createNotification( + $notificationModel = new \App\Models\Notification(); + $notificationModel->createNotification( $userId, 'domain_expiring', '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 { - $this->notificationModel->createNotification( + $notificationModel = new \App\Models\Notification(); + $notificationModel->createNotification( $userId, '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 { + $notificationModel = new \App\Models\Notification(); $message = !empty($changes) ? "{$domainName} - {$changes}" : "{$domainName} WHOIS data updated"; - $this->notificationModel->createNotification( + $notificationModel->createNotification( $userId, 'domain_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 { + $notificationModel = new \App\Models\Notification(); $message = !empty($reason) ? "Could not refresh {$domainName} - {$reason}" : "Could not refresh {$domainName}"; - $this->notificationModel->createNotification( + $notificationModel->createNotification( $userId, 'whois_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 { - $this->notificationModel->createNotification( + $notificationModel = new \App\Models\Notification(); + $notificationModel->createNotification( $userId, 'session_new', '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 { - $this->notificationModel->createNotification( + $notificationModel = new \App\Models\Notification(); + $notificationModel->createNotification( $userId, 'system_welcome', '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 { - $this->notificationModel->createNotification( + $notificationModel = new \App\Models\Notification(); + $notificationModel->createNotification( $userId, 'system_upgrade', '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 { diff --git a/app/Views/errors/admin-detail.php b/app/Views/errors/admin-detail.php index 6f03597..8508d45 100644 --- a/app/Views/errors/admin-detail.php +++ b/app/Views/errors/admin-detail.php @@ -107,8 +107,8 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro

Resolved

Date:

- -

Notes:

+ +

Notes:

@@ -254,6 +254,57 @@ $errorTypeShort = substr(strrchr($error['error_type'], '\\'), 1) ?: $error['erro + + +