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 + + +