From 68389cec3911ebfea583700b39610e9a13ed9d8f Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:54:43 -0600 Subject: [PATCH 01/13] refactored database and webhook notifications --- .../SendSpeedtestCompletedNotification.php | 34 ----- .../SendSpeedtestThresholdNotification.php | 101 ------------- app/Listeners/ProcessCompletedSpeedtest.php | 109 +++++++++++++- app/Listeners/ProcessFailedSpeedtest.php | 13 +- .../ProcessSpeedtestBenchmarkFailed.php | 142 ++++++++++++++++++ .../SendSpeedtestCompletedNotification.php | 54 ------- .../SendSpeedtestThresholdNotification.php | 126 ---------------- lang/en/results.php | 2 + 8 files changed, 256 insertions(+), 325 deletions(-) delete mode 100644 app/Listeners/Database/SendSpeedtestCompletedNotification.php delete mode 100644 app/Listeners/Database/SendSpeedtestThresholdNotification.php create mode 100644 app/Listeners/ProcessSpeedtestBenchmarkFailed.php delete mode 100644 app/Listeners/Webhook/SendSpeedtestCompletedNotification.php delete mode 100644 app/Listeners/Webhook/SendSpeedtestThresholdNotification.php diff --git a/app/Listeners/Database/SendSpeedtestCompletedNotification.php b/app/Listeners/Database/SendSpeedtestCompletedNotification.php deleted file mode 100644 index acaaf9d42..000000000 --- a/app/Listeners/Database/SendSpeedtestCompletedNotification.php +++ /dev/null @@ -1,34 +0,0 @@ -database_enabled) { - return; - } - - if (! $notificationSettings->database_on_speedtest_run) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title(__('results.speedtest_completed')) - ->success() - ->sendToDatabase($user); - } - } -} diff --git a/app/Listeners/Database/SendSpeedtestThresholdNotification.php b/app/Listeners/Database/SendSpeedtestThresholdNotification.php deleted file mode 100644 index a998faff5..000000000 --- a/app/Listeners/Database/SendSpeedtestThresholdNotification.php +++ /dev/null @@ -1,101 +0,0 @@ -database_enabled) { - return; - } - - if (! $notificationSettings->database_on_threshold_failure) { - return; - } - - $thresholdSettings = new ThresholdSettings; - - if (! $thresholdSettings->absolute_enabled) { - return; - } - - if ($thresholdSettings->absolute_download > 0) { - $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings); - } - - if ($thresholdSettings->absolute_upload > 0) { - $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings); - } - - if ($thresholdSettings->absolute_ping > 0) { - $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings); - } - } - - /** - * Send database notification if absolute download threshold is breached. - */ - protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): void - { - if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title(__('results.download_threshold_breached')) - ->body('Speedtest #'.$event->result->id.' breached the download threshold of '.$thresholdSettings->absolute_download.' Mbps at '.Number::toBitRate($event->result->download_bits).'.') - ->warning() - ->sendToDatabase($user); - } - } - - /** - * Send database notification if absolute upload threshold is breached. - */ - protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): void - { - if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title(__('results.upload_threshold_breached')) - ->body('Speedtest #'.$event->result->id.' breached the upload threshold of '.$thresholdSettings->absolute_upload.' Mbps at '.Number::toBitRate($event->result->upload_bits).'.') - ->warning() - ->sendToDatabase($user); - } - } - - /** - * Send database notification if absolute upload threshold is breached. - */ - protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): void - { - if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title(__('results.ping_threshold_breached')) - ->body('Speedtest #'.$event->result->id.' breached the ping threshold of '.$thresholdSettings->absolute_ping.'ms at '.$event->result->ping.'ms.') - ->warning() - ->sendToDatabase($user); - } - } -} diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 01bf317e6..209f8f7c6 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -5,11 +5,19 @@ use App\Events\SpeedtestCompleted; use App\Models\Result; use App\Models\User; +use App\Settings\NotificationSettings; use Filament\Actions\Action; use Filament\Notifications\Notification; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Log; +use Spatie\WebhookServer\WebhookCall; class ProcessCompletedSpeedtest { + public function __construct( + public NotificationSettings $notificationSettings, + ) {} + /** * Handle the event. */ @@ -17,8 +25,48 @@ public function handle(SpeedtestCompleted $event): void { $result = $event->result; - if ($result->dispatched_by && ! $result->scheduled) { - $this->notifyDispatchingUser($result); + $result->loadMissing(['dispatchedBy']); + + // $this->notifyAppriseChannels($result); + $this->notifyDatabaseChannels($result); + $this->notifyDispatchingUser($result); + // $this->notifyMailChannels($result); + $this->notifyWebhookChannels($result); + } + + /** + * Notify Apprise channels. + */ + private function notifyAppriseChannels(Result $result): void + { + // + } + + /** + * Notify database channels. + */ + private function notifyDatabaseChannels(Result $result): void + { + // Don't send database notification if dispatched by a user or test is unhealthy. + if (filled($result->dispatched_by) || ! $result->healthy) { + return; + } + + // Check if database notifications are enabled. + if (! $this->notificationSettings->database_enabled || ! $this->notificationSettings->database_on_speedtest_run) { + return; + } + + foreach (User::all() as $user) { + Notification::make() + ->title(__('results.speedtest_completed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->success() + ->sendToDatabase($user); } } @@ -27,9 +75,11 @@ public function handle(SpeedtestCompleted $event): void */ private function notifyDispatchingUser(Result $result): void { - $user = User::find($result->dispatched_by); + if (empty($result->dispatched_by) || ! $result->healthy) { + return; + } - $user->notify( + $result->dispatchedBy->notify( Notification::make() ->title(__('results.speedtest_completed')) ->actions([ @@ -41,4 +91,55 @@ private function notifyDispatchingUser(Result $result): void ->toDatabase(), ); } + + /** + * Notify mail channels. + */ + private function notifyMailChannels(Result $result): void + { + // + } + + /** + * Notify webhook channels. + */ + private function notifyWebhookChannels(Result $result): void + { + // Don't send webhook if dispatched by a user or test is unhealthy. + if (filled($result->dispatched_by) || ! $result->healthy) { + return; + } + + // Check if webhook notifications are enabled. + if (! $this->notificationSettings->webhook_enabled || ! $this->notificationSettings->webhook_on_speedtest_run) { + return; + } + + // Check if webhook urls are configured. + if (! count($this->notificationSettings->webhook_urls)) { + Log::warning('Webhook urls not found, check webhook notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->webhook_urls as $url) { + WebhookCall::create() + ->url($url['url']) + ->payload([ + 'result_id' => $result->id, + 'site_name' => config('app.name'), + 'server_name' => Arr::get($result->data, 'server.name'), + 'server_id' => Arr::get($result->data, 'server.id'), + 'isp' => Arr::get($result->data, 'isp'), + 'ping' => $result->ping, + 'download' => $result->downloadBits, + 'upload' => $result->uploadBits, + 'packet_loss' => Arr::get($result->data, 'packetLoss'), + 'speedtest_url' => Arr::get($result->data, 'result.url'), + 'url' => url('/admin/results'), + ]) + ->doNotSign() + ->dispatch(); + } + } } diff --git a/app/Listeners/ProcessFailedSpeedtest.php b/app/Listeners/ProcessFailedSpeedtest.php index d11e5e7a0..3363a3a33 100644 --- a/app/Listeners/ProcessFailedSpeedtest.php +++ b/app/Listeners/ProcessFailedSpeedtest.php @@ -4,7 +4,6 @@ use App\Events\SpeedtestFailed; use App\Models\Result; -use App\Models\User; use Filament\Actions\Action; use Filament\Notifications\Notification; @@ -17,9 +16,9 @@ public function handle(SpeedtestFailed $event): void { $result = $event->result; - if ($result->dispatched_by && ! $result->scheduled) { - $this->notifyDispatchingUser($result); - } + $result->loadMissing(['dispatchedBy']); + + $this->notifyDispatchingUser($result); } /** @@ -27,9 +26,11 @@ public function handle(SpeedtestFailed $event): void */ private function notifyDispatchingUser(Result $result): void { - $user = User::find($result->dispatched_by); + if (empty($result->dispatched_by)) { + return; + } - $user->notify( + $result->dispatchedBy->notify( Notification::make() ->title(__('results.speedtest_failed')) ->actions([ diff --git a/app/Listeners/ProcessSpeedtestBenchmarkFailed.php b/app/Listeners/ProcessSpeedtestBenchmarkFailed.php new file mode 100644 index 000000000..6e9353a6c --- /dev/null +++ b/app/Listeners/ProcessSpeedtestBenchmarkFailed.php @@ -0,0 +1,142 @@ +result; + + $result->loadMissing(['dispatchedBy']); + + // $this->notifyAppriseChannels($result); + $this->notifyDatabaseChannels($result); + $this->notifyDispatchingUser($result); + // $this->notifyMailChannels($result); + $this->notifyWebhookChannels($result); + } + + /** + * Notify Apprise channels. + */ + private function notifyAppriseChannels(Result $result): void + { + // + } + + /** + * Notify database channels. + */ + private function notifyDatabaseChannels(Result $result): void + { + // Don't send database notification if dispatched by a user. + if (filled($result->dispatched_by)) { + return; + } + + // Check if database notifications are enabled. + if (! $this->notificationSettings->database_enabled || ! $this->notificationSettings->database_on_threshold_failure) { + return; + } + + foreach (User::all() as $user) { + Notification::make() + ->title(__('results.speedtest_benchmark_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->success() + ->sendToDatabase($user); + } + } + + /** + * Notify the user who dispatched the speedtest. + */ + private function notifyDispatchingUser(Result $result): void + { + if (empty($result->dispatched_by)) { + return; + } + + $result->dispatchedBy->notify( + Notification::make() + ->title(__('results.speedtest_benchmark_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->warning() + ->toDatabase(), + ); + } + + /** + * Notify mail channels. + */ + private function notifyMailChannels(Result $result): void + { + // + } + + /** + * Notify webhook channels. + */ + private function notifyWebhookChannels(Result $result): void + { + // Don't send webhook if dispatched by a user. + if (filled($result->dispatched_by)) { + return; + } + + // Check if webhook notifications are enabled. + if (! $this->notificationSettings->webhook_enabled || ! $this->notificationSettings->webhook_on_threshold_failure) { + return; + } + + // Check if webhook urls are configured. + if (! count($this->notificationSettings->webhook_urls)) { + Log::warning('Webhook urls not found, check webhook notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->webhook_urls as $url) { + WebhookCall::create() + ->url($url['url']) + ->payload([ + 'result_id' => $result->id, + 'site_name' => config('app.name'), + 'isp' => $result->isp, + 'benchmarks' => $result->benchmarks, + 'speedtest_url' => $result->result_url, + 'url' => url('/admin/results'), + ]) + ->doNotSign() + ->dispatch(); + } + } +} diff --git a/app/Listeners/Webhook/SendSpeedtestCompletedNotification.php b/app/Listeners/Webhook/SendSpeedtestCompletedNotification.php deleted file mode 100644 index bee0668d6..000000000 --- a/app/Listeners/Webhook/SendSpeedtestCompletedNotification.php +++ /dev/null @@ -1,54 +0,0 @@ -webhook_enabled) { - return; - } - - if (! $notificationSettings->webhook_on_speedtest_run) { - return; - } - - if (! count($notificationSettings->webhook_urls)) { - Log::warning('Webhook urls not found, check webhook notification channel settings.'); - - return; - } - - foreach ($notificationSettings->webhook_urls as $url) { - WebhookCall::create() - ->url($url['url']) - ->payload([ - 'result_id' => $event->result->id, - 'site_name' => config('app.name'), - 'server_name' => Arr::get($event->result->data, 'server.name'), - 'server_id' => Arr::get($event->result->data, 'server.id'), - 'isp' => Arr::get($event->result->data, 'isp'), - 'ping' => $event->result->ping, - 'download' => $event->result->downloadBits, - 'upload' => $event->result->uploadBits, - 'packet_loss' => Arr::get($event->result->data, 'packetLoss'), - 'speedtest_url' => Arr::get($event->result->data, 'result.url'), - 'url' => url('/admin/results'), - ]) - ->doNotSign() - ->dispatch(); - } - } -} diff --git a/app/Listeners/Webhook/SendSpeedtestThresholdNotification.php b/app/Listeners/Webhook/SendSpeedtestThresholdNotification.php deleted file mode 100644 index bb64866b4..000000000 --- a/app/Listeners/Webhook/SendSpeedtestThresholdNotification.php +++ /dev/null @@ -1,126 +0,0 @@ -webhook_enabled) { - return; - } - - if (! $notificationSettings->webhook_on_threshold_failure) { - return; - } - - if (! count($notificationSettings->webhook_urls)) { - Log::warning('Webhook urls not found, check webhook notification channel settings.'); - - return; - } - - $thresholdSettings = new ThresholdSettings; - - if (! $thresholdSettings->absolute_enabled) { - return; - } - - $failed = []; - - if ($thresholdSettings->absolute_download > 0) { - array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_upload > 0) { - array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_ping > 0) { - array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - $failed = array_filter($failed); - - if (! count($failed)) { - Log::warning('Failed webhook thresholds not found, won\'t send notification.'); - - return; - } - - foreach ($notificationSettings->webhook_urls as $url) { - WebhookCall::create() - ->url($url['url']) - ->payload([ - 'result_id' => $event->result->id, - 'site_name' => config('app.name'), - 'isp' => $event->result->isp, - 'metrics' => $failed, - 'speedtest_url' => $event->result->result_url, - 'url' => url('/admin/results'), - ]) - ->doNotSign() - ->dispatch(); - } - } - - /** - * Build webhook notification if absolute download threshold is breached. - */ - protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { - return false; - } - - return [ - 'name' => 'Download', - 'threshold' => $thresholdSettings->absolute_download.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2), - ]; - } - - /** - * Build webhook notification if absolute upload threshold is breached. - */ - protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { - return false; - } - - return [ - 'name' => 'Upload', - 'threshold' => $thresholdSettings->absolute_upload.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2), - ]; - } - - /** - * Build webhook notification if absolute ping threshold is breached. - */ - protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { - return false; - } - - return [ - 'name' => 'Ping', - 'threshold' => $thresholdSettings->absolute_ping.' ms', - 'value' => round($event->result->ping, 2).' ms', - ]; - } -} diff --git a/lang/en/results.php b/lang/en/results.php index f59df96d8..6d8a2016d 100644 --- a/lang/en/results.php +++ b/lang/en/results.php @@ -61,6 +61,8 @@ 'view_on_speedtest_net' => 'View on Speedtest.net', // Notifications + 'speedtest_benchmark_passed' => 'Speedtest benchmark passed', + 'speedtest_benchmark_failed' => 'Speedtest benchmark failed', 'speedtest_started' => 'Speedtest started', 'speedtest_completed' => 'Speedtest completed', 'speedtest_failed' => 'Speedtest failed', From 604e381787733cf369b4e6bab609313cacf040f9 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:08:02 -0600 Subject: [PATCH 02/13] added Apprise notification placeholders --- app/Listeners/ProcessCompletedSpeedtest.php | 5 +++++ app/Listeners/ProcessFailedSpeedtest.php | 14 ++++++++++++++ app/Listeners/ProcessSpeedtestBenchmarkFailed.php | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 209f8f7c6..48b6772cc 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -39,6 +39,11 @@ public function handle(SpeedtestCompleted $event): void */ private function notifyAppriseChannels(Result $result): void { + // Don't send Apprise notification if dispatched by a user or test is unhealthy. + if (filled($result->dispatched_by) || ! $result->healthy) { + return; + } + // } diff --git a/app/Listeners/ProcessFailedSpeedtest.php b/app/Listeners/ProcessFailedSpeedtest.php index 3363a3a33..8ba154387 100644 --- a/app/Listeners/ProcessFailedSpeedtest.php +++ b/app/Listeners/ProcessFailedSpeedtest.php @@ -18,9 +18,23 @@ public function handle(SpeedtestFailed $event): void $result->loadMissing(['dispatchedBy']); + // $this->notifyAppriseChannels($result); $this->notifyDispatchingUser($result); } + /** + * Notify Apprise channels. + */ + private function notifyAppriseChannels(Result $result): void + { + // Don't send Apprise notification if dispatched by a user or test is unhealthy. + if (filled($result->dispatched_by) || ! $result->healthy) { + return; + } + + // + } + /** * Notify the user who dispatched the speedtest. */ diff --git a/app/Listeners/ProcessSpeedtestBenchmarkFailed.php b/app/Listeners/ProcessSpeedtestBenchmarkFailed.php index 6e9353a6c..8f3a1076b 100644 --- a/app/Listeners/ProcessSpeedtestBenchmarkFailed.php +++ b/app/Listeners/ProcessSpeedtestBenchmarkFailed.php @@ -41,6 +41,11 @@ public function handle(SpeedtestBenchmarkFailed $event): void */ private function notifyAppriseChannels(Result $result): void { + // Don't send Apprise notification if dispatched by a user. + if (filled($result->dispatched_by)) { + return; + } + // } From 7a19d357d20ad799ce3059087c8409a534f89ee9 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:08:43 -0600 Subject: [PATCH 03/13] refactored mail notification channel --- .../SendSpeedtestCompletedNotification.php | 39 ------ .../SendSpeedtestThresholdNotification.php | 117 ------------------ app/Listeners/ProcessCompletedSpeedtest.php | 23 +++- ...iled.php => ProcessUnhealthySpeedtest.php} | 28 ++++- app/Mail/SpeedtestThresholdMail.php | 57 --------- app/Mail/UnhealthySpeedtestMail.php | 83 +++++++++++++ .../emails/speedtest-threshold.blade.php | 24 ---- .../views/mail/speedtest/unhealthy.blade.php | 20 +++ 8 files changed, 149 insertions(+), 242 deletions(-) delete mode 100644 app/Listeners/Mail/SendSpeedtestCompletedNotification.php delete mode 100644 app/Listeners/Mail/SendSpeedtestThresholdNotification.php rename app/Listeners/{ProcessSpeedtestBenchmarkFailed.php => ProcessUnhealthySpeedtest.php} (81%) delete mode 100644 app/Mail/SpeedtestThresholdMail.php create mode 100644 app/Mail/UnhealthySpeedtestMail.php delete mode 100644 resources/views/emails/speedtest-threshold.blade.php create mode 100644 resources/views/mail/speedtest/unhealthy.blade.php diff --git a/app/Listeners/Mail/SendSpeedtestCompletedNotification.php b/app/Listeners/Mail/SendSpeedtestCompletedNotification.php deleted file mode 100644 index 2e731cd99..000000000 --- a/app/Listeners/Mail/SendSpeedtestCompletedNotification.php +++ /dev/null @@ -1,39 +0,0 @@ -mail_enabled) { - return; - } - - if (! $notificationSettings->mail_on_speedtest_run) { - return; - } - - if (! count($notificationSettings->mail_recipients)) { - Log::warning('Mail recipients not found, check mail notification channel settings.'); - - return; - } - - foreach ($notificationSettings->mail_recipients as $recipient) { - Mail::to($recipient) - ->send(new SpeedtestCompletedMail($event->result)); - } - } -} diff --git a/app/Listeners/Mail/SendSpeedtestThresholdNotification.php b/app/Listeners/Mail/SendSpeedtestThresholdNotification.php deleted file mode 100644 index 774851df5..000000000 --- a/app/Listeners/Mail/SendSpeedtestThresholdNotification.php +++ /dev/null @@ -1,117 +0,0 @@ -mail_enabled) { - return; - } - - if (! $notificationSettings->mail_on_threshold_failure) { - return; - } - - if (! count($notificationSettings->mail_recipients) > 0) { - Log::warning('Mail recipients not found, check mail notification channel settings.'); - - return; - } - - $thresholdSettings = new ThresholdSettings; - - if (! $thresholdSettings->absolute_enabled) { - return; - } - - $failed = []; - - if ($thresholdSettings->absolute_download > 0) { - array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_upload > 0) { - array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_ping > 0) { - array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - $failed = array_filter($failed); - - if (! count($failed)) { - Log::warning('Failed mail thresholds not found, won\'t send notification.'); - - return; - } - - foreach ($notificationSettings->mail_recipients as $recipient) { - Mail::to($recipient) - ->send(new SpeedtestThresholdMail($event->result, $failed)); - } - } - - /** - * Build mail notification if absolute download threshold is breached. - */ - protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { - return false; - } - - return [ - 'name' => 'Download', - 'threshold' => $thresholdSettings->absolute_download.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2), - ]; - } - - /** - * Build mail notification if absolute upload threshold is breached. - */ - protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { - return false; - } - - return [ - 'name' => 'Upload', - 'threshold' => $thresholdSettings->absolute_upload.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2), - ]; - } - - /** - * Build mail notification if absolute ping threshold is breached. - */ - protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { - return false; - } - - return [ - 'name' => 'Ping', - 'threshold' => $thresholdSettings->absolute_ping.' ms', - 'value' => round($event->result->ping, 2).' ms', - ]; - } -} diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 48b6772cc..2e7474a1b 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -3,6 +3,7 @@ namespace App\Listeners; use App\Events\SpeedtestCompleted; +use App\Mail\SpeedtestCompletedMail; use App\Models\Result; use App\Models\User; use App\Settings\NotificationSettings; @@ -10,6 +11,7 @@ use Filament\Notifications\Notification; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; use Spatie\WebhookServer\WebhookCall; class ProcessCompletedSpeedtest @@ -30,7 +32,7 @@ public function handle(SpeedtestCompleted $event): void // $this->notifyAppriseChannels($result); $this->notifyDatabaseChannels($result); $this->notifyDispatchingUser($result); - // $this->notifyMailChannels($result); + $this->notifyMailChannels($result); $this->notifyWebhookChannels($result); } @@ -102,7 +104,24 @@ private function notifyDispatchingUser(Result $result): void */ private function notifyMailChannels(Result $result): void { - // + if (empty($result->dispatched_by) || ! $result->healthy) { + return; + } + + if (! $this->notificationSettings->mail_enabled || ! $this->notificationSettings->mail_on_speedtest_run) { + return; + } + + if (! count($this->notificationSettings->mail_recipients)) { + Log::warning('Mail recipients not found, check mail notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->mail_recipients as $recipient) { + Mail::to($recipient) + ->send(new SpeedtestCompletedMail($result)); + } } /** diff --git a/app/Listeners/ProcessSpeedtestBenchmarkFailed.php b/app/Listeners/ProcessUnhealthySpeedtest.php similarity index 81% rename from app/Listeners/ProcessSpeedtestBenchmarkFailed.php rename to app/Listeners/ProcessUnhealthySpeedtest.php index 8f3a1076b..5a6d0e057 100644 --- a/app/Listeners/ProcessSpeedtestBenchmarkFailed.php +++ b/app/Listeners/ProcessUnhealthySpeedtest.php @@ -3,15 +3,17 @@ namespace App\Listeners; use App\Events\SpeedtestBenchmarkFailed; +use App\Mail\UnhealthySpeedtestMail; use App\Models\Result; use App\Models\User; use App\Settings\NotificationSettings; use Filament\Actions\Action; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; use Spatie\WebhookServer\WebhookCall; -class ProcessSpeedtestBenchmarkFailed +class ProcessUnhealthySpeedtest { /** * Create the event listener. @@ -32,7 +34,7 @@ public function handle(SpeedtestBenchmarkFailed $event): void // $this->notifyAppriseChannels($result); $this->notifyDatabaseChannels($result); $this->notifyDispatchingUser($result); - // $this->notifyMailChannels($result); + $this->notifyMailChannels($result); $this->notifyWebhookChannels($result); } @@ -104,7 +106,27 @@ private function notifyDispatchingUser(Result $result): void */ private function notifyMailChannels(Result $result): void { - // + // Don't send webhook if dispatched by a user. + if (filled($result->dispatched_by)) { + return; + } + + // Check if mail notifications are enabled. + if (! $this->notificationSettings->mail_enabled || ! $this->notificationSettings->mail_on_threshold_failure) { + return; + } + + // Check if mail recipients are configured. + if (! count($this->notificationSettings->mail_recipients)) { + Log::warning('Mail recipients not found, check mail notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->mail_recipients as $recipient) { + Mail::to($recipient) + ->send(new UnhealthySpeedtestMail($result)); + } } /** diff --git a/app/Mail/SpeedtestThresholdMail.php b/app/Mail/SpeedtestThresholdMail.php deleted file mode 100644 index 94ad14af9..000000000 --- a/app/Mail/SpeedtestThresholdMail.php +++ /dev/null @@ -1,57 +0,0 @@ -result->id, - ); - } - - /** - * Get the message content definition. - */ - public function content(): Content - { - return new Content( - markdown: 'emails.speedtest-threshold', - with: [ - 'id' => $this->result->id, - 'service' => Str::title($this->result->service->getLabel()), - 'serverName' => $this->result->server_name, - 'serverId' => $this->result->server_id, - 'isp' => $this->result->isp, - 'speedtest_url' => $this->result->result_url, - 'url' => url('/admin/results'), - 'metrics' => $this->metrics, - ], - ); - } -} diff --git a/app/Mail/UnhealthySpeedtestMail.php b/app/Mail/UnhealthySpeedtestMail.php new file mode 100644 index 000000000..29bb302d4 --- /dev/null +++ b/app/Mail/UnhealthySpeedtestMail.php @@ -0,0 +1,83 @@ +result->id, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + $benchmarks = []; + + foreach ($this->result->benchmarks as $metric => $benchmark) { + $benchmarks[] = $this->formatBenchmark($metric, $benchmark); + } + + return new Content( + markdown: 'mail.speedtest.unhealthy', + with: [ + 'id' => $this->result->id, + 'service' => str($this->result->service->getLabel())->title(), + 'isp' => $this->result->isp, + 'url' => url('/admin/results'), + 'benchmarks' => $benchmarks, + ], + ); + } + + /** + * Format a benchmark for display in the email. + */ + private function formatBenchmark(string $metric, array $benchmark): array + { + $metricName = str($metric)->title(); + $type = str($benchmark['type'])->title(); + $thresholdValue = $benchmark['value']. ' ' . str($benchmark['unit'])->title(); + + // Get the actual result value + $resultValue = match ($metric) { + 'download' => Number::toBitRate($this->result->download_bits, 2), + 'upload' => Number::toBitRate($this->result->upload_bits, 2), + 'ping' => round(Number::castToType($this->result->ping, 'float'), 2).' ms', + default => 'N/A', + }; + + return [ + 'metric' => $metricName, + 'type' => $type, + 'threshold_value' => $thresholdValue, + 'result_value' => $resultValue, + 'passed' => $benchmark['passed'], + ]; + } +} diff --git a/resources/views/emails/speedtest-threshold.blade.php b/resources/views/emails/speedtest-threshold.blade.php deleted file mode 100644 index 55bdaff31..000000000 --- a/resources/views/emails/speedtest-threshold.blade.php +++ /dev/null @@ -1,24 +0,0 @@ - -# Speedtest Threshold Breached - #{{ $id }} - -A new speedtest was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. - - -| **Metric** | **Threshold** | **Value** | -|:-----------|:--------------|----------:| -@foreach ($metrics as $item) -| {{ $item['name'] }} | {{ $item['threshold'] }} | {{ $item['value'] }} | -@endforeach - - - -View Results - - - -View Results on Ookla - - -Thanks,
-{{ config('app.name') }} -
diff --git a/resources/views/mail/speedtest/unhealthy.blade.php b/resources/views/mail/speedtest/unhealthy.blade.php new file mode 100644 index 000000000..faa9c836e --- /dev/null +++ b/resources/views/mail/speedtest/unhealthy.blade.php @@ -0,0 +1,20 @@ + +# Speedtest Threshold Breached - #{{ $id }} + +A new speedtest was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. + + +| **Metric** | **Type** | **Threshold Value** | **Result Value** | **Status** | +|:-----------|:---------|:--------------------|:-----------------|:---------:| +@foreach ($benchmarks as $benchmark) +| {{ $benchmark['metric'] }} | {{ $benchmark['type'] }} | {{ $benchmark['threshold_value'] }} | {{ $benchmark['result_value'] }} | {{ $benchmark['passed'] ? '✅' : '❌' }} | +@endforeach + + + +{{ __('general.view') }} + + +Thanks,
+{{ config('app.name') }} +
From 354af072647a87aeba8bac67019a53a3a7c44dad Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:14:58 -0600 Subject: [PATCH 04/13] remove footer from unhealthy speedtest notification email --- resources/views/mail/speedtest/unhealthy.blade.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/views/mail/speedtest/unhealthy.blade.php b/resources/views/mail/speedtest/unhealthy.blade.php index faa9c836e..f5934fd1e 100644 --- a/resources/views/mail/speedtest/unhealthy.blade.php +++ b/resources/views/mail/speedtest/unhealthy.blade.php @@ -14,7 +14,4 @@ {{ __('general.view') }} - -Thanks,
-{{ config('app.name') }} From 5c8e25777b4bdcf37c51a7c8ae25f5eb7e86a41a Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:58:28 -0600 Subject: [PATCH 05/13] add database notifications polling interval to admin panel --- app/Providers/Filament/AdminPanelProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index aa8e024ff..c1fe7d2d1 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -40,6 +40,7 @@ public function panel(Panel $panel): Panel ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([]) ->databaseNotifications() + ->databaseNotificationsPolling('5s') ->maxContentWidth(config('speedtest.content_width')) ->middleware([ EncryptCookies::class, From 0997f95bb37ed4521a9306a4c44e3c843c202345 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:46:22 -0600 Subject: [PATCH 06/13] add filament-simple-alert package to composer dependencies --- composer.json | 1 + composer.lock | 77 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2b38b1ec7..fef7f5773 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "require": { "php": "^8.2", "chrisullyott/php-filesize": "^4.2.1", + "codewithdennis/filament-simple-alert": "4.x", "dragonmantank/cron-expression": "^3.6.0", "filament/filament": "4.1.0", "filament/spatie-laravel-settings-plugin": "^4.1", diff --git a/composer.lock b/composer.lock index b5d427f75..d473d8628 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39020dcee9d9965e781ef550aca663ac", + "content-hash": "ccf5016c746153e60d1ad5e2996a4bea", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -625,6 +625,79 @@ ], "time": "2023-12-20T15:40:13+00:00" }, + { + "name": "codewithdennis/filament-simple-alert", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/CodeWithDennis/filament-simple-alert.git", + "reference": "d30b0cad908f3ade1bed153d486fd564ac312ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CodeWithDennis/filament-simple-alert/zipball/d30b0cad908f3ade1bed153d486fd564ac312ffd", + "reference": "d30b0cad908f3ade1bed153d486fd564ac312ffd", + "shasum": "" + }, + "require": { + "filament/filament": "^4.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.15.0" + }, + "require-dev": { + "laravel/pint": "^1.16", + "nunomaduro/collision": "^7.9", + "orchestra/testbench": "^8.0", + "pestphp/pest": "^2.1", + "pestphp/pest-plugin-arch": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "SimpleAlert": "CodeWithDennis\\SimpleAlert\\Facades\\SimpleAlert" + }, + "providers": [ + "CodeWithDennis\\SimpleAlert\\SimpleAlertServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "CodeWithDennis\\SimpleAlert\\": "src/", + "CodeWithDennis\\SimpleAlert\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CodeWithDennis", + "role": "Developer" + } + ], + "description": "A plugin for adding straightforward alerts to your filament pages", + "homepage": "https://github.com/codewithdennis/filament-simple-alert", + "keywords": [ + "CodeWithDennis", + "filament-simple-alert", + "laravel" + ], + "support": { + "issues": "https://github.com/codewithdennis/filament-simple-alert/issues", + "source": "https://github.com/codewithdennis/filament-simple-alert" + }, + "funding": [ + { + "url": "https://github.com/CodeWithDennis", + "type": "github" + } + ], + "time": "2025-06-21T18:43:06+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -13714,5 +13787,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 3da5bc5af8f399ead29f7e65612ebfd9619e7bac Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:46:32 -0600 Subject: [PATCH 07/13] bump tailwindcss and @tailwindcss/vite to version 4.1.17 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb0c8bb10..e4153991b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "": { "devDependencies": { "@tailwindcss/typography": "^0.5.10", - "@tailwindcss/vite": "^4.1.16", + "@tailwindcss/vite": "^4.1.17", "autoprefixer": "^10.4.15", "laravel-vite-plugin": "^1.0.0", - "tailwindcss": "^4.1.16", + "tailwindcss": "^4.1.17", "vite": "^6.4.1" } }, diff --git a/package.json b/package.json index cd7b12885..87e7212c3 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.10", - "@tailwindcss/vite": "^4.1.16", + "@tailwindcss/vite": "^4.1.17", "autoprefixer": "^10.4.15", "laravel-vite-plugin": "^1.0.0", - "tailwindcss": "^4.1.16", + "tailwindcss": "^4.1.17", "vite": "^6.4.1" } } From 451f7440e5b77840cd36d9d2d1845b882c12a467 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:46:59 -0600 Subject: [PATCH 08/13] bundle admin panel theme --- app/Providers/Filament/AdminPanelProvider.php | 1 + resources/css/{panel.css => filament/admin/theme.css} | 10 ++++++++++ vite.config.js | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) rename resources/css/{panel.css => filament/admin/theme.css} (50%) diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index c1fe7d2d1..f72019481 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -32,6 +32,7 @@ public function panel(Panel $panel): Panel ->colors([ 'primary' => Color::Amber, ]) + ->viteTheme('resources/css/filament/admin/theme.css') ->favicon(asset('img/speedtest-tracker-icon.png')) ->sidebarCollapsibleOnDesktop() ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') diff --git a/resources/css/panel.css b/resources/css/filament/admin/theme.css similarity index 50% rename from resources/css/panel.css rename to resources/css/filament/admin/theme.css index 4503ad7e8..b39705336 100644 --- a/resources/css/panel.css +++ b/resources/css/filament/admin/theme.css @@ -1,3 +1,13 @@ +@import '../../../../vendor/filament/filament/resources/css/theme.css'; + +@source '../../../../app/Filament/**/*'; +@source '../../../../resources/views/filament/**/*'; + +/* Filament Plugins */ +@source '../../../../vendor/codewithdennis/filament-simple-alert/resources/**/*.blade.php'; +@source inline('animate-{spin,pulse,bounce}'); + +/* Additional styles */ .fi-topbar #dashboardAction .fi-btn-label, .fi-topbar #speedtestAction .fi-btn-label { display: none; diff --git a/vite.config.js b/vite.config.js index fd6e9f1b6..d4b0e527b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,7 +7,7 @@ import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css'], + input: ['resources/css/app.css', 'resources/css/filament/admin/theme.css'], refresh: [`resources/views/**/*`], }), tailwindcss(), From 42119f59cdf29885b2e8a40d77a3ae90ba421254 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:49:14 -0600 Subject: [PATCH 09/13] moved core channels to tabs for easier navigation --- app/Filament/Pages/Settings/Notification.php | 318 ++++++++++--------- lang/en/settings/notifications.php | 3 - 2 files changed, 170 insertions(+), 151 deletions(-) diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 66bb8575c..1e87891f1 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -13,7 +13,9 @@ use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; use App\Settings\NotificationSettings; +use CodeWithDennis\SimpleAlert\Components\SimpleAlert; use Filament\Actions\Action; +use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -22,13 +24,16 @@ use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; +use Filament\Support\Icons\Heroicon; use Illuminate\Support\Facades\Auth; class Notification extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bell'; + protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bell-alert'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; @@ -60,128 +65,145 @@ public function form(Schema $schema): Schema { return $schema ->components([ - Grid::make([ - 'default' => 1, - 'md' => 3, - ]) - ->columnSpan('full') + Tabs::make() ->schema([ - Grid::make([ - 'default' => 1, - ]) + Tab::make(__('settings/notifications.database')) + ->icon(Heroicon::OutlinedCircleStack) ->schema([ - Section::make(__('settings/notifications.database')) - ->description(__('settings/notifications.database_description')) + Toggle::make('database_enabled') + ->label(__('general.enable')) + ->live(), + + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('database_enabled') !== true) ->schema([ - Toggle::make('database_enabled') - ->label(__('settings/notifications.enable_database_notifications')) - ->reactive() - ->columnSpanFull(), - Grid::make([ - 'default' => 1, - ]) - ->hidden(fn (Get $get) => $get('database_enabled') !== true) + Fieldset::make(__('settings.triggers')) + ->columns(1) ->schema([ - Fieldset::make(__('settings.triggers')) - ->schema([ - Toggle::make('database_on_speedtest_run') - ->label(__('settings/notifications.database_on_speedtest_run')) - ->columnSpanFull(), - Toggle::make('database_on_threshold_failure') - ->label(__('settings/notifications.database_on_threshold_failure')) - ->columnSpanFull(), - ]), - Actions::make([ - Action::make('test database') - ->label(__('settings/notifications.test_database_channel')) - ->action(fn () => SendDatabaseTestNotification::run(user: Auth::user())), - ]), + Checkbox::make('database_on_speedtest_run') + ->label(__('settings/notifications.database_on_speedtest_run')), + + Checkbox::make('database_on_threshold_failure') + ->label(__('settings/notifications.database_on_threshold_failure')), ]), - ]) - ->compact() - ->columnSpan('full'), - Section::make(__('settings/notifications.mail')) + Actions::make([ + Action::make('test database') + ->label(__('settings/notifications.test_database_channel')) + ->action(fn () => SendDatabaseTestNotification::run(user: Auth::user())), + ]), + ]), + + // ... + ]), + + Tab::make(__('settings/notifications.mail')) + ->icon(Heroicon::OutlinedEnvelope) + ->schema([ + Toggle::make('mail_enabled') + ->label(__('general.enable')) + ->live(), + + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('mail_enabled') !== true) ->schema([ - Toggle::make('mail_enabled') - ->label(__('settings/notifications.enable_mail_notifications')) - ->reactive() - ->columnSpanFull(), - Grid::make([ - 'default' => 1, - ]) - ->hidden(fn (Get $get) => $get('mail_enabled') !== true) + Fieldset::make(__('settings.triggers')) + ->columns(1) ->schema([ - Fieldset::make(__('settings.triggers')) - ->schema([ - Toggle::make('mail_on_speedtest_run') - ->label(__('settings/notifications.mail_on_speedtest_run')) - ->columnSpanFull(), - Toggle::make('mail_on_threshold_failure') - ->label(__('settings/notifications.mail_on_threshold_failure')) - ->columnSpanFull(), - ]), - Repeater::make('mail_recipients') - ->label(__('settings/notifications.recipients')) - ->schema([ - TextInput::make('email_address') - ->placeholder('your@email.com') - ->email() - ->required(), - ]) - ->columnSpanFull(), - Actions::make([ - Action::make('test mail') - ->label(__('settings/notifications.test_mail_channel')) - ->action(fn (Get $get) => SendMailTestNotification::run(recipients: $get('mail_recipients'))) - ->hidden(fn (Get $get) => ! count($get('mail_recipients'))), - ]), + Checkbox::make('mail_on_speedtest_run') + ->label(__('settings/notifications.mail_on_speedtest_run')), + + Checkbox::make('mail_on_threshold_failure') + ->label(__('settings/notifications.mail_on_threshold_failure')), ]), - ]) - ->compact() - ->columnSpan('full'), - Section::make(__('settings/notifications.webhook')) + Repeater::make('mail_recipients') + ->label(__('settings/notifications.recipients')) + ->schema([ + TextInput::make('email_address') + ->placeholder('your@email.com') + ->email() + ->required(), + ]), + + Actions::make([ + Action::make('test mail') + ->label(__('settings/notifications.test_mail_channel')) + ->action(fn (Get $get) => SendMailTestNotification::run(recipients: $get('mail_recipients'))) + ->hidden(fn (Get $get) => ! count($get('mail_recipients'))), + ]), + ]), + + // ... + ]), + + Tab::make(__('settings/notifications.webhook')) + ->icon(Heroicon::OutlinedGlobeAlt) + ->schema([ + Toggle::make('webhook_enabled') + ->label(__('general.enable')) + ->live(), + + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('webhook_enabled') !== true) ->schema([ - Toggle::make('webhook_enabled') - ->label(__('settings/notifications.enable_webhook_notifications')) - ->reactive() - ->columnSpanFull(), - Grid::make([ - 'default' => 1, - ]) - ->hidden(fn (Get $get) => $get('webhook_enabled') !== true) + Fieldset::make(__('settings.triggers')) + ->columns(1) ->schema([ - Fieldset::make(__('settings.triggers')) - ->schema([ - Toggle::make('webhook_on_speedtest_run') - ->label(__('settings/notifications.webhook_on_speedtest_run')) - ->columnSpan(2), - Toggle::make('webhook_on_threshold_failure') - ->label(__('settings/notifications.webhook_on_threshold_failure')) - ->columnSpan(2), - ]), - Repeater::make('webhook_urls') - ->label(__('settings/notifications.recipients')) - ->schema([ - TextInput::make('url') - ->placeholder('https://webhook.site/longstringofcharacters') - ->maxLength(2000) - ->required() - ->url(), - ]) - ->columnSpanFull(), - Actions::make([ - Action::make('test webhook') - ->label(__('settings/notifications.test_webhook_channel')) - ->action(fn (Get $get) => SendWebhookTestNotification::run(webhooks: $get('webhook_urls'))) - ->hidden(fn (Get $get) => ! count($get('webhook_urls'))), - ]), + Checkbox::make('webhook_on_speedtest_run') + ->label(__('settings/notifications.webhook_on_speedtest_run')), + + Checkbox::make('webhook_on_threshold_failure') + ->label(__('settings/notifications.webhook_on_threshold_failure')), ]), - ]) - ->compact() - ->columnSpan('full'), + Repeater::make('webhook_urls') + ->label(__('settings/notifications.recipients')) + ->schema([ + TextInput::make('url') + ->placeholder('https://webhook.site/longstringofcharacters') + ->maxLength(2000) + ->required() + ->url(), + ]), + + Actions::make([ + Action::make('test webhook') + ->label(__('settings/notifications.test_webhook_channel')) + ->action(fn (Get $get) => SendWebhookTestNotification::run(webhooks: $get('webhook_urls'))) + ->hidden(fn (Get $get) => ! count($get('webhook_urls'))), + ]), + ]), + + // ... + ]), + ]) + ->columnSpanFull(), + + // ! DEPRECATED CHANNELS + SimpleAlert::make('deprecation_warning') + ->title('Deprecated Notification Channels') + ->description('The following notification channels are deprecated and will be removed in a future release!') + ->border() + ->warning() + ->columnSpanFull(), + + Grid::make([ + 'default' => 1, + 'md' => 3, + ]) + ->columnSpan('full') + ->schema([ + Grid::make([ + 'default' => 1, + ]) + ->schema([ Section::make('Pushover') ->description('⚠️ Pushover is deprecated and will be removed in a future release.') ->schema([ @@ -192,46 +214,46 @@ public function form(Schema $schema): Schema Grid::make([ 'default' => 1, ]) - ->hidden(fn (Get $get) => $get('pushover_enabled') !== true) - ->schema([ - Fieldset::make('Triggers') - ->schema([ - Toggle::make('pushover_on_speedtest_run') - ->label('Notify on every speedtest run') - ->columnSpanFull(), - Toggle::make('pushover_on_threshold_failure') - ->label('Notify on threshold failures') - ->columnSpanFull(), + ->hidden(fn (Get $get) => $get('pushover_enabled') !== true) + ->schema([ + Fieldset::make('Triggers') + ->schema([ + Toggle::make('pushover_on_speedtest_run') + ->label('Notify on every speedtest run') + ->columnSpanFull(), + Toggle::make('pushover_on_threshold_failure') + ->label('Notify on threshold failures') + ->columnSpanFull(), + ]), + Repeater::make('pushover_webhooks') + ->label('Pushover Webhooks') + ->schema([ + TextInput::make('url') + ->label('URL') + ->placeholder('http://api.pushover.net/1/messages.json') + ->maxLength(2000) + ->required() + ->url(), + TextInput::make('user_key') + ->label('User Key') + ->placeholder('Your Pushover User Key') + ->maxLength(200) + ->required(), + TextInput::make('api_token') + ->label('API Token') + ->placeholder('Your Pushover API Token') + ->maxLength(200) + ->required(), + ]) + ->columnSpanFull(), + Actions::make([ + Action::make('test pushover') + ->label('Test Pushover webhook') + ->action(fn (Get $get) => SendPushoverTestNotification::run( + webhooks: $get('pushover_webhooks') + )) + ->hidden(fn (Get $get) => ! count($get('pushover_webhooks'))), ]), - Repeater::make('pushover_webhooks') - ->label('Pushover Webhooks') - ->schema([ - TextInput::make('url') - ->label('URL') - ->placeholder('http://api.pushover.net/1/messages.json') - ->maxLength(2000) - ->required() - ->url(), - TextInput::make('user_key') - ->label('User Key') - ->placeholder('Your Pushover User Key') - ->maxLength(200) - ->required(), - TextInput::make('api_token') - ->label('API Token') - ->placeholder('Your Pushover API Token') - ->maxLength(200) - ->required(), - ]) - ->columnSpanFull(), - Actions::make([ - Action::make('test pushover') - ->label('Test Pushover webhook') - ->action(fn (Get $get) => SendPushoverTestNotification::run( - webhooks: $get('pushover_webhooks') - )) - ->hidden(fn (Get $get) => ! count($get('pushover_webhooks'))), - ]), ]), ]) ->compact() diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 77f779529..6dcf09a88 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -7,14 +7,12 @@ // Database notifications 'database' => 'Database', 'database_description' => 'Notifications sent to this channel will show up under the 🔔 icon in the header.', - 'enable_database_notifications' => 'Enable database notifications', 'database_on_speedtest_run' => 'Notify on every speedtest run', 'database_on_threshold_failure' => 'Notify on threshold failures', 'test_database_channel' => 'Test database channel', // Mail notifications 'mail' => 'Mail', - 'enable_mail_notifications' => 'Enable mail notifications', 'recipients' => 'Recipients', 'mail_on_speedtest_run' => 'Notify on every speedtest run', 'mail_on_threshold_failure' => 'Notify on threshold failures', @@ -23,7 +21,6 @@ // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', - 'enable_webhook_notifications' => 'Enable webhook notifications', 'webhook_on_speedtest_run' => 'Notify on every speedtest run', 'webhook_on_threshold_failure' => 'Notify on threshold failures', 'test_webhook_channel' => 'Test webhook channel', From 85f126f229d10685a4256fbdf62bd8bbda5d2abd Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:49:58 -0600 Subject: [PATCH 10/13] lint fixes --- app/Filament/Pages/Settings/Notification.php | 78 ++++++++++---------- app/Mail/UnhealthySpeedtestMail.php | 2 +- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 1e87891f1..3fd0d82c5 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -214,46 +214,46 @@ public function form(Schema $schema): Schema Grid::make([ 'default' => 1, ]) - ->hidden(fn (Get $get) => $get('pushover_enabled') !== true) - ->schema([ - Fieldset::make('Triggers') - ->schema([ - Toggle::make('pushover_on_speedtest_run') - ->label('Notify on every speedtest run') - ->columnSpanFull(), - Toggle::make('pushover_on_threshold_failure') - ->label('Notify on threshold failures') - ->columnSpanFull(), - ]), - Repeater::make('pushover_webhooks') - ->label('Pushover Webhooks') - ->schema([ - TextInput::make('url') - ->label('URL') - ->placeholder('http://api.pushover.net/1/messages.json') - ->maxLength(2000) - ->required() - ->url(), - TextInput::make('user_key') - ->label('User Key') - ->placeholder('Your Pushover User Key') - ->maxLength(200) - ->required(), - TextInput::make('api_token') - ->label('API Token') - ->placeholder('Your Pushover API Token') - ->maxLength(200) - ->required(), - ]) - ->columnSpanFull(), - Actions::make([ - Action::make('test pushover') - ->label('Test Pushover webhook') - ->action(fn (Get $get) => SendPushoverTestNotification::run( - webhooks: $get('pushover_webhooks') - )) - ->hidden(fn (Get $get) => ! count($get('pushover_webhooks'))), + ->hidden(fn (Get $get) => $get('pushover_enabled') !== true) + ->schema([ + Fieldset::make('Triggers') + ->schema([ + Toggle::make('pushover_on_speedtest_run') + ->label('Notify on every speedtest run') + ->columnSpanFull(), + Toggle::make('pushover_on_threshold_failure') + ->label('Notify on threshold failures') + ->columnSpanFull(), ]), + Repeater::make('pushover_webhooks') + ->label('Pushover Webhooks') + ->schema([ + TextInput::make('url') + ->label('URL') + ->placeholder('http://api.pushover.net/1/messages.json') + ->maxLength(2000) + ->required() + ->url(), + TextInput::make('user_key') + ->label('User Key') + ->placeholder('Your Pushover User Key') + ->maxLength(200) + ->required(), + TextInput::make('api_token') + ->label('API Token') + ->placeholder('Your Pushover API Token') + ->maxLength(200) + ->required(), + ]) + ->columnSpanFull(), + Actions::make([ + Action::make('test pushover') + ->label('Test Pushover webhook') + ->action(fn (Get $get) => SendPushoverTestNotification::run( + webhooks: $get('pushover_webhooks') + )) + ->hidden(fn (Get $get) => ! count($get('pushover_webhooks'))), + ]), ]), ]) ->compact() diff --git a/app/Mail/UnhealthySpeedtestMail.php b/app/Mail/UnhealthySpeedtestMail.php index 29bb302d4..e22f4ec28 100644 --- a/app/Mail/UnhealthySpeedtestMail.php +++ b/app/Mail/UnhealthySpeedtestMail.php @@ -62,7 +62,7 @@ private function formatBenchmark(string $metric, array $benchmark): array { $metricName = str($metric)->title(); $type = str($benchmark['type'])->title(); - $thresholdValue = $benchmark['value']. ' ' . str($benchmark['unit'])->title(); + $thresholdValue = $benchmark['value'].' '.str($benchmark['unit'])->title(); // Get the actual result value $resultValue = match ($metric) { From d1c2bac6f96258747fa6f68cb95af5d932849c35 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:16:20 -0600 Subject: [PATCH 11/13] refactor test mail --- app/Actions/Notifications/SendMailTestNotification.php | 2 +- app/Mail/{Test.php => TestMail.php} | 4 ++-- resources/views/{emails => mail}/test.blade.php | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename app/Mail/{Test.php => TestMail.php} (87%) rename resources/views/{emails => mail}/test.blade.php (100%) diff --git a/app/Actions/Notifications/SendMailTestNotification.php b/app/Actions/Notifications/SendMailTestNotification.php index 6bcad272d..f8220e20f 100644 --- a/app/Actions/Notifications/SendMailTestNotification.php +++ b/app/Actions/Notifications/SendMailTestNotification.php @@ -2,7 +2,7 @@ namespace App\Actions\Notifications; -use App\Mail\Test as TestMail; +use App\Mail\TestMail; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Mail; use Lorisleiva\Actions\Concerns\AsAction; diff --git a/app/Mail/Test.php b/app/Mail/TestMail.php similarity index 87% rename from app/Mail/Test.php rename to app/Mail/TestMail.php index 5fe6fd88b..c6e43a269 100644 --- a/app/Mail/Test.php +++ b/app/Mail/TestMail.php @@ -9,7 +9,7 @@ use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; -class Test extends Mailable implements ShouldQueue +class TestMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; @@ -29,7 +29,7 @@ public function envelope(): Envelope public function content(): Content { return new Content( - markdown: 'emails.test', + markdown: 'mail.test', ); } } diff --git a/resources/views/emails/test.blade.php b/resources/views/mail/test.blade.php similarity index 100% rename from resources/views/emails/test.blade.php rename to resources/views/mail/test.blade.php From 6e8a88f251b544d88ea118485c675b36bde838a2 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:22:30 -0600 Subject: [PATCH 12/13] refactor: rename mail class and update email template for completed speedtests --- app/Listeners/ProcessCompletedSpeedtest.php | 4 ++-- ...{SpeedtestCompletedMail.php => CompletedSpeedtestMail.php} | 4 ++-- .../speedtest/completed.blade.php} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename app/Mail/{SpeedtestCompletedMail.php => CompletedSpeedtestMail.php} (93%) rename resources/views/{emails/speedtest-completed.blade.php => mail/speedtest/completed.blade.php} (100%) diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 2e7474a1b..94c2caa89 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -3,7 +3,7 @@ namespace App\Listeners; use App\Events\SpeedtestCompleted; -use App\Mail\SpeedtestCompletedMail; +use App\Mail\CompletedSpeedtestMail; use App\Models\Result; use App\Models\User; use App\Settings\NotificationSettings; @@ -120,7 +120,7 @@ private function notifyMailChannels(Result $result): void foreach ($this->notificationSettings->mail_recipients as $recipient) { Mail::to($recipient) - ->send(new SpeedtestCompletedMail($result)); + ->send(new CompletedSpeedtestMail($result)); } } diff --git a/app/Mail/SpeedtestCompletedMail.php b/app/Mail/CompletedSpeedtestMail.php similarity index 93% rename from app/Mail/SpeedtestCompletedMail.php rename to app/Mail/CompletedSpeedtestMail.php index 6f7295771..109d95360 100644 --- a/app/Mail/SpeedtestCompletedMail.php +++ b/app/Mail/CompletedSpeedtestMail.php @@ -12,7 +12,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; -class SpeedtestCompletedMail extends Mailable implements ShouldQueue +class CompletedSpeedtestMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; @@ -41,7 +41,7 @@ public function envelope(): Envelope public function content(): Content { return new Content( - markdown: 'emails.speedtest-completed', + markdown: 'mail.speedtest.completed', with: [ 'id' => $this->result->id, 'service' => Str::title($this->result->service->getLabel()), diff --git a/resources/views/emails/speedtest-completed.blade.php b/resources/views/mail/speedtest/completed.blade.php similarity index 100% rename from resources/views/emails/speedtest-completed.blade.php rename to resources/views/mail/speedtest/completed.blade.php From e4f5437baeba5940a085fcd755024a362d398b35 Mon Sep 17 00:00:00 2001 From: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:31:39 -0600 Subject: [PATCH 13/13] bump codewithdennis/filament-simple-alert --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index fef7f5773..5652a0c4d 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "require": { "php": "^8.2", "chrisullyott/php-filesize": "^4.2.1", - "codewithdennis/filament-simple-alert": "4.x", + "codewithdennis/filament-simple-alert": "^4.0.2", "dragonmantank/cron-expression": "^3.6.0", "filament/filament": "4.1.0", "filament/spatie-laravel-settings-plugin": "^4.1", diff --git a/composer.lock b/composer.lock index d473d8628..58af2d1e9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ccf5016c746153e60d1ad5e2996a4bea", + "content-hash": "3aff9923fe99afc6088082ec8c3be834", "packages": [ { "name": "anourvalar/eloquent-serialize",