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/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php
index 66bb8575c..3fd0d82c5 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([
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/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 01bf317e6..94c2caa89 100644
--- a/app/Listeners/ProcessCompletedSpeedtest.php
+++ b/app/Listeners/ProcessCompletedSpeedtest.php
@@ -3,13 +3,23 @@
namespace App\Listeners;
use App\Events\SpeedtestCompleted;
+use App\Mail\CompletedSpeedtestMail;
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 Illuminate\Support\Facades\Mail;
+use Spatie\WebhookServer\WebhookCall;
class ProcessCompletedSpeedtest
{
+ public function __construct(
+ public NotificationSettings $notificationSettings,
+ ) {}
+
/**
* Handle the event.
*/
@@ -17,8 +27,53 @@ 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
+ {
+ // Don't send Apprise notification if dispatched by a user or test is unhealthy.
+ if (filled($result->dispatched_by) || ! $result->healthy) {
+ return;
+ }
+
+ //
+ }
+
+ /**
+ * 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 +82,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 +98,72 @@ private function notifyDispatchingUser(Result $result): void
->toDatabase(),
);
}
+
+ /**
+ * Notify mail channels.
+ */
+ 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 CompletedSpeedtestMail($result));
+ }
+ }
+
+ /**
+ * 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..8ba154387 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,23 @@ public function handle(SpeedtestFailed $event): void
{
$result = $event->result;
- if ($result->dispatched_by && ! $result->scheduled) {
- $this->notifyDispatchingUser($result);
+ $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;
}
+
+ //
}
/**
@@ -27,9 +40,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/ProcessUnhealthySpeedtest.php b/app/Listeners/ProcessUnhealthySpeedtest.php
new file mode 100644
index 000000000..5a6d0e057
--- /dev/null
+++ b/app/Listeners/ProcessUnhealthySpeedtest.php
@@ -0,0 +1,169 @@
+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
+ {
+ // Don't send Apprise notification if dispatched by a user.
+ if (filled($result->dispatched_by)) {
+ return;
+ }
+
+ //
+ }
+
+ /**
+ * 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
+ {
+ // 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));
+ }
+ }
+
+ /**
+ * 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/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/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/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/app/Mail/UnhealthySpeedtestMail.php b/app/Mail/UnhealthySpeedtestMail.php
new file mode 100644
index 000000000..e22f4ec28
--- /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/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php
index aa8e024ff..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')
@@ -40,6 +41,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,
diff --git a/composer.json b/composer.json
index 2b38b1ec7..5652a0c4d 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.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 b5d427f75..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": "39020dcee9d9965e781ef550aca663ac",
+ "content-hash": "3aff9923fe99afc6088082ec8c3be834",
"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"
}
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',
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',
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"
}
}
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/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/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
diff --git a/resources/views/mail/speedtest/unhealthy.blade.php b/resources/views/mail/speedtest/unhealthy.blade.php
new file mode 100644
index 000000000..f5934fd1e
--- /dev/null
+++ b/resources/views/mail/speedtest/unhealthy.blade.php
@@ -0,0 +1,17 @@
+
+# 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') }}
+
+
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
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(),