From 9211aa4cfe20f229e5e379bb06a6fcd5aad31a15 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 5 Dec 2025 08:27:57 -0500 Subject: [PATCH 01/63] Release v1.11.2 (#2490) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index c85c93937..c9657334b 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-04'), + 'build_date' => Carbon::parse('2025-12-05'), - 'build_version' => 'v1.11.1', + 'build_version' => 'v1.11.2', 'content_width' => env('CONTENT_WIDTH', '7xl'), From a47e3225e5a3e4346d2b401c5991ad5ca55fa87f Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 5 Dec 2025 15:33:44 -0500 Subject: [PATCH 02/63] Release v1.12.0 (#2493) Co-authored-by: Sven van Ginkel Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../SendAppriseTestNotification.php | 45 ++++++ app/Filament/Pages/Dashboard.php | 39 +---- .../Pages/Settings/DataIntegration.php | 2 +- app/Filament/Pages/Settings/Notification.php | 78 ++++++++- app/Filament/Pages/Settings/Thresholds.php | 2 +- .../Resources/Results/ResultResource.php | 2 +- app/Filament/Resources/Users/UserResource.php | 4 +- app/Filament/Widgets/StatsOverviewWidget.php | 76 --------- app/Http/Controllers/HomeController.php | 12 +- app/Listeners/ProcessCompletedSpeedtest.php | 55 ++++++- app/Listeners/ProcessUnhealthySpeedtest.php | 88 +++++++++- app/Livewire/LatestResultStats.php | 24 +++ app/Livewire/PlatformStats.php | 45 ++++++ .../{RunSpeedtestAction.php => Actions.php} | 19 +-- app/Notifications/Apprise/AppriseMessage.php | 66 ++++++++ .../Apprise/SpeedtestNotification.php | 40 +++++ .../Apprise/TestNotification.php | 34 ++++ app/Notifications/AppriseChannel.php | 49 ++++-- app/Providers/AppServiceProvider.php | 16 ++ app/Providers/Filament/AdminPanelProvider.php | 21 +-- app/Providers/FilamentServiceProvider.php | 2 +- app/Rules/AppriseScheme.php | 22 +++ app/Settings/NotificationSettings.php | 12 ++ compose.yaml | 21 +++ composer.json | 2 +- composer.lock | 2 +- config/services.php | 4 - config/speedtest.php | 2 +- ..._31_164343_create_apprise_notification.php | 16 ++ lang/en/general.php | 11 ++ lang/en/results.php | 1 - lang/en/settings/notifications.php | 13 ++ resources/css/app.css | 1 + resources/css/custom.css | 11 ++ resources/css/filament/admin/theme.css | 3 + .../apprise/speedtest-completed.blade.php | 11 ++ .../apprise/speedtest-threshold.blade.php | 7 + resources/views/dashboard.blade.php | 31 ++-- .../views/filament/pages/dashboard.blade.php | 98 ++++++++++- resources/views/layouts/app.blade.php | 52 +++++- .../livewire/latest-result-stats.blade.php | 153 ++++++++++++++++++ .../views/livewire/platform-stats.blade.php | 68 ++++++++ .../views/livewire/topbar/actions.blade.php | 10 ++ .../topbar/run-speedtest-action.blade.php | 9 -- 44 files changed, 1049 insertions(+), 230 deletions(-) create mode 100644 app/Actions/Notifications/SendAppriseTestNotification.php delete mode 100644 app/Filament/Widgets/StatsOverviewWidget.php create mode 100644 app/Livewire/LatestResultStats.php create mode 100644 app/Livewire/PlatformStats.php rename app/Livewire/Topbar/{RunSpeedtestAction.php => Actions.php} (82%) create mode 100644 app/Notifications/Apprise/AppriseMessage.php create mode 100644 app/Notifications/Apprise/SpeedtestNotification.php create mode 100644 app/Notifications/Apprise/TestNotification.php create mode 100644 app/Rules/AppriseScheme.php create mode 100644 database/settings/2024_12_31_164343_create_apprise_notification.php create mode 100644 resources/css/custom.css create mode 100644 resources/views/apprise/speedtest-completed.blade.php create mode 100644 resources/views/apprise/speedtest-threshold.blade.php create mode 100644 resources/views/livewire/latest-result-stats.blade.php create mode 100644 resources/views/livewire/platform-stats.blade.php create mode 100644 resources/views/livewire/topbar/actions.blade.php delete mode 100644 resources/views/livewire/topbar/run-speedtest-action.blade.php diff --git a/app/Actions/Notifications/SendAppriseTestNotification.php b/app/Actions/Notifications/SendAppriseTestNotification.php new file mode 100644 index 000000000..062202b06 --- /dev/null +++ b/app/Actions/Notifications/SendAppriseTestNotification.php @@ -0,0 +1,45 @@ +title('You need to add Apprise channel URLs!') + ->warning() + ->send(); + + return; + } + + foreach ($channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + Notification::make() + ->title('Skipping missing channel URL!') + ->warning() + ->send(); + + continue; + } + + FacadesNotification::route('apprise_urls', $channelUrl) + ->notify(new TestNotification); + } + + Notification::make() + ->title('Test Apprise notification sent.') + ->success() + ->send(); + } +} diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index 6db680a00..bc6d7591d 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -2,20 +2,11 @@ namespace App\Filament\Pages; -use App\Filament\Widgets\RecentDownloadChartWidget; -use App\Filament\Widgets\RecentDownloadLatencyChartWidget; -use App\Filament\Widgets\RecentJitterChartWidget; -use App\Filament\Widgets\RecentPingChartWidget; -use App\Filament\Widgets\RecentUploadChartWidget; -use App\Filament\Widgets\RecentUploadLatencyChartWidget; -use App\Filament\Widgets\StatsOverviewWidget; -use Carbon\Carbon; -use Cron\CronExpression; use Filament\Pages\Dashboard as BasePage; class Dashboard extends BasePage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-layout-dashboard'; protected string $view = 'filament.pages.dashboard'; @@ -28,32 +19,4 @@ public static function getNavigationLabel(): string { return __('dashboard.title'); } - - public function getSubheading(): ?string - { - $schedule = config('speedtest.schedule'); - - if (blank($schedule) || $schedule === false) { - return __('dashboard.no_speedtests_scheduled'); - } - - $cronExpression = new CronExpression($schedule); - - $nextRunDate = Carbon::parse($cronExpression->getNextRunDate(timeZone: config('app.display_timezone')))->format(config('app.datetime_format')); - - return __('dashboard.next_speedtest_at').': '.$nextRunDate; - } - - protected function getHeaderWidgets(): array - { - return [ - StatsOverviewWidget::make(), - RecentDownloadChartWidget::make(), - RecentUploadChartWidget::make(), - RecentPingChartWidget::make(), - RecentJitterChartWidget::make(), - RecentDownloadLatencyChartWidget::make(), - RecentUploadLatencyChartWidget::make(), - ]; - } } diff --git a/app/Filament/Pages/Settings/DataIntegration.php b/app/Filament/Pages/Settings/DataIntegration.php index d61f41df8..e680627dd 100644 --- a/app/Filament/Pages/Settings/DataIntegration.php +++ b/app/Filament/Pages/Settings/DataIntegration.php @@ -23,7 +23,7 @@ class DataIntegration extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-circle-stack'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-database'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 915225265..9a5decb00 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -2,6 +2,7 @@ namespace App\Filament\Pages\Settings; +use App\Actions\Notifications\SendAppriseTestNotification; use App\Actions\Notifications\SendDatabaseTestNotification; use App\Actions\Notifications\SendDiscordTestNotification; use App\Actions\Notifications\SendGotifyTestNotification; @@ -12,6 +13,7 @@ use App\Actions\Notifications\SendSlackTestNotification; use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; +use App\Rules\AppriseScheme; use App\Settings\NotificationSettings; use CodeWithDennis\SimpleAlert\Components\SimpleAlert; use Filament\Actions\Action; @@ -33,7 +35,7 @@ class Notification extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bell-alert'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-bell-ringing'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; @@ -199,6 +201,80 @@ public function form(Schema $schema): Schema // ... ]), + Tab::make(__('settings/notifications.apprise')) + ->icon(Heroicon::CloudArrowUp) + ->schema([ + SimpleAlert::make('wehbook_info') + ->title(__('general.documentation')) + ->description(__('settings/notifications.apprise_hint_description')) + ->border() + ->info() + ->actions([ + Action::make('webhook_docs') + ->label(__('general.view_documentation')) + ->icon('heroicon-m-arrow-long-right') + ->color('info') + ->link() + ->url('https://docs.speedtest-tracker.dev/settings/notifications/apprise') + ->openUrlInNewTab(), + ]) + ->columnSpanFull(), + + Toggle::make('apprise_enabled') + ->label(__('settings/notifications.enable_apprise_notifications')) + ->reactive() + ->columnSpanFull(), + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('apprise_enabled') !== true) + ->schema([ + Fieldset::make(__('settings/notifications.apprise_server')) + ->schema([ + TextInput::make('apprise_server_url') + ->label(__('settings/notifications.apprise_server_url')) + ->placeholder('http://localhost:8000') + ->maxLength(2000) + ->required() + ->url() + ->columnSpanFull(), + Checkbox::make('apprise_verify_ssl') + ->label(__('settings/notifications.apprise_verify_ssl')) + ->default(true) + ->columnSpanFull(), + ]), + Fieldset::make(__('settings.triggers')) + ->schema([ + Checkbox::make('apprise_on_speedtest_run') + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->columnSpanFull(), + Checkbox::make('apprise_on_threshold_failure') + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->columnSpanFull(), + ]), + Repeater::make('apprise_channel_urls') + ->label(__('settings/notifications.apprise_channels')) + ->schema([ + TextInput::make('channel_url') + ->label(__('settings/notifications.apprise_channel_url')) + ->placeholder('discord://WebhookID/WebhookToken') + ->helperText(__('settings/notifications.apprise_channel_url_helper')) + ->maxLength(2000) + ->distinct() + ->required() + ->rule(new AppriseScheme), + ]) + ->columnSpanFull(), + Actions::make([ + Action::make('test apprise') + ->label(__('settings/notifications.test_apprise_channel')) + ->action(fn (Get $get) => SendAppriseTestNotification::run( + channel_urls: $get('apprise_channel_urls'), + )) + ->hidden(fn (Get $get) => ! count($get('apprise_channel_urls'))), + ]), + ]), + ]), ]) ->columnSpanFull(), diff --git a/app/Filament/Pages/Settings/Thresholds.php b/app/Filament/Pages/Settings/Thresholds.php index 6c8adb4ab..1953ff52a 100644 --- a/app/Filament/Pages/Settings/Thresholds.php +++ b/app/Filament/Pages/Settings/Thresholds.php @@ -15,7 +15,7 @@ class Thresholds extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-triangle'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-alert-triangle'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; diff --git a/app/Filament/Resources/Results/ResultResource.php b/app/Filament/Resources/Results/ResultResource.php index 6efd4a9f4..5ff0893d7 100644 --- a/app/Filament/Resources/Results/ResultResource.php +++ b/app/Filament/Resources/Results/ResultResource.php @@ -14,7 +14,7 @@ class ResultResource extends Resource { protected static ?string $model = Result::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-table-cells'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-table'; public static function getNavigationLabel(): string { diff --git a/app/Filament/Resources/Users/UserResource.php b/app/Filament/Resources/Users/UserResource.php index 914172ab8..c1b2d958d 100644 --- a/app/Filament/Resources/Users/UserResource.php +++ b/app/Filament/Resources/Users/UserResource.php @@ -14,9 +14,7 @@ class UserResource extends Resource { protected static ?string $model = User::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-users'; - - protected static string|\UnitEnum|null $navigationGroup = 'Settings'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-users'; protected static ?int $navigationSort = 4; diff --git a/app/Filament/Widgets/StatsOverviewWidget.php b/app/Filament/Widgets/StatsOverviewWidget.php deleted file mode 100644 index 8178c10e3..000000000 --- a/app/Filament/Widgets/StatsOverviewWidget.php +++ /dev/null @@ -1,76 +0,0 @@ -result = Result::query() - ->select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) - ->where('status', '=', ResultStatus::Completed) - ->latest() - ->first(); - - if (blank($this->result)) { - return [ - Stat::make(__('dashboard.latest_download'), '-') - ->icon('heroicon-o-arrow-down-tray'), - Stat::make(__('dashboard.latest_upload'), '-') - ->icon('heroicon-o-arrow-up-tray'), - Stat::make(__('dashboard.latest_ping'), '-') - ->icon('heroicon-o-clock'), - ]; - } - - $previous = Result::query() - ->select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) - ->where('id', '<', $this->result->id) - ->where('status', '=', ResultStatus::Completed) - ->latest() - ->first(); - - if (! $previous) { - return [ - Stat::make(__('dashboard.latest_download'), fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->download_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-down-tray'), - Stat::make(__('dashboard.latest_upload'), fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->upload_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-up-tray'), - Stat::make(__('dashboard.latest_ping'), fn (): string => ! blank($this->result) ? number_format($this->result->ping, 2).' ms' : 'n/a') - ->icon('heroicon-o-clock'), - ]; - } - - $downloadChange = percentChange($this->result->download, $previous->download, 2); - $uploadChange = percentChange($this->result->upload, $previous->upload, 2); - $pingChange = percentChange($this->result->ping, $previous->ping, 2); - - return [ - Stat::make(__('dashboard.latest_download'), fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->download_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-down-tray') - ->description($downloadChange > 0 ? $downloadChange.'% '.__('general.faster') : abs($downloadChange).'% '.__('general.slower')) - ->descriptionIcon($downloadChange > 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down') - ->color($downloadChange > 0 ? 'success' : 'danger'), - Stat::make(__('dashboard.latest_upload'), fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->upload_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-up-tray') - ->description($uploadChange > 0 ? $uploadChange.'% '.__('general.faster') : abs($uploadChange).'% '.__('general.slower')) - ->descriptionIcon($uploadChange > 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down') - ->color($uploadChange > 0 ? 'success' : 'danger'), - Stat::make(__('dashboard.latest_ping'), fn (): string => ! blank($this->result) ? number_format($this->result->ping, 2).' ms' : 'n/a') - ->icon('heroicon-o-clock') - ->description($pingChange > 0 ? $pingChange.'% '.__('general.slower') : abs($pingChange).'% '.__('general.faster')) - ->descriptionIcon($pingChange > 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down') - ->color($pingChange > 0 ? 'danger' : 'success'), - ]; - } -} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 0b6d0e906..543c692e4 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use App\Enums\ResultStatus; -use App\Models\Result; use Illuminate\Http\Request; class HomeController extends Controller @@ -13,14 +11,6 @@ class HomeController extends Controller */ public function __invoke(Request $request) { - $latestResult = Result::query() - ->select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) - ->where('status', '=', ResultStatus::Completed) - ->latest() - ->first(); - - return view('dashboard', [ - 'latestResult' => $latestResult, - ]); + return view('dashboard'); } } diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 228151750..66b927f1a 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -3,15 +3,19 @@ namespace App\Listeners; use App\Events\SpeedtestCompleted; +use App\Helpers\Number; use App\Mail\CompletedSpeedtestMail; use App\Models\Result; use App\Models\User; +use App\Notifications\Apprise\SpeedtestNotification; use App\Settings\NotificationSettings; use Filament\Actions\Action; -use Filament\Notifications\Notification; +use Filament\Notifications\Notification as FilamentNotification; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Str; use Spatie\WebhookServer\WebhookCall; class ProcessCompletedSpeedtest @@ -29,7 +33,7 @@ public function handle(SpeedtestCompleted $event): void $result->loadMissing(['dispatchedBy']); - // $this->notifyAppriseChannels($result); + $this->notifyAppriseChannels($result); $this->notifyDatabaseChannels($result); $this->notifyDispatchingUser($result); $this->notifyMailChannels($result); @@ -42,11 +46,50 @@ 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) { + if (filled($result->dispatched_by) || $result->healthy === false) { return; } - // + // Check if Apprise notifications are enabled. + if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_speedtest_run) { + return; + } + + if (! count($this->notificationSettings->apprise_channel_urls)) { + Log::warning('Apprise channel URLs not found, check Apprise notification channel settings.'); + + return; + } + + // Build the speedtest data + $body = view('apprise.speedtest-completed', [ + 'id' => $result->id, + 'service' => Str::title($result->service->getLabel()), + 'serverName' => $result->server_name, + 'serverId' => $result->server_id, + 'isp' => $result->isp, + 'ping' => round($result->ping).' ms', + 'download' => Number::toBitRate(bits: $result->download_bits, precision: 2), + 'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2), + 'packetLoss' => $result->packet_loss, + 'speedtest_url' => $result->result_url, + 'url' => url('/admin/results'), + ])->render(); + + $title = 'Speedtest Completed – #'.$result->id; + + // Send notification to each configured channel URL + foreach ($this->notificationSettings->apprise_channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + Log::warning('Skipping entry with missing channel_url.'); + + continue; + } + + Notification::route('apprise_urls', $channelUrl) + ->notify(new SpeedtestNotification($title, $body, 'info')); + } } /** @@ -65,7 +108,7 @@ private function notifyDatabaseChannels(Result $result): void } foreach (User::all() as $user) { - Notification::make() + FilamentNotification::make() ->title(__('results.speedtest_completed')) ->actions([ Action::make('view') @@ -87,7 +130,7 @@ private function notifyDispatchingUser(Result $result): void } $result->dispatchedBy->notify( - Notification::make() + FilamentNotification::make() ->title(__('results.speedtest_completed')) ->actions([ Action::make('view') diff --git a/app/Listeners/ProcessUnhealthySpeedtest.php b/app/Listeners/ProcessUnhealthySpeedtest.php index 5a6d0e057..68b9b6a9b 100644 --- a/app/Listeners/ProcessUnhealthySpeedtest.php +++ b/app/Listeners/ProcessUnhealthySpeedtest.php @@ -3,14 +3,18 @@ namespace App\Listeners; use App\Events\SpeedtestBenchmarkFailed; +use App\Helpers\Number; use App\Mail\UnhealthySpeedtestMail; use App\Models\Result; use App\Models\User; +use App\Notifications\Apprise\SpeedtestNotification; use App\Settings\NotificationSettings; use Filament\Actions\Action; -use Filament\Notifications\Notification; +use Filament\Notifications\Notification as FilamentNotification; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Str; use Spatie\WebhookServer\WebhookCall; class ProcessUnhealthySpeedtest @@ -31,7 +35,7 @@ public function handle(SpeedtestBenchmarkFailed $event): void $result->loadMissing(['dispatchedBy']); - // $this->notifyAppriseChannels($result); + $this->notifyAppriseChannels($result); $this->notifyDatabaseChannels($result); $this->notifyDispatchingUser($result); $this->notifyMailChannels($result); @@ -48,7 +52,79 @@ private function notifyAppriseChannels(Result $result): void return; } - // + if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_threshold_failure) { + return; + } + + if (! count($this->notificationSettings->apprise_channel_urls)) { + Log::warning('Apprise channel URLs not found, check Apprise notification channel settings.'); + + return; + } + + if (empty($result->benchmarks)) { + Log::warning('Benchmark data not found, won\'t send Apprise notification.'); + + return; + } + + // Build metrics array from failed benchmarks + $failed = []; + + foreach ($result->benchmarks as $metric => $benchmark) { + if ($benchmark['passed'] === false) { + $failed[] = [ + 'name' => ucfirst($metric), + 'threshold' => $benchmark['value'].' '.$benchmark['unit'], + 'value' => $this->formatMetricValue($metric, $result), + ]; + } + } + + if (! count($failed)) { + Log::warning('No failed thresholds found in benchmarks, won\'t send Apprise notification.'); + + return; + } + + $body = view('apprise.speedtest-threshold', [ + 'id' => $result->id, + 'service' => Str::title($result->service->getLabel()), + 'serverName' => $result->server_name, + 'serverId' => $result->server_id, + 'isp' => $result->isp, + 'metrics' => $failed, + 'speedtest_url' => $result->result_url, + 'url' => url('/admin/results'), + ])->render(); + + $title = 'Speedtest Threshold Breach – #'.$result->id; + + // Send notification to each configured channel URL + foreach ($this->notificationSettings->apprise_channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + Log::warning('Skipping entry with missing channel_url.'); + + continue; + } + + Notification::route('apprise_urls', $channelUrl) + ->notify(new SpeedtestNotification($title, $body, 'warning')); + } + } + + /** + * Format metric value for display in notification. + */ + private function formatMetricValue(string $metric, Result $result): string + { + return match ($metric) { + 'download' => Number::toBitRate(bits: $result->download_bits, precision: 2), + 'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2), + 'ping' => round($result->ping, 2).' ms', + default => '', + }; } /** @@ -67,7 +143,7 @@ private function notifyDatabaseChannels(Result $result): void } foreach (User::all() as $user) { - Notification::make() + FilamentNotification::make() ->title(__('results.speedtest_benchmark_failed')) ->actions([ Action::make('view') @@ -89,7 +165,7 @@ private function notifyDispatchingUser(Result $result): void } $result->dispatchedBy->notify( - Notification::make() + FilamentNotification::make() ->title(__('results.speedtest_benchmark_failed')) ->actions([ Action::make('view') @@ -106,7 +182,7 @@ private function notifyDispatchingUser(Result $result): void */ private function notifyMailChannels(Result $result): void { - // Don't send webhook if dispatched by a user. + // Don't send mail if dispatched by a user. if (filled($result->dispatched_by)) { return; } diff --git a/app/Livewire/LatestResultStats.php b/app/Livewire/LatestResultStats.php new file mode 100644 index 000000000..64217b8ab --- /dev/null +++ b/app/Livewire/LatestResultStats.php @@ -0,0 +1,24 @@ +latest() + ->first(); + } + + public function render() + { + return view('livewire.latest-result-stats'); + } +} diff --git a/app/Livewire/PlatformStats.php b/app/Livewire/PlatformStats.php new file mode 100644 index 000000000..17668eabf --- /dev/null +++ b/app/Livewire/PlatformStats.php @@ -0,0 +1,45 @@ +getNextRunDate(timeZone: config('app.display_timezone'))); + } + + return null; + } + + #[Computed] + public function platformStats(): array + { + $totalResults = Result::count(); + $completedResults = Result::where('status', ResultStatus::Completed)->count(); + $failedResults = Result::where('status', ResultStatus::Failed)->count(); + + return [ + 'total' => Number::format($totalResults), + 'completed' => Number::format($completedResults), + 'failed' => Number::format($failedResults), + ]; + } + + public function render() + { + return view('livewire.platform-stats'); + } +} diff --git a/app/Livewire/Topbar/RunSpeedtestAction.php b/app/Livewire/Topbar/Actions.php similarity index 82% rename from app/Livewire/Topbar/RunSpeedtestAction.php rename to app/Livewire/Topbar/Actions.php index ad8c05271..95a8abcce 100644 --- a/app/Livewire/Topbar/RunSpeedtestAction.php +++ b/app/Livewire/Topbar/Actions.php @@ -13,21 +13,21 @@ use Filament\Forms\Contracts\HasForms; use Filament\Notifications\Notification; use Filament\Support\Enums\IconPosition; +use Filament\Support\Enums\Size; use Illuminate\Support\Facades\Auth; use Livewire\Component; -class RunSpeedtestAction extends Component implements HasActions, HasForms +class Actions extends Component implements HasActions, HasForms { use InteractsWithActions, InteractsWithForms; public function dashboardAction(): Action { - return Action::make('home') - ->label(__('results.public_dashboard')) - ->icon('heroicon-o-chart-bar') - ->iconPosition(IconPosition::Before) + return Action::make('metrics') + ->iconButton() + ->icon('tabler-chart-histogram') ->color('gray') - ->url(shouldOpenInNewTab: true, url: route('home')) + ->url(url: route('home')) ->extraAttributes([ 'id' => 'dashboardAction', ]); @@ -61,13 +61,14 @@ public function speedtestAction(): Action ->success() ->send(); }) - ->modalHeading(__('results.run_speedtest')) + ->modalHeading(__('results.speedtest')) ->modalWidth('lg') ->modalSubmitActionLabel(__('results.start')) ->button() + ->size(Size::Medium) ->color('primary') ->label(__('results.speedtest')) - ->icon('heroicon-o-rocket-launch') + ->icon('tabler-rocket') ->iconPosition(IconPosition::Before) ->hidden(! Auth::check() && Auth::user()->is_admin) ->extraAttributes([ @@ -77,6 +78,6 @@ public function speedtestAction(): Action public function render() { - return view('livewire.topbar.run-speedtest-action'); + return view('livewire.topbar.actions'); } } diff --git a/app/Notifications/Apprise/AppriseMessage.php b/app/Notifications/Apprise/AppriseMessage.php new file mode 100644 index 000000000..a510ded7b --- /dev/null +++ b/app/Notifications/Apprise/AppriseMessage.php @@ -0,0 +1,66 @@ +urls = $urls; + + return $this; + } + + public function title(string $title): self + { + $this->title = $title; + + return $this; + } + + public function body(string $body): self + { + $this->body = $body; + + return $this; + } + + public function type(string $type): self + { + $this->type = $type; + + return $this; + } + + public function format(string $format): self + { + $this->format = $format; + + return $this; + } + + public function tag(string $tag): self + { + $this->tag = $tag; + + return $this; + } +} diff --git a/app/Notifications/Apprise/SpeedtestNotification.php b/app/Notifications/Apprise/SpeedtestNotification.php new file mode 100644 index 000000000..3c2ffb3cd --- /dev/null +++ b/app/Notifications/Apprise/SpeedtestNotification.php @@ -0,0 +1,40 @@ + + */ + public function via(object $notifiable): array + { + return ['apprise']; + } + + /** + * Get the Apprise message representation of the notification. + */ + public function toApprise(object $notifiable): AppriseMessage + { + return AppriseMessage::create() + ->urls($notifiable->routes['apprise_urls']) + ->title($this->title) + ->body($this->body) + ->type($this->type); + } +} diff --git a/app/Notifications/Apprise/TestNotification.php b/app/Notifications/Apprise/TestNotification.php new file mode 100644 index 000000000..f07810fcc --- /dev/null +++ b/app/Notifications/Apprise/TestNotification.php @@ -0,0 +1,34 @@ + + */ + public function via(object $notifiable): array + { + return ['apprise']; + } + + /** + * Get the Apprise message representation of the notification. + */ + public function toApprise(object $notifiable): AppriseMessage + { + return AppriseMessage::create() + ->urls($notifiable->routes['apprise_urls']) + ->title('Test Notification') + ->body('👋 Testing the Apprise notification channel.') + ->type('info'); + } +} diff --git a/app/Notifications/AppriseChannel.php b/app/Notifications/AppriseChannel.php index af5ac3683..3cd2592a1 100644 --- a/app/Notifications/AppriseChannel.php +++ b/app/Notifications/AppriseChannel.php @@ -2,6 +2,7 @@ namespace App\Notifications; +use App\Settings\NotificationSettings; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -20,34 +21,54 @@ public function send(object $notifiable, Notification $notification): void return; } - $appriseUrl = config('services.apprise.url'); + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + + if (empty($appriseUrl)) { + Log::warning('Apprise notification skipped: No Server URL configured'); + + return; + } try { - $response = Http::timeout(5) + $request = Http::timeout(5) ->withHeaders([ 'Content-Type' => 'application/json', - ]) - // ->when(true, function ($http) { - // $http->withoutVerifying(); - // }) - ->post("{$appriseUrl}/notify", [ - 'urls' => $message->urls, - 'title' => $message->title, - 'body' => $message->body, - 'type' => $message->type ?? 'info', - 'format' => $message->format ?? 'text', - 'tag' => $message->tag ?? null, ]); + // If SSL verification is disabled in settings, skip it + if (! $settings->apprise_verify_ssl) { + $request = $request->withoutVerifying(); + } + + $response = $request->post("{$appriseUrl}/notify", [ + 'urls' => $message->urls, + 'title' => $message->title, + 'body' => $message->body, + 'type' => $message->type ?? 'info', + 'format' => $message->format ?? 'text', + 'tag' => $message->tag ?? null, + ]); + if ($response->failed()) { Log::error('Apprise notification failed', [ + 'channel' => $message->urls, + 'instance' => $appriseUrl, 'status' => $response->status(), 'body' => $response->body(), ]); + } else { + Log::info('Apprise notification sent', [ + 'channel' => $message->urls, + 'instance' => $appriseUrl, + ]); } - } catch (\Exception $e) { + } catch (\Throwable $e) { Log::error('Apprise notification exception', [ + 'channel' => $message->urls ?? 'unknown', + 'instance' => $appriseUrl, 'message' => $e->getMessage(), + 'exception' => get_class($e), ]); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e4ea6209f..ba434c79e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,11 +4,14 @@ use App\Enums\UserRole; use App\Models\User; +use App\Notifications\AppriseChannel; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Http\Request; +use Illuminate\Notifications\ChannelManager; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; @@ -44,12 +47,25 @@ public function boot(): void $this->defineGates(); $this->forceHttps(); $this->setApiRateLimit(); + $this->registerNotificationChannels(); AboutCommand::add('Speedtest Tracker', fn () => [ 'Version' => config('speedtest.build_version'), ]); } + /** + * Register custom notification channels. + */ + protected function registerNotificationChannels(): void + { + Notification::resolved(function (ChannelManager $service) { + $service->extend('apprise', function ($app) { + return new AppriseChannel; + }); + }); + } + /** * Define custom if statements, these were added to make the blade templates more readable. * diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index f72019481..3a6ba6f40 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,12 +2,10 @@ namespace App\Providers\Filament; -use App\Services\GitHub\Repository; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Navigation\NavigationGroup; -use Filament\Navigation\NavigationItem; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; @@ -59,25 +57,8 @@ public function panel(Panel $panel): Panel ]) ->navigationGroups([ NavigationGroup::make() - ->label(__('general.settings')), - NavigationGroup::make() - ->label(__('general.links')) + ->label(__('general.settings')) ->collapsible(false), - ]) - ->navigationItems([ - NavigationItem::make(__('general.documentation')) - ->url('https://docs.speedtest-tracker.dev/', shouldOpenInNewTab: true) - ->icon('heroicon-o-book-open') - ->group(__('general.links')), - NavigationItem::make(__('general.donate')) - ->url('https://github.com/sponsors/alexjustesen', shouldOpenInNewTab: true) - ->icon('heroicon-o-banknotes') - ->group(__('general.links')), - NavigationItem::make(config('speedtest.build_version')) - ->url('https://github.com/alexjustesen/speedtest-tracker', shouldOpenInNewTab: true) - ->icon('tabler-brand-github') - ->badge(fn (): string => Repository::updateAvailable() ? __('general.update_available') : __('general.up_to_date')) - ->group(__('general.links')), ]); } } diff --git a/app/Providers/FilamentServiceProvider.php b/app/Providers/FilamentServiceProvider.php index 1f6b2bd44..99e95cd2e 100644 --- a/app/Providers/FilamentServiceProvider.php +++ b/app/Providers/FilamentServiceProvider.php @@ -24,7 +24,7 @@ public function boot(): void { FilamentView::registerRenderHook( PanelsRenderHook::GLOBAL_SEARCH_BEFORE, - fn (): string => Blade::render("@livewire('topbar.run-speedtest-action')"), + fn (): string => Blade::render("@livewire('topbar.actions')"), ); } } diff --git a/app/Rules/AppriseScheme.php b/app/Rules/AppriseScheme.php new file mode 100644 index 000000000..03a50059e --- /dev/null +++ b/app/Rules/AppriseScheme.php @@ -0,0 +1,22 @@ + [ - 'url' => env('APPRISE_URL', 'http://apprise:8000'), - ], - 'telegram-bot-api' => [ 'token' => env('TELEGRAM_BOT_TOKEN'), ], diff --git a/config/speedtest.php b/config/speedtest.php index c9657334b..8fd6c0f81 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -8,7 +8,7 @@ */ 'build_date' => Carbon::parse('2025-12-05'), - 'build_version' => 'v1.11.2', + 'build_version' => 'v1.12.0', 'content_width' => env('CONTENT_WIDTH', '7xl'), diff --git a/database/settings/2024_12_31_164343_create_apprise_notification.php b/database/settings/2024_12_31_164343_create_apprise_notification.php new file mode 100644 index 000000000..1be9ac906 --- /dev/null +++ b/database/settings/2024_12_31_164343_create_apprise_notification.php @@ -0,0 +1,16 @@ +migrator->add('notification.apprise_enabled', false); + $this->migrator->add('notification.apprise_server_url', null); + $this->migrator->add('notification.apprise_on_speedtest_run', false); + $this->migrator->add('notification.apprise_on_threshold_failure', false); + $this->migrator->add('notification.apprise_verify_ssl', true); + $this->migrator->add('notification.apprise_channel_urls', null); + } +}; diff --git a/lang/en/general.php b/lang/en/general.php index dbc2d675f..33c212e21 100644 --- a/lang/en/general.php +++ b/lang/en/general.php @@ -1,6 +1,11 @@ 'Current version', + 'latest_version' => 'Latest version', + 'github' => 'GitHub', + 'repository' => 'Repository', + // Common actions 'save' => 'Save', 'cancel' => 'Cancel', @@ -32,6 +37,8 @@ 'created_at' => 'Created at', 'updated_at' => 'Updated at', 'url' => 'URL', + 'stats' => 'Stats', + 'statistics' => 'Statistics', // Navigation 'dashboard' => 'Dashboard', @@ -42,6 +49,7 @@ 'view_documentation' => 'View documentation', 'links' => 'Links', 'donate' => 'Donate', + 'donations' => 'Donations', // Roles 'admin' => 'Admin', @@ -54,12 +62,15 @@ 'last_month' => 'Last month', // Metrics + 'metrics' => 'Metrics', 'average' => 'Average', 'high' => 'High', 'low' => 'Low', 'faster' => 'faster', 'slower' => 'slower', 'healthy' => 'Healthy', + 'not_measured' => 'Not measured', + 'unhealthy' => 'Unhealthy', // Units 'ms' => 'ms', diff --git a/lang/en/results.php b/lang/en/results.php index 6d8a2016d..65f81386f 100644 --- a/lang/en/results.php +++ b/lang/en/results.php @@ -72,7 +72,6 @@ // Run Speedtest Action 'speedtest' => 'Speedtest', - 'public_dashboard' => 'Public Dashboard', 'select_server' => 'Select Server', 'select_server_helper' => 'Leave empty to run the speedtest without specifying a server. Blocked servers will be skipped.', 'manual_servers' => 'Manual servers', diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index ddc1baae8..203590b23 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -14,6 +14,19 @@ 'recipients' => 'Recipients', 'test_mail_channel' => 'Test mail channel', + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Enable Apprise notifications', + 'apprise_server' => 'Apprise Server', + 'apprise_server_url' => 'Apprise Server URL', + 'apprise_verify_ssl' => 'Verify SSL', + 'apprise_channels' => 'Apprise Channels', + 'apprise_channel_url' => 'Channel URL', + 'apprise_hint_description' => 'For more information on setting up Apprise, view the documentation.', + 'apprise_channel_url_helper' => 'Provide the service endpoint URL for notifications.', + 'test_apprise_channel' => 'Test Apprise', + 'apprise_channel_url_validation_error' => 'The Apprise channel URL must not start with "http" or "https". Please provide a valid Apprise URL scheme.', + // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', diff --git a/resources/css/app.css b/resources/css/app.css index 54ea6064a..532b718c1 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@import './custom.css'; /* Safelist max-width utilities to always generate them */ @source inline("max-w-{xs,sm,md,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,full,min,max,fit,prose,screen-sm,screen-md,screen-lg,screen-xl,screen-2xl}"); diff --git a/resources/css/custom.css b/resources/css/custom.css new file mode 100644 index 000000000..465ee0cf8 --- /dev/null +++ b/resources/css/custom.css @@ -0,0 +1,11 @@ +.dashboard-page .fi-section-header { + padding-bottom: 0px; +} + +.dashboard-page .fi-section-header .fi-section-header-heading { + @apply font-medium text-zinc-600 dark:text-zinc-400; +} + +.dashboard-page .fi-section-content-ctn { + border-top: none; +} diff --git a/resources/css/filament/admin/theme.css b/resources/css/filament/admin/theme.css index b39705336..6d386b0e9 100644 --- a/resources/css/filament/admin/theme.css +++ b/resources/css/filament/admin/theme.css @@ -1,4 +1,6 @@ +@import 'tailwindcss'; @import '../../../../vendor/filament/filament/resources/css/theme.css'; +@import '../../custom.css'; @source '../../../../app/Filament/**/*'; @source '../../../../resources/views/filament/**/*'; @@ -6,6 +8,7 @@ /* Filament Plugins */ @source '../../../../vendor/codewithdennis/filament-simple-alert/resources/**/*.blade.php'; @source inline('animate-{spin,pulse,bounce}'); +@source inline('{bg,text,border,ring}-{amber,zinc}-{50,100,200,300,400,500,600,700,800,900,950}'); /* Additional styles */ .fi-topbar #dashboardAction .fi-btn-label, diff --git a/resources/views/apprise/speedtest-completed.blade.php b/resources/views/apprise/speedtest-completed.blade.php new file mode 100644 index 000000000..6363ee642 --- /dev/null +++ b/resources/views/apprise/speedtest-completed.blade.php @@ -0,0 +1,11 @@ +A new speedtest on {{ config('app.name') }} was completed using {{ $service }}. + +Server name: {{ $serverName }} +Server ID: {{ $serverId }} +ISP: {{ $isp }} +Ping: {{ $ping }} +Download: {{ $download }} +Upload: {{ $upload }} +Packet Loss: {{ $packetLoss }} % +Ookla Speedtest: {{ $speedtest_url }} +URL: {{ $url }} diff --git a/resources/views/apprise/speedtest-threshold.blade.php b/resources/views/apprise/speedtest-threshold.blade.php new file mode 100644 index 000000000..6d0bb4926 --- /dev/null +++ b/resources/views/apprise/speedtest-threshold.blade.php @@ -0,0 +1,7 @@ +A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. + +@foreach ($metrics as $item) +- **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }} +@endforeach +- **Ookla Speedtest:** {{ $speedtest_url }} +- **URL:** {{ $url }} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 8a03ce90c..b3d3762b7 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,39 +1,26 @@ -
-
- @livewire(\App\Filament\Widgets\StatsOverviewWidget::class) -
+
+ + + - @isset($latestResult) -
- Latest result: -
- @endisset +
+

+ + Metrics +

-
@livewire(\App\Filament\Widgets\RecentDownloadChartWidget::class) -
-
@livewire(\App\Filament\Widgets\RecentUploadChartWidget::class) -
-
@livewire(\App\Filament\Widgets\RecentPingChartWidget::class) -
-
@livewire(\App\Filament\Widgets\RecentJitterChartWidget::class) -
-
@livewire(\App\Filament\Widgets\RecentDownloadLatencyChartWidget::class) -
-
@livewire(\App\Filament\Widgets\RecentUploadLatencyChartWidget::class)
-
- diff --git a/resources/views/filament/pages/dashboard.blade.php b/resources/views/filament/pages/dashboard.blade.php index f351a1c97..42eb6bbf5 100644 --- a/resources/views/filament/pages/dashboard.blade.php +++ b/resources/views/filament/pages/dashboard.blade.php @@ -1,3 +1,97 @@ - - {{-- Silence is golden --}} + +
+ + + + +
+ + + {{ __('general.documentation') }} + + +
+

Need help getting started or configuring your speedtests?

+
+ +
+ + {{ __('general.view_documentation') }} + +
+
+ + + + {{ __('general.donations') }} + + +
+

Support the development and maintenance of Speedtest Tracker by making a donation.

+
+ +
+ + {{ __('general.donate') }} + +
+
+ + + + {{ __('general.speedtest_tracker') }} + + + @if (\App\Services\GitHub\Repository::updateAvailable()) + + + {{ __('general.update_available') }} + + + @endif + +
    +
  • +

    {{ __('general.current_version') }}

    +

    {{ config('speedtest.build_version') }}

    +
  • + +
  • +

    {{ __('general.latest_version') }}

    +

    {{ \App\Services\GitHub\Repository::getLatestVersion() }}

    +
  • +
+ +
+ + {{ __('general.github') }} {{ str(__('general.repository'))->lower() }} + +
+
+
+
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 80ba6986d..cc94b33d3 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -40,7 +40,57 @@

{{ $title ?? 'Page Title' }} - {{ config('app.name') }}

-
+
+
+ + + + + +
+ + @filled($this->latestResult) +
+
+

+ + Latest result +

+ + + {{ __('general.view') }} + +
+ + + + Benchmark status + + +
+ @if($this->latestResult->healthy === true) +
+
+
+ + {{ __('general.healthy') }} + @elseif($this->latestResult->healthy === false) +
+
+
+ + {{ __('general.unhealthy') }} + @else +
+
+
+ + {{ __('general.not_measured') }} + @endif +
+
+ + + + {{ __('general.download') }} + + + @php + $downloadBenchmark = Arr::get($this->latestResult->benchmarks, 'download'); + $downloadBenchmarkPassed = Arr::get($downloadBenchmark, 'passed', false); + @endphp + + @filled($downloadBenchmark) + + $downloadBenchmarkPassed, + 'text-amber-500 dark:text-amber-400' => ! $downloadBenchmarkPassed, + ]) title="Benchmark {{ $downloadBenchmarkPassed ? 'passed' : 'failed' }}"> + @if (! $downloadBenchmarkPassed) + + @endif + {{ Arr::get($downloadBenchmark, 'value').' '.str(Arr::get($downloadBenchmark, 'unit'))->title() }} + + + @endfilled + +

+ @php + $download = \App\Helpers\Bitrate::formatBits(\App\Helpers\Bitrate::bytesToBits($this->latestResult?->download)); + + $download = explode(' ', $download); + @endphp + + {{ $download[0] }} + {{ $download[1].'ps' }} +

+
+ + + + {{ __('general.upload') }} + + + @php + $uploadBenchmark = Arr::get($this->latestResult->benchmarks, 'upload'); + $uploadBenchmarkPassed = Arr::get($uploadBenchmark, 'passed', false); + @endphp + + @filled($uploadBenchmark) + + $uploadBenchmarkPassed, + 'text-amber-500 dark:text-amber-400' => ! $uploadBenchmarkPassed, + ]) title="Benchmark {{ $uploadBenchmarkPassed ? 'passed' : 'failed' }}"> + @if (! $uploadBenchmarkPassed) + + @endif + {{ Arr::get($uploadBenchmark, 'value').' '.str(Arr::get($uploadBenchmark, 'unit'))->title() }} + + + @endfilled + +

+ @php + $upload = \App\Helpers\Bitrate::formatBits(\App\Helpers\Bitrate::bytesToBits($this->latestResult?->upload)); + + $upload = explode(' ', $upload); + @endphp + + {{ $upload[0] }} + {{ $upload[1].'ps' }} +

+
+ + + + {{ __('general.ping') }} + + + @php + $pingBenchmark = Arr::get($this->latestResult->benchmarks, 'ping'); + $pingBenchmarkPassed = Arr::get($pingBenchmark, 'passed', false); + @endphp + + @filled($pingBenchmark) + + $pingBenchmarkPassed, + 'text-amber-500 dark:text-amber-400' => ! $pingBenchmarkPassed, + ]) title="Benchmark {{ $pingBenchmarkPassed ? 'passed' : 'failed' }}"> + @if (! $pingBenchmarkPassed) + + @endif + {{ Arr::get($pingBenchmark, 'value').' '.str(Arr::get($pingBenchmark, 'unit')) }} + + + @endfilled + +

+ {{ $this->latestResult?->ping }} + ms +

+
+
+ @endfilled +
\ No newline at end of file diff --git a/resources/views/livewire/platform-stats.blade.php b/resources/views/livewire/platform-stats.blade.php new file mode 100644 index 000000000..c9ddfa9c9 --- /dev/null +++ b/resources/views/livewire/platform-stats.blade.php @@ -0,0 +1,68 @@ +
+
+

+ + {{ __('general.statistics') }} +

+ + {{-- +
+

Quota Usage

+ Edit +
+ +
+
+ Bandwidth + 450MB of 1 GB +
+ +
+
+
+
+
--}} + + @filled($this->nextSpeedtest) + + + Next Speedtest in + + +

{{ $this->nextSpeedtest->diffForHumans() }}

+
+ @else + + + Next Speedtest in + + +

No scheduled speedtests

+
+ @endfilled + + + + Total tests + + +

{{ $this->platformStats['total'] }}

+
+ + + + Total successful tests + + +

{{ $this->platformStats['completed'] }}

+
+ + + + Total failed tests + + +

{{ $this->platformStats['failed'] }}

+
+
+
diff --git a/resources/views/livewire/topbar/actions.blade.php b/resources/views/livewire/topbar/actions.blade.php new file mode 100644 index 000000000..b27fb1767 --- /dev/null +++ b/resources/views/livewire/topbar/actions.blade.php @@ -0,0 +1,10 @@ +
+
+ {{ $this->speedtestAction }} + + {{ $this->dashboardAction }} + +
+ + +
diff --git a/resources/views/livewire/topbar/run-speedtest-action.blade.php b/resources/views/livewire/topbar/run-speedtest-action.blade.php deleted file mode 100644 index 3c88447e0..000000000 --- a/resources/views/livewire/topbar/run-speedtest-action.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -
-
- {{ $this->dashboard }} - - {{ $this->speedtestAction }} -
- - -
From 395b206ddf9aaf317509c0dfb8408908a78ddbe6 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 5 Dec 2025 15:51:23 -0500 Subject: [PATCH 03/63] Add packet loss to latest result stats (#2495) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../livewire/latest-result-stats.blade.php | 41 ++++++------------- .../views/livewire/platform-stats.blade.php | 2 +- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index a8a32bcc7..d4d367905 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -16,34 +16,6 @@
- - - Benchmark status - - -
- @if($this->latestResult->healthy === true) -
-
-
- - {{ __('general.healthy') }} - @elseif($this->latestResult->healthy === false) -
-
-
- - {{ __('general.unhealthy') }} - @else -
-
-
- - {{ __('general.not_measured') }} - @endif -
-
- {{ __('general.download') }} @@ -148,6 +120,17 @@ ms

+ + + + {{ __('results.packet_loss') }} + + +

+ {{ $this->latestResult?->packet_loss }} + % +

+
@endfilled - \ No newline at end of file + diff --git a/resources/views/livewire/platform-stats.blade.php b/resources/views/livewire/platform-stats.blade.php index c9ddfa9c9..c120845b6 100644 --- a/resources/views/livewire/platform-stats.blade.php +++ b/resources/views/livewire/platform-stats.blade.php @@ -51,7 +51,7 @@ - Total successful tests + Total completed tests

{{ $this->platformStats['completed'] }}

From 4ac18918d6bafd68085d801deeb3a76e43c59285 Mon Sep 17 00:00:00 2001 From: Gurjit Singh Date: Fri, 5 Dec 2025 23:10:26 +0100 Subject: [PATCH 04/63] Fix typo: helpertext -> helperText in DataIntegration settings (#2497) --- app/Filament/Pages/Settings/DataIntegration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Pages/Settings/DataIntegration.php b/app/Filament/Pages/Settings/DataIntegration.php index e680627dd..67a66bf52 100644 --- a/app/Filament/Pages/Settings/DataIntegration.php +++ b/app/Filament/Pages/Settings/DataIntegration.php @@ -62,7 +62,7 @@ public function form(Schema $schema): Schema ->schema([ Toggle::make('influxdb_v2_enabled') ->label(__('settings/data_integration.influxdb_v2_enabled')) - ->helpertext(__('settings/data_integration.influxdb_v2_description')) + ->helperText(__('settings/data_integration.influxdb_v2_description')) ->reactive() ->columnSpanFull(), Grid::make(['default' => 1, 'md' => 3]) From aac527a8b481bbcbc017da025f32bd63b1a90ad0 Mon Sep 17 00:00:00 2001 From: Gurjit Singh Date: Fri, 5 Dec 2025 23:58:43 +0100 Subject: [PATCH 05/63] Fix grammar error in English translation (#2499) --- lang/en/settings/data_integration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/settings/data_integration.php b/lang/en/settings/data_integration.php index f55a77441..60ee353d6 100644 --- a/lang/en/settings/data_integration.php +++ b/lang/en/settings/data_integration.php @@ -28,7 +28,7 @@ 'influxdb_test_success_body' => 'Test data has been sent to InfluxDB, check if the data was received.', // Bulk write notifications - 'influxdb_bulk_write_failed' => 'Failed to build write to Influxdb.', + 'influxdb_bulk_write_failed' => 'Failed to bulk write to Influxdb.', 'influxdb_bulk_write_failed_body' => 'Check the logs for more details.', 'influxdb_bulk_write_success' => 'Finished bulk data load to Influxdb.', 'influxdb_bulk_write_success_body' => 'Data has been sent to InfluxDB, check if the data was received.', From e2bc2be246abc5b2389b7b1f57f508fbf4ac75bc Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 5 Dec 2025 18:58:49 -0500 Subject: [PATCH 06/63] Add latest result date to latest result stats section (#2501) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../livewire/latest-result-stats.blade.php | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index d4d367905..425963821 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -1,19 +1,25 @@
@filled($this->latestResult)
-
-

- - Latest result -

- - - {{ __('general.view') }} - +
+
+
+

+ + Latest result +

+ +

{{ $this->latestResult->created_at->format(config('app.datetime_format')) }}

+
+ + + {{ __('general.view') }} + +
From bc0064a4ad772eedfe39712e730b3599e06e57f7 Mon Sep 17 00:00:00 2001 From: Gurjit Singh Date: Sat, 6 Dec 2025 01:00:44 +0100 Subject: [PATCH 07/63] Refactor: Use strict comparison in chart widget filters (#2498) Co-authored-by: Alex Justesen --- app/Filament/Widgets/RecentDownloadChartWidget.php | 6 +++--- app/Filament/Widgets/RecentDownloadLatencyChartWidget.php | 6 +++--- app/Filament/Widgets/RecentJitterChartWidget.php | 6 +++--- app/Filament/Widgets/RecentPingChartWidget.php | 6 +++--- app/Filament/Widgets/RecentUploadChartWidget.php | 6 +++--- app/Filament/Widgets/RecentUploadLatencyChartWidget.php | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/Filament/Widgets/RecentDownloadChartWidget.php b/app/Filament/Widgets/RecentDownloadChartWidget.php index da62367ac..098708648 100644 --- a/app/Filament/Widgets/RecentDownloadChartWidget.php +++ b/app/Filament/Widgets/RecentDownloadChartWidget.php @@ -38,13 +38,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'download', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') diff --git a/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php b/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php index 1e73db54f..e06c86c87 100644 --- a/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php +++ b/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php @@ -36,13 +36,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'data', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') diff --git a/app/Filament/Widgets/RecentJitterChartWidget.php b/app/Filament/Widgets/RecentJitterChartWidget.php index ff598cafd..03dd59b13 100644 --- a/app/Filament/Widgets/RecentJitterChartWidget.php +++ b/app/Filament/Widgets/RecentJitterChartWidget.php @@ -36,13 +36,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'data', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') diff --git a/app/Filament/Widgets/RecentPingChartWidget.php b/app/Filament/Widgets/RecentPingChartWidget.php index b31c02530..096a190ec 100644 --- a/app/Filament/Widgets/RecentPingChartWidget.php +++ b/app/Filament/Widgets/RecentPingChartWidget.php @@ -37,13 +37,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'ping', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') diff --git a/app/Filament/Widgets/RecentUploadChartWidget.php b/app/Filament/Widgets/RecentUploadChartWidget.php index df3d15ffb..1bb96eb04 100644 --- a/app/Filament/Widgets/RecentUploadChartWidget.php +++ b/app/Filament/Widgets/RecentUploadChartWidget.php @@ -38,13 +38,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'upload', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') diff --git a/app/Filament/Widgets/RecentUploadLatencyChartWidget.php b/app/Filament/Widgets/RecentUploadLatencyChartWidget.php index 5b79fa0c5..90315ddd9 100644 --- a/app/Filament/Widgets/RecentUploadLatencyChartWidget.php +++ b/app/Filament/Widgets/RecentUploadLatencyChartWidget.php @@ -36,13 +36,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'data', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') From bd108a996caf413f5a91342952dd5ff27fd16676 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 5 Dec 2025 19:12:40 -0500 Subject: [PATCH 08/63] Show view result when authenticated (#2502) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- lang/en/auth.php | 1 + resources/views/layouts/app.blade.php | 26 ++++++++++++++----- .../livewire/latest-result-stats.blade.php | 16 +++++++----- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lang/en/auth.php b/lang/en/auth.php index 6598e2c06..f0d112f16 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -13,6 +13,7 @@ | */ + 'sign_in' => 'Sign in', 'failed' => 'These credentials do not match our records.', 'password' => 'The provided password is incorrect.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index cc94b33d3..1d23aaac1 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -91,12 +91,26 @@ class="p-2 rounded-md transition-all"
- - Admin Panel - + @auth + + {{ __('general.admin') }} + + @else + + {{ __('auth.sign_in') }} + + @endauth
diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index 425963821..4c74e4d75 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -12,13 +12,15 @@

{{ $this->latestResult->created_at->format(config('app.datetime_format')) }}

- - {{ __('general.view') }} - + @auth + + {{ __('general.view') }} + + @endauth From 5b4ba87ee5ea747fbc6c8caa1749d33a186e6edb Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 10:22:36 -0500 Subject: [PATCH 09/63] Show platform stats only when authenticated (#2505) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- resources/views/dashboard.blade.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index b3d3762b7..e31a16ba8 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,6 +1,8 @@
- + @auth + + @endauth From 9a101363d59b65a213b1952229a5f470902f09bf Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 10:27:49 -0500 Subject: [PATCH 10/63] Update benchmark status indicators to use check and alert icons (#2506) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../livewire/latest-result-stats.blade.php | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index 4c74e4d75..d6a18b12c 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -37,12 +37,14 @@ @filled($downloadBenchmark) $downloadBenchmarkPassed, + 'inline-flex items-center gap-x-1 text-xs font-medium underline decoration-dotted decoration-1 decoration-zinc-500 underline-offset-4', + 'text-green-500 dark:text-green-400' => $downloadBenchmarkPassed, 'text-amber-500 dark:text-amber-400' => ! $downloadBenchmarkPassed, ]) title="Benchmark {{ $downloadBenchmarkPassed ? 'passed' : 'failed' }}"> - @if (! $downloadBenchmarkPassed) - + @if ($downloadBenchmarkPassed) + + @else + @endif {{ Arr::get($downloadBenchmark, 'value').' '.str(Arr::get($downloadBenchmark, 'unit'))->title() }} @@ -74,11 +76,13 @@ @filled($uploadBenchmark) $uploadBenchmarkPassed, + 'inline-flex items-center gap-x-1 text-xs font-medium underline decoration-dotted decoration-1 decoration-zinc-500 underline-offset-4', + 'text-green-500 dark:text-green-400' => $uploadBenchmarkPassed, 'text-amber-500 dark:text-amber-400' => ! $uploadBenchmarkPassed, ]) title="Benchmark {{ $uploadBenchmarkPassed ? 'passed' : 'failed' }}"> - @if (! $uploadBenchmarkPassed) + @if ($uploadBenchmarkPassed) + + @else @endif {{ Arr::get($uploadBenchmark, 'value').' '.str(Arr::get($uploadBenchmark, 'unit'))->title() }} @@ -111,11 +115,13 @@ @filled($pingBenchmark) $pingBenchmarkPassed, + 'inline-flex items-center gap-x-1 text-xs font-medium underline decoration-dotted decoration-1 decoration-zinc-500 underline-offset-4', + 'text-green-500 dark:text-green-400' => $pingBenchmarkPassed, 'text-amber-500 dark:text-amber-400' => ! $pingBenchmarkPassed, ]) title="Benchmark {{ $pingBenchmarkPassed ? 'passed' : 'failed' }}"> - @if (! $pingBenchmarkPassed) + @if ($pingBenchmarkPassed) + + @else @endif {{ Arr::get($pingBenchmark, 'value').' '.str(Arr::get($pingBenchmark, 'unit')) }} From a06b231eb6ee7c6dc7409fcbb5d0998c74e0cabf Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 11:16:29 -0500 Subject: [PATCH 11/63] Show banner for next scheduled test (#2507) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Livewire/NextSpeedtestBanner.php | 22 ++++++ app/Livewire/PlatformStats.php | 14 ---- app/Services/ScheduledSpeedtestService.php | 29 ++++++++ resources/views/dashboard.blade.php | 2 + .../views/filament/pages/dashboard.blade.php | 2 + .../livewire/next-speedtest-banner.blade.php | 17 +++++ .../views/livewire/platform-stats.blade.php | 26 ++----- .../ScheduledSpeedtestServiceTest.php | 67 +++++++++++++++++++ 8 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 app/Livewire/NextSpeedtestBanner.php create mode 100644 app/Services/ScheduledSpeedtestService.php create mode 100644 resources/views/livewire/next-speedtest-banner.blade.php create mode 100644 tests/Unit/Services/ScheduledSpeedtestServiceTest.php diff --git a/app/Livewire/NextSpeedtestBanner.php b/app/Livewire/NextSpeedtestBanner.php new file mode 100644 index 000000000..2e20874ac --- /dev/null +++ b/app/Livewire/NextSpeedtestBanner.php @@ -0,0 +1,22 @@ +getNextRunDate(timeZone: config('app.display_timezone'))); - } - - return null; - } - #[Computed] public function platformStats(): array { diff --git a/app/Services/ScheduledSpeedtestService.php b/app/Services/ScheduledSpeedtestService.php new file mode 100644 index 000000000..8f9b85fc6 --- /dev/null +++ b/app/Services/ScheduledSpeedtestService.php @@ -0,0 +1,29 @@ +getNextRunDate(timeZone: config('app.display_timezone')) + ); + } +} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index e31a16ba8..e1b26abed 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,5 +1,7 @@
+ + @auth @endauth diff --git a/resources/views/filament/pages/dashboard.blade.php b/resources/views/filament/pages/dashboard.blade.php index 42eb6bbf5..42a849976 100644 --- a/resources/views/filament/pages/dashboard.blade.php +++ b/resources/views/filament/pages/dashboard.blade.php @@ -1,5 +1,7 @@
+ + diff --git a/resources/views/livewire/next-speedtest-banner.blade.php b/resources/views/livewire/next-speedtest-banner.blade.php new file mode 100644 index 000000000..ce65f81d7 --- /dev/null +++ b/resources/views/livewire/next-speedtest-banner.blade.php @@ -0,0 +1,17 @@ +
+ @if ($this->nextSpeedtest) +
+
+
+ +
+ +
+

+ Next scheduled test at {{ $this->nextSpeedtest->timezone(config('app.display_timezone'))->format('F jS, Y, g:i a') }}. +

+
+
+
+ @endif +
diff --git a/resources/views/livewire/platform-stats.blade.php b/resources/views/livewire/platform-stats.blade.php index c120845b6..567c1f31a 100644 --- a/resources/views/livewire/platform-stats.blade.php +++ b/resources/views/livewire/platform-stats.blade.php @@ -1,5 +1,5 @@
-
+

{{ __('general.statistics') }} @@ -23,25 +23,7 @@

--}} - @filled($this->nextSpeedtest) - - - Next Speedtest in - - -

{{ $this->nextSpeedtest->diffForHumans() }}

-
- @else - - - Next Speedtest in - - -

No scheduled speedtests

-
- @endfilled - - + Total tests @@ -49,7 +31,7 @@

{{ $this->platformStats['total'] }}

- + Total completed tests @@ -57,7 +39,7 @@

{{ $this->platformStats['completed'] }}

- + Total failed tests diff --git a/tests/Unit/Services/ScheduledSpeedtestServiceTest.php b/tests/Unit/Services/ScheduledSpeedtestServiceTest.php new file mode 100644 index 000000000..e8c4cc5af --- /dev/null +++ b/tests/Unit/Services/ScheduledSpeedtestServiceTest.php @@ -0,0 +1,67 @@ +set('speedtest.schedule', null); + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeNull(); +}); + +test('returns null when schedule config is false', function () { + config()->set('speedtest.schedule', false); + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeNull(); +}); + +test('returns null when schedule config is blank string', function () { + config()->set('speedtest.schedule', ''); + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeNull(); +}); + +test('returns Carbon instance when schedule is configured', function () { + config()->set('speedtest.schedule', '*/5 * * * *'); // Every 5 minutes + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeInstanceOf(Carbon::class); +}); + +test('returns correct next scheduled time for hourly cron', function () { + config()->set('speedtest.schedule', '0 * * * *'); // Every hour at minute 0 + config()->set('app.display_timezone', 'UTC'); + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeInstanceOf(Carbon::class); + expect($result->minute)->toBe(0); +}); + +test('returns correct next scheduled time for daily cron', function () { + config()->set('speedtest.schedule', '0 0 * * *'); // Every day at midnight + config()->set('app.display_timezone', 'UTC'); + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeInstanceOf(Carbon::class); + expect($result->hour)->toBe(0); + expect($result->minute)->toBe(0); +}); + +test('returns future date for next scheduled test', function () { + config()->set('speedtest.schedule', '*/5 * * * *'); // Every 5 minutes + config()->set('app.display_timezone', 'UTC'); + + $result = ScheduledSpeedtestService::getNextScheduledTest(); + + expect($result)->toBeInstanceOf(Carbon::class); + expect($result->isFuture())->toBeTrue(); +}); From b70e3f513dc22370411c6a2e4dd7e9845482cdb8 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 11:20:07 -0500 Subject: [PATCH 12/63] Release v1.12.1 (#2508) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 8fd6c0f81..85cc44e97 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-05'), + 'build_date' => Carbon::parse('2025-12-06'), - 'build_version' => 'v1.12.0', + 'build_version' => 'v1.12.1', 'content_width' => env('CONTENT_WIDTH', '7xl'), From 2c4f7f519d70d7c48f85f5b68761c3a5620d4881 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 11:45:29 -0500 Subject: [PATCH 13/63] Remove high and low latency columns from results table (#2510) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../Resources/Results/Tables/ResultTable.php | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/app/Filament/Resources/Results/Tables/ResultTable.php b/app/Filament/Resources/Results/Tables/ResultTable.php index 7c1f4765e..f08c9d992 100644 --- a/app/Filament/Resources/Results/Tables/ResultTable.php +++ b/app/Filament/Resources/Results/Tables/ResultTable.php @@ -102,26 +102,6 @@ public static function table(Table $table): Table return number_format((float) $state, 0, '.', '').' ms'; }), - TextColumn::make('data.download.latency.high') - ->label(__('results.download_latency_high')) - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->download->latency->high', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - - TextColumn::make('data.download.latency.low') - ->label(__('results.download_latency_low')) - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->download->latency->low', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - TextColumn::make('data.upload.latency.jitter') ->label(__('results.upload_latency_jitter')) ->toggleable(isToggledHiddenByDefault: true) @@ -132,26 +112,6 @@ public static function table(Table $table): Table return number_format((float) $state, 0, '.', '').' ms'; }), - TextColumn::make('data.upload.latency.high') - ->label(__('results.upload_latency_high')) - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->upload->latency->high', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - - TextColumn::make('data.upload.latency.low') - ->label(__('results.upload_latency_low')) - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->upload->latency->low', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - IconColumn::make('healthy') ->label(__('general.healthy')) ->boolean() From a280795a24543739f884749f8386cc44b248df21 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 11:50:28 -0500 Subject: [PATCH 14/63] Remove deferred loading from results table (#2511) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Filament/Resources/Results/Tables/ResultTable.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/Filament/Resources/Results/Tables/ResultTable.php b/app/Filament/Resources/Results/Tables/ResultTable.php index f08c9d992..65f31a731 100644 --- a/app/Filament/Resources/Results/Tables/ResultTable.php +++ b/app/Filament/Resources/Results/Tables/ResultTable.php @@ -135,8 +135,6 @@ public static function table(Table $table): Table ->toggleable(isToggledHiddenByDefault: true) ->sortable(), ]) - ->deferFilters(false) - ->deferColumnManager(false) ->filters([ Filter::make('created_at') ->label(__('general.created_at')) @@ -276,7 +274,6 @@ public static function table(Table $table): Table ]) ->defaultSort('id', 'desc') ->paginationPageOptions([10, 25, 50]) - ->deferLoading() ->poll('60s'); } } From 0cd47d4256962c196cccd3a609695ab830614792 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 12:15:35 -0500 Subject: [PATCH 15/63] Remove truncate results action and related job from results table (#2512) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../Resources/Results/Tables/ResultTable.php | 15 ------ app/Jobs/TruncateResults.php | 48 ------------------- lang/en/results.php | 3 -- 3 files changed, 66 deletions(-) delete mode 100644 app/Jobs/TruncateResults.php diff --git a/app/Filament/Resources/Results/Tables/ResultTable.php b/app/Filament/Resources/Results/Tables/ResultTable.php index 65f31a731..c7d8adbe2 100644 --- a/app/Filament/Resources/Results/Tables/ResultTable.php +++ b/app/Filament/Resources/Results/Tables/ResultTable.php @@ -5,7 +5,6 @@ use App\Enums\ResultStatus; use App\Filament\Exports\ResultExporter; use App\Helpers\Number; -use App\Jobs\TruncateResults; use App\Models\Result; use Filament\Actions\Action; use Filament\Actions\ActionGroup; @@ -251,26 +250,12 @@ public static function table(Table $table): Table ]) ->toolbarActions([ DeleteBulkAction::make(), - ]) - ->headerActions([ ExportAction::make() ->exporter(ResultExporter::class) ->columnMapping(false) ->modalHeading(__('results.export_all_results')) ->modalDescription(__('results.export_all_results_description')) ->fileName(fn (): string => 'results-'.now()->timestamp), - ActionGroup::make([ - Action::make('truncate') - ->label(__('results.truncate_results')) - ->action(fn () => TruncateResults::dispatch(Auth::user())) - ->requiresConfirmation() - ->modalHeading(__('results.truncate_results')) - ->modalDescription(__('results.truncate_results_description')) - ->color('danger') - ->icon('heroicon-o-trash') - ->hidden(fn (): bool => ! Auth::user()->is_admin), - ]) - ->dropdownPlacement('left-start'), ]) ->defaultSort('id', 'desc') ->paginationPageOptions([10, 25, 50]) diff --git a/app/Jobs/TruncateResults.php b/app/Jobs/TruncateResults.php deleted file mode 100644 index e597fc2d3..000000000 --- a/app/Jobs/TruncateResults.php +++ /dev/null @@ -1,48 +0,0 @@ -truncate(); - } catch (Throwable $th) { - $this->fail($th); - - return; - } - - Notification::make() - ->title(__('results.truncate_results_success')) - ->success() - ->sendToDatabase($this->user); - } -} diff --git a/lang/en/results.php b/lang/en/results.php index 65f81386f..8b625d37a 100644 --- a/lang/en/results.php +++ b/lang/en/results.php @@ -55,9 +55,6 @@ // Actions 'update_comments' => 'Update comments', - 'truncate_results' => 'Truncate results', - 'truncate_results_description' => 'Are you sure you want to truncate all results? This action is irreversible.', - 'truncate_results_success' => 'Results table truncated!', 'view_on_speedtest_net' => 'View on Speedtest.net', // Notifications From 9995400963705da9ad81b11029012d4b4308df50 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 19:02:30 -0500 Subject: [PATCH 16/63] Round packet loss value to two decimal places in latest result stats (#2515) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- resources/views/livewire/latest-result-stats.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index d6a18b12c..c6dfbd850 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -141,7 +141,7 @@

- {{ $this->latestResult?->packet_loss }} + {{ round($this->latestResult?->packet_loss, 2) }} %

From 618d48221d6c9eeb7438f465726d9b18a8871ed0 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sat, 6 Dec 2025 19:06:25 -0500 Subject: [PATCH 17/63] Release v1.12.2 (#2516) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/speedtest.php b/config/speedtest.php index 85cc44e97..413914928 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -8,7 +8,7 @@ */ 'build_date' => Carbon::parse('2025-12-06'), - 'build_version' => 'v1.12.1', + 'build_version' => 'v1.12.2', 'content_width' => env('CONTENT_WIDTH', '7xl'), From 89323e8c32469866d0df302eec24f70729762518 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Mon, 8 Dec 2025 14:21:19 +0100 Subject: [PATCH 18/63] chore: round ping value to two decimal places in latest result stats (#2520) --- resources/views/livewire/latest-result-stats.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index c6dfbd850..983714062 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -130,7 +130,7 @@ @endfilled

- {{ $this->latestResult?->ping }} + {{ round($this->latestResult?->ping, 2) }} ms

From 5fa65d0b60efa3678f43765e5d4b0b013f541419 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Mon, 8 Dec 2025 14:34:39 +0100 Subject: [PATCH 19/63] bug; fix last result not using the display_timezone env var (#2522) Co-authored-by: Alex Justesen --- resources/views/livewire/latest-result-stats.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index 983714062..a3cc2b708 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -9,7 +9,7 @@ Latest result -

{{ $this->latestResult->created_at->format(config('app.datetime_format')) }}

+

{{ $this->latestResult->created_at->timezone(config('app.display_timezone'))->format(config('app.datetime_format')) }}

@auth From 61df96b2c4c7ede9e7f55c1a0eea47912166a3fe Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Mon, 8 Dec 2025 11:33:18 -0500 Subject: [PATCH 20/63] Combine result server columns (#2526) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../Resources/Results/Tables/ResultTable.php | 42 ++++++++++++------- .../Tables/Columns/ResultServerColumn.php | 28 +++++++++++++ lang/en/general.php | 2 + .../columns/result-server-column.blade.php | 7 ++++ 4 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 app/Filament/Tables/Columns/ResultServerColumn.php create mode 100644 resources/views/filament/tables/columns/result-server-column.blade.php diff --git a/app/Filament/Resources/Results/Tables/ResultTable.php b/app/Filament/Resources/Results/Tables/ResultTable.php index c7d8adbe2..6808e9f7d 100644 --- a/app/Filament/Resources/Results/Tables/ResultTable.php +++ b/app/Filament/Resources/Results/Tables/ResultTable.php @@ -4,6 +4,7 @@ use App\Enums\ResultStatus; use App\Filament\Exports\ResultExporter; +use App\Filament\Tables\Columns\ResultServerColumn; use App\Helpers\Number; use App\Models\Result; use Filament\Actions\Action; @@ -49,19 +50,9 @@ public static function table(Table $table): Table ->label(__('results.service')) ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('data.server.id') - ->label(__('results.server_id')) - ->toggleable(isToggledHiddenByDefault: false) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->server->id', $direction); - }), - - TextColumn::make('data.server.name') - ->label(__('results.server_name')) - ->toggleable(isToggledHiddenByDefault: false) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->server->name', $direction); - }), + ResultServerColumn::make('server') + ->label(__('general.server')) + ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('download') ->label(__('results.download')) @@ -131,7 +122,7 @@ public static function table(Table $table): Table ->label(__('general.created_at')) ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) - ->toggleable(isToggledHiddenByDefault: true) + ->toggleable(isToggledHiddenByDefault: false) ->sortable(), ]) ->filters([ @@ -158,6 +149,7 @@ public static function table(Table $table): Table fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date), ); }), + SelectFilter::make('ip_address') ->label(__('results.ip_address')) ->multiple() @@ -175,6 +167,7 @@ public static function table(Table $table): Table ->toArray(); }) ->attribute('data->interface->externalIp'), + SelectFilter::make('server_name') ->label(__('results.server_name')) ->multiple() @@ -192,6 +185,25 @@ public static function table(Table $table): Table ->toArray(); }) ->attribute('data->server->name'), + + SelectFilter::make('server_id') + ->label(__('results.server_id')) + ->multiple() + ->options(function (): array { + return Result::query() + ->select('data->server->id AS data_server_id') + ->whereNotNull('data->server->id') + ->where('status', '=', ResultStatus::Completed) + ->distinct() + ->orderBy('data->server->id') + ->get() + ->mapWithKeys(function (Result $item, int $key) { + return [$item['data_server_id'] => $item['data_server_id']]; + }) + ->toArray(); + }) + ->attribute('data->server->id'), + TernaryFilter::make('scheduled') ->label(__('results.scheduled')) ->nullable() @@ -203,10 +215,12 @@ public static function table(Table $table): Table false: fn (Builder $query) => $query->where('scheduled', false), blank: fn (Builder $query) => $query, ), + SelectFilter::make('status') ->label(__('general.status')) ->multiple() ->options(ResultStatus::class), + TernaryFilter::make('healthy') ->label(__('general.healthy')) ->nullable() diff --git a/app/Filament/Tables/Columns/ResultServerColumn.php b/app/Filament/Tables/Columns/ResultServerColumn.php new file mode 100644 index 000000000..357951e15 --- /dev/null +++ b/app/Filament/Tables/Columns/ResultServerColumn.php @@ -0,0 +1,28 @@ +serverName = $this->record->server_name; + + return $this->serverName; + } + + public function getServerId(): ?int + { + $this->serverId = $this->record->server_id; + + return $this->serverId; + } +} diff --git a/lang/en/general.php b/lang/en/general.php index 33c212e21..9b8aa7d95 100644 --- a/lang/en/general.php +++ b/lang/en/general.php @@ -37,6 +37,8 @@ 'created_at' => 'Created at', 'updated_at' => 'Updated at', 'url' => 'URL', + 'server' => 'Server', + 'servers' => 'Servers', 'stats' => 'Stats', 'statistics' => 'Statistics', diff --git a/resources/views/filament/tables/columns/result-server-column.blade.php b/resources/views/filament/tables/columns/result-server-column.blade.php new file mode 100644 index 000000000..f16445828 --- /dev/null +++ b/resources/views/filament/tables/columns/result-server-column.blade.php @@ -0,0 +1,7 @@ +
+ {{ $getServerName() }} + + @isset($getServerId) + (#{{ $getServerId() }}) + @endisset +
From 633d3b3c71f2641a9c6f34a126424e86a1e98341 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 9 Dec 2025 01:46:41 +0100 Subject: [PATCH 21/63] chore: Switch Apprise to markdown (#2531) --- app/Listeners/ProcessCompletedSpeedtest.php | 2 +- app/Listeners/ProcessUnhealthySpeedtest.php | 2 +- .../Apprise/SpeedtestNotification.php | 4 +++- .../Apprise/TestNotification.php | 8 +++++-- .../apprise/speedtest-completed.blade.php | 22 ++++++++++--------- .../apprise/speedtest-threshold.blade.php | 15 ++++++++----- 6 files changed, 33 insertions(+), 20 deletions(-) diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 66b927f1a..1b72f3053 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -88,7 +88,7 @@ private function notifyAppriseChannels(Result $result): void } Notification::route('apprise_urls', $channelUrl) - ->notify(new SpeedtestNotification($title, $body, 'info')); + ->notify(new SpeedtestNotification($title, $body, 'info', 'markdown')); } } diff --git a/app/Listeners/ProcessUnhealthySpeedtest.php b/app/Listeners/ProcessUnhealthySpeedtest.php index 68b9b6a9b..4f51e4267 100644 --- a/app/Listeners/ProcessUnhealthySpeedtest.php +++ b/app/Listeners/ProcessUnhealthySpeedtest.php @@ -110,7 +110,7 @@ private function notifyAppriseChannels(Result $result): void } Notification::route('apprise_urls', $channelUrl) - ->notify(new SpeedtestNotification($title, $body, 'warning')); + ->notify(new SpeedtestNotification($title, $body, 'warning', 'markdown')); } } diff --git a/app/Notifications/Apprise/SpeedtestNotification.php b/app/Notifications/Apprise/SpeedtestNotification.php index 3c2ffb3cd..710466e57 100644 --- a/app/Notifications/Apprise/SpeedtestNotification.php +++ b/app/Notifications/Apprise/SpeedtestNotification.php @@ -14,6 +14,7 @@ public function __construct( public string $title, public string $body, public string $type = 'info', + public string $format = 'markdown', ) {} /** @@ -35,6 +36,7 @@ public function toApprise(object $notifiable): AppriseMessage ->urls($notifiable->routes['apprise_urls']) ->title($this->title) ->body($this->body) - ->type($this->type); + ->type($this->type) + ->format($this->format); } } diff --git a/app/Notifications/Apprise/TestNotification.php b/app/Notifications/Apprise/TestNotification.php index f07810fcc..8ba9011cc 100644 --- a/app/Notifications/Apprise/TestNotification.php +++ b/app/Notifications/Apprise/TestNotification.php @@ -25,10 +25,14 @@ public function via(object $notifiable): array */ public function toApprise(object $notifiable): AppriseMessage { + $body = '👋 This is a test notification from **'.config('app.name')."**.\n\n"; + $body .= "If you're seeing this, your Apprise notification channel is configured correctly!\n\n"; + return AppriseMessage::create() ->urls($notifiable->routes['apprise_urls']) ->title('Test Notification') - ->body('👋 Testing the Apprise notification channel.') - ->type('info'); + ->body($body) + ->type('info') + ->format('markdown'); } } diff --git a/resources/views/apprise/speedtest-completed.blade.php b/resources/views/apprise/speedtest-completed.blade.php index 6363ee642..2efca5492 100644 --- a/resources/views/apprise/speedtest-completed.blade.php +++ b/resources/views/apprise/speedtest-completed.blade.php @@ -1,11 +1,13 @@ -A new speedtest on {{ config('app.name') }} was completed using {{ $service }}. +A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}**. -Server name: {{ $serverName }} -Server ID: {{ $serverId }} -ISP: {{ $isp }} -Ping: {{ $ping }} -Download: {{ $download }} -Upload: {{ $upload }} -Packet Loss: {{ $packetLoss }} % -Ookla Speedtest: {{ $speedtest_url }} -URL: {{ $url }} +### Results +- **Server:** {{ $serverName }} (ID: {{ $serverId }}) +- **ISP:** {{ $isp }} +- **Ping:** {{ $ping }} +- **Download:** {{ $download }} +- **Upload:** {{ $upload }} +- **Packet Loss:** {{ $packetLoss }}% + +### Links +- [View Ookla Results]({{ $speedtest_url }}) +- [View Dashboard]({{ $url }}) diff --git a/resources/views/apprise/speedtest-threshold.blade.php b/resources/views/apprise/speedtest-threshold.blade.php index 6d0bb4926..14a719d0e 100644 --- a/resources/views/apprise/speedtest-threshold.blade.php +++ b/resources/views/apprise/speedtest-threshold.blade.php @@ -1,7 +1,12 @@ -A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. - +A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached +### Failed Metrics @foreach ($metrics as $item) -- **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }} +- **{{ $item['name'] }}** + - **Threshold:** {{ $item['threshold'] }} | **Actual:** {{ $item['value'] }} @endforeach -- **Ookla Speedtest:** {{ $speedtest_url }} -- **URL:** {{ $url }} +### Server Information +- **Server:** {{ $serverName }} (ID: {{ $serverId }}) +- **ISP:** {{ $isp }} +### Links +- [View Ookla Results]({{ $speedtest_url }}) +- [View Dashboard]({{ $url }}) From 8e869a085db55c3bc78972972b373bddce7861a1 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Mon, 8 Dec 2025 20:26:10 -0500 Subject: [PATCH 22/63] Release v1.12.3 (#2532) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 413914928..dfaf9ac76 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-06'), + 'build_date' => Carbon::parse('2025-12-08'), - 'build_version' => 'v1.12.2', + 'build_version' => 'v1.12.3', 'content_width' => env('CONTENT_WIDTH', '7xl'), From 49fdce4f8159a817cc7a2609cdab53e9776c5c78 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 9 Dec 2025 18:59:03 -0500 Subject: [PATCH 23/63] Added version to dashboard footer (#2537) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- resources/views/layouts/app.blade.php | 81 +------------------ .../views/layouts/partials/footer.blade.php | 15 ++++ .../views/layouts/partials/header.blade.php | 78 ++++++++++++++++++ 3 files changed, 96 insertions(+), 78 deletions(-) create mode 100644 resources/views/layouts/partials/footer.blade.php create mode 100644 resources/views/layouts/partials/header.blade.php diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 1d23aaac1..078505014 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -35,86 +35,11 @@
-
-
-

{{ $title ?? 'Page Title' }} - {{ config('app.name') }}

-
- -
-
- - - - - -
- - @auth - - {{ __('general.admin') }} - - @else - - {{ __('auth.sign_in') }} - - @endauth -
-
+ @include('layouts.partials.header') {{ $slot }} + + @include('layouts.partials.footer')
{{-- Scripts --}} diff --git a/resources/views/layouts/partials/footer.blade.php b/resources/views/layouts/partials/footer.blade.php new file mode 100644 index 000000000..bca1d3815 --- /dev/null +++ b/resources/views/layouts/partials/footer.blade.php @@ -0,0 +1,15 @@ + diff --git a/resources/views/layouts/partials/header.blade.php b/resources/views/layouts/partials/header.blade.php new file mode 100644 index 000000000..b8eed7ffc --- /dev/null +++ b/resources/views/layouts/partials/header.blade.php @@ -0,0 +1,78 @@ +
+
+

{{ $title ?? 'Page Title' }} - {{ config('app.name') }}

+
+ +
+
+ + + + + +
+ + @auth + + {{ __('general.admin') }} + + @else + + {{ __('auth.sign_in') }} + + @endauth +
+
From 73f44abb3cf6b3a2918144e7e201b1f34cbe5145 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Thu, 11 Dec 2025 17:26:31 +0100 Subject: [PATCH 24/63] feat: add speedtest button to metric dashboard (#2521) --- app/Livewire/Topbar/Actions.php | 2 ++ resources/views/layouts/partials/header.blade.php | 2 ++ resources/views/livewire/topbar/actions.blade.php | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Topbar/Actions.php b/app/Livewire/Topbar/Actions.php index 95a8abcce..5f687ae86 100644 --- a/app/Livewire/Topbar/Actions.php +++ b/app/Livewire/Topbar/Actions.php @@ -21,6 +21,8 @@ class Actions extends Component implements HasActions, HasForms { use InteractsWithActions, InteractsWithForms; + public bool $showDashboard = true; + public function dashboardAction(): Action { return Action::make('metrics') diff --git a/resources/views/layouts/partials/header.blade.php b/resources/views/layouts/partials/header.blade.php index b8eed7ffc..d26419f78 100644 --- a/resources/views/layouts/partials/header.blade.php +++ b/resources/views/layouts/partials/header.blade.php @@ -55,6 +55,8 @@ class="p-2 rounded-md transition-all"
@auth + + {{ $this->speedtestAction }} - {{ $this->dashboardAction }} - + @if ($showDashboard) + {{ $this->dashboardAction }} + @endif
From b7e573f56474babfcf3bf568b2e28c0902f3ceef Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Mon, 15 Dec 2025 08:54:42 -0500 Subject: [PATCH 25/63] Release v1.12.4 (#2548) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index dfaf9ac76..ec4e255a3 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-08'), + 'build_date' => Carbon::parse('2025-12-15'), - 'build_version' => 'v1.12.3', + 'build_version' => 'v1.12.4', 'content_width' => env('CONTENT_WIDTH', '7xl'), From c4e2e7f627067193daf5590b0f5bac03de1f2d71 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Mon, 15 Dec 2025 09:57:54 -0500 Subject: [PATCH 26/63] fix: adjust speedtest button size based on request context (#2550) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Livewire/Topbar/Actions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Livewire/Topbar/Actions.php b/app/Livewire/Topbar/Actions.php index 5f687ae86..9077724c8 100644 --- a/app/Livewire/Topbar/Actions.php +++ b/app/Livewire/Topbar/Actions.php @@ -67,7 +67,7 @@ public function speedtestAction(): Action ->modalWidth('lg') ->modalSubmitActionLabel(__('results.start')) ->button() - ->size(Size::Medium) + ->size(request()->is('filament*') ? Size::Medium : Size::Large) ->color('primary') ->label(__('results.speedtest')) ->icon('tabler-rocket') From 74062c902d9985f95cf25c53ee0069a23cddee51 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Mon, 15 Dec 2025 19:55:37 -0500 Subject: [PATCH 27/63] Update form method to use Schema instead of Form in ListOoklaServers (#2551) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Filament/Pages/Tools/ListOoklaServers.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Filament/Pages/Tools/ListOoklaServers.php b/app/Filament/Pages/Tools/ListOoklaServers.php index d2d5ea3eb..c82ad82b3 100644 --- a/app/Filament/Pages/Tools/ListOoklaServers.php +++ b/app/Filament/Pages/Tools/ListOoklaServers.php @@ -7,9 +7,9 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Contracts\HasForms; -use Filament\Forms\Form; use Filament\Notifications\Notification; use Filament\Pages\Page; +use Filament\Schemas\Schema; class ListOoklaServers extends Page implements HasForms { @@ -69,10 +69,10 @@ public function fetchServers(): void } } - public function form(Form $form): Form + public function form(Schema $schema): Schema { - return $form - ->schema([ + return $schema + ->components([ Textarea::make('servers') ->label(false) ->rows(20) From 3381265bdf5082bc66fc37466d3657f8fbabc711 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 16 Dec 2025 06:48:11 -0500 Subject: [PATCH 28/63] fix: datetime format for next speedtest banner (#2554) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- resources/views/livewire/next-speedtest-banner.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/next-speedtest-banner.blade.php b/resources/views/livewire/next-speedtest-banner.blade.php index ce65f81d7..e66d3c1ee 100644 --- a/resources/views/livewire/next-speedtest-banner.blade.php +++ b/resources/views/livewire/next-speedtest-banner.blade.php @@ -8,7 +8,7 @@

- Next scheduled test at {{ $this->nextSpeedtest->timezone(config('app.display_timezone'))->format('F jS, Y, g:i a') }}. + Next scheduled test at {{ $this->nextSpeedtest->timezone(config('app.display_timezone'))->format(config('app.datetime_format')) }}.

From 963fc051ad6a8645c0ab2ee585854c9b5cd79c65 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 16 Dec 2025 07:55:52 -0500 Subject: [PATCH 29/63] update external IP fetching logic to handle failures (#2555) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Actions/GetExternalIpAddress.php | 20 +++++++++++++++----- app/Jobs/Ookla/SkipSpeedtestJob.php | 18 +++++++++++++++++- config/speedtest.php | 6 +++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/Actions/GetExternalIpAddress.php b/app/Actions/GetExternalIpAddress.php index 6a4d0b114..48417c1f1 100644 --- a/app/Actions/GetExternalIpAddress.php +++ b/app/Actions/GetExternalIpAddress.php @@ -12,18 +12,28 @@ class GetExternalIpAddress { use AsAction; - public function handle(): bool|string + public function handle(?string $url = null): array { + $url = $url ?? config('speedtest.preflight.get_external_ip_url'); + try { $response = Http::retry(3, 100) ->timeout(5) - ->get(url: 'https://icanhazip.com/'); + ->get(url: $url); } catch (Throwable $e) { - Log::error('Failed to fetch external IP address.', [$e->getMessage()]); + $message = sprintf('Failed to fetch external IP address from "%s". See the logs for more details.', $url); + + Log::error($message, [$e->getMessage()]); - return false; + return [ + 'ok' => false, + 'body' => $message, + ]; } - return Str::trim($response->body()); + return [ + 'ok' => $response->ok(), + 'body' => Str::of($response->body())->trim()->toString(), + ]; } } diff --git a/app/Jobs/Ookla/SkipSpeedtestJob.php b/app/Jobs/Ookla/SkipSpeedtestJob.php index 13c444133..12c276fc1 100644 --- a/app/Jobs/Ookla/SkipSpeedtestJob.php +++ b/app/Jobs/Ookla/SkipSpeedtestJob.php @@ -4,6 +4,7 @@ use App\Actions\GetExternalIpAddress; use App\Enums\ResultStatus; +use App\Events\SpeedtestFailed; use App\Events\SpeedtestSkipped; use App\Helpers\Network; use App\Models\Result; @@ -47,8 +48,23 @@ public function handle(): void $externalIp = GetExternalIpAddress::run(); + if ($externalIp['ok'] === false) { + $this->result->update([ + 'data->type' => 'log', + 'data->level' => 'error', + 'data->message' => $externalIp['body'], + 'status' => ResultStatus::Failed, + ]); + + SpeedtestFailed::dispatch($this->result); + + $this->batch()->cancel(); + + return; + } + $shouldSkip = $this->shouldSkip( - externalIp: $externalIp, + externalIp: $externalIp['body'], ); if ($shouldSkip === false) { diff --git a/config/speedtest.php b/config/speedtest.php index ec4e255a3..937125766 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -29,7 +29,11 @@ 'interface' => env('SPEEDTEST_INTERFACE'), - 'checkinternet_url' => env('SPEEDTEST_CHECKINTERNET_URL', 'https://icanhazip.com'), + 'preflight' => [ + 'get_external_ip_url' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_GET_EXTERNAL_IP_URL', 'https://icanhazip.com'), + ], + + 'checkinternet_url' => env('SPEEDTEST_CHECKINTERNET_URL'), // ! DEPRECATED, use preflight.get_external_ip_url instead /** * IP filtering settings. From 03e02b6eb900b478b297868b48c8cb52c92f8613 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 16 Dec 2025 09:24:42 -0500 Subject: [PATCH 30/63] check for internet using ping (#2556) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Actions/CheckInternetConnection.php | 33 -------- app/Actions/GetExternalIpAddress.php | 2 +- app/Actions/PingHostname.php | 27 ++++++ app/Jobs/CheckForInternetConnectionJob.php | 10 ++- composer.json | 2 +- composer.lock | 98 +++++++++++++--------- config/speedtest.php | 5 +- 7 files changed, 98 insertions(+), 79 deletions(-) delete mode 100644 app/Actions/CheckInternetConnection.php create mode 100644 app/Actions/PingHostname.php diff --git a/app/Actions/CheckInternetConnection.php b/app/Actions/CheckInternetConnection.php deleted file mode 100644 index 22e26f78d..000000000 --- a/app/Actions/CheckInternetConnection.php +++ /dev/null @@ -1,33 +0,0 @@ -timeout(5) - ->get(config('speedtest.checkinternet_url')); - - if (! $response->ok()) { - return false; - } - - return Str::trim($response->body()); - } catch (Throwable $e) { - Log::error('Failed to connect to the internet.', [$e->getMessage()]); - - return false; - } - } -} diff --git a/app/Actions/GetExternalIpAddress.php b/app/Actions/GetExternalIpAddress.php index 48417c1f1..d47cc0a6f 100644 --- a/app/Actions/GetExternalIpAddress.php +++ b/app/Actions/GetExternalIpAddress.php @@ -14,7 +14,7 @@ class GetExternalIpAddress public function handle(?string $url = null): array { - $url = $url ?? config('speedtest.preflight.get_external_ip_url'); + $url = $url ?? config('speedtest.preflight.external_ip_url'); try { $response = Http::retry(3, 100) diff --git a/app/Actions/PingHostname.php b/app/Actions/PingHostname.php new file mode 100644 index 000000000..7707f1abe --- /dev/null +++ b/app/Actions/PingHostname.php @@ -0,0 +1,27 @@ +run(); + + return $ping; + } +} diff --git a/app/Jobs/CheckForInternetConnectionJob.php b/app/Jobs/CheckForInternetConnectionJob.php index 37c5e6f04..c0f39a61f 100644 --- a/app/Jobs/CheckForInternetConnectionJob.php +++ b/app/Jobs/CheckForInternetConnectionJob.php @@ -2,7 +2,7 @@ namespace App\Jobs; -use App\Actions\CheckInternetConnection; +use App\Actions\PingHostname; use App\Enums\ResultStatus; use App\Events\SpeedtestChecking; use App\Events\SpeedtestFailed; @@ -44,14 +44,18 @@ public function handle(): void SpeedtestChecking::dispatch($this->result); - if (CheckInternetConnection::run() !== false) { + $ping = PingHostname::run(); + + if ($ping->isSuccess()) { return; } + $message = sprintf('Failed to connected to hostname "%s". Error received "%s".', $ping->getHost(), $ping->error()?->value); + $this->result->update([ 'data->type' => 'log', 'data->level' => 'error', - 'data->message' => 'Failed to connect to the internet.', + 'data->message' => $message, 'status' => ResultStatus::Failed, ]); diff --git a/composer.json b/composer.json index 77cf9452a..4fe2349e4 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "dragonmantank/cron-expression": "^3.6.0", "filament/filament": "4.1.0", "filament/spatie-laravel-settings-plugin": "^4.1", - "geerlingguy/ping": "^1.2.1", "influxdata/influxdb-client-php": "^3.8", "laravel-notification-channels/telegram": "^6.0", "laravel/framework": "^12.41.1", @@ -36,6 +35,7 @@ "spatie/laravel-query-builder": "^6.3.6", "spatie/laravel-settings": "^3.6.0", "spatie/laravel-webhook-server": "^3.8.3", + "spatie/ping": "^1.1.1", "zircote/swagger-php": "^5.7.6" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 23ff73cb5..6c492d7d9 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": "405d221f03e4de1894ce759a9e751448", + "content-hash": "374762e19dbfc99374c14f3f12a4ae3e", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -1779,43 +1779,6 @@ ], "time": "2025-12-03T09:33:47+00:00" }, - { - "name": "geerlingguy/ping", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/geerlingguy/Ping.git", - "reference": "e0206326e23c99e3e8820e24705f8ca517adff93" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/geerlingguy/Ping/zipball/e0206326e23c99e3e8820e24705f8ca517adff93", - "reference": "e0206326e23c99e3e8820e24705f8ca517adff93", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "JJG/Ping.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeff Geerling", - "email": "jeff@jeffgeerling.com" - } - ], - "description": "A PHP class to ping hosts.", - "support": { - "issues": "https://github.com/geerlingguy/Ping/issues", - "source": "https://github.com/geerlingguy/Ping/tree/1.2.1" - }, - "time": "2019-07-29T21:54:12+00:00" - }, { "name": "graham-campbell/result-type", "version": "v1.1.3", @@ -6919,6 +6882,65 @@ ], "time": "2025-02-14T12:55:41+00:00" }, + { + "name": "spatie/ping", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/ping.git", + "reference": "6123a6209148e8919f58121d256f43c75856ab35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ping/zipball/6123a6209148e8919f58121d256f43c75856ab35", + "reference": "6123a6209148e8919f58121d256f43c75856ab35", + "shasum": "" + }, + "require": { + "php": "^8.4", + "symfony/process": "^7.0" + }, + "require-dev": { + "laravel/pint": "^1.0", + "pestphp/pest": "^3.0", + "spatie/pest-expectations": "^1.13", + "spatie/ray": "^1.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ping\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Run an ICMP ping and get structured results", + "homepage": "https://github.com/spatie/ping", + "keywords": [ + "ping", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/ping/issues", + "source": "https://github.com/spatie/ping/tree/1.1.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-08-12T20:58:12+00:00" + }, { "name": "spatie/shiki-php", "version": "2.3.2", diff --git a/config/speedtest.php b/config/speedtest.php index 937125766..204bf21ec 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -30,11 +30,10 @@ 'interface' => env('SPEEDTEST_INTERFACE'), 'preflight' => [ - 'get_external_ip_url' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_GET_EXTERNAL_IP_URL', 'https://icanhazip.com'), + 'external_ip_url' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_EXTERNAL_IP_URL', 'https://icanhazip.com'), + 'internet_check_hostname' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_INTERNET_CHECK_HOSTNAME', 'icanhazip.com'), ], - 'checkinternet_url' => env('SPEEDTEST_CHECKINTERNET_URL'), // ! DEPRECATED, use preflight.get_external_ip_url instead - /** * IP filtering settings. */ From c7c467ff8a08f6ea2aeb0225186b2a92eb199b2c Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 16 Dec 2025 09:32:52 -0500 Subject: [PATCH 31/63] move skip IPs configuration to preflight settings (#2557) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Jobs/Ookla/SkipSpeedtestJob.php | 4 ++-- config/speedtest.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/Jobs/Ookla/SkipSpeedtestJob.php b/app/Jobs/Ookla/SkipSpeedtestJob.php index 12c276fc1..948fe792d 100644 --- a/app/Jobs/Ookla/SkipSpeedtestJob.php +++ b/app/Jobs/Ookla/SkipSpeedtestJob.php @@ -92,11 +92,11 @@ private function shouldSkip(string $externalIp): bool|string $skipIPs = array_filter( array_map( 'trim', - explode(',', config('speedtest.skip_ips')), + explode(',', config('speedtest.preflight.skip_ips')), ), ); - if (count($skipIPs) < 1) { + if (empty($skipIPs)) { return false; } diff --git a/config/speedtest.php b/config/speedtest.php index 204bf21ec..48dc7d083 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -32,6 +32,7 @@ 'preflight' => [ 'external_ip_url' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_EXTERNAL_IP_URL', 'https://icanhazip.com'), 'internet_check_hostname' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_INTERNET_CHECK_HOSTNAME', 'icanhazip.com'), + 'skip_ips' => env('SPEEDTEST_SKIP_IPS'), ], /** @@ -39,8 +40,6 @@ */ 'allowed_ips' => env('ALLOWED_IPS'), - 'skip_ips' => env('SPEEDTEST_SKIP_IPS', ''), - /** * Threshold settings. */ From cd0915f601371d822c89f52972dc9887e99085cf Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 16 Dec 2025 09:47:48 -0500 Subject: [PATCH 32/63] Release v1.13.0 (#2558) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- resources/views/filament/pages/dashboard.blade.php | 3 +++ resources/views/livewire/latest-result-stats.blade.php | 8 ++++---- resources/views/livewire/platform-stats.blade.php | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 48dc7d083..b68a6a040 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-15'), + 'build_date' => Carbon::parse('2025-12-16'), - 'build_version' => 'v1.12.4', + 'build_version' => 'v1.13.0', 'content_width' => env('CONTENT_WIDTH', '7xl'), diff --git a/resources/views/filament/pages/dashboard.blade.php b/resources/views/filament/pages/dashboard.blade.php index 42a849976..b92cde2a9 100644 --- a/resources/views/filament/pages/dashboard.blade.php +++ b/resources/views/filament/pages/dashboard.blade.php @@ -11,6 +11,7 @@ class="col-span-1" icon="tabler-book" icon-size="md" + :compact="true" > {{ __('general.documentation') }} @@ -35,6 +36,7 @@ class="col-span-1" class="col-span-1" icon="tabler-cash-banknote-heart" icon-size="md" + :compact="true" > {{ __('general.donations') }} @@ -59,6 +61,7 @@ class="col-span-1" class="col-span-1" icon="tabler-brand-github" icon-size="md" + :compact="true" > {{ __('general.speedtest_tracker') }} diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index a3cc2b708..c1c69eeec 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -24,7 +24,7 @@
- + {{ __('general.download') }} @@ -63,7 +63,7 @@

- + {{ __('general.upload') }} @@ -102,7 +102,7 @@

- + {{ __('general.ping') }} @@ -135,7 +135,7 @@

- + {{ __('results.packet_loss') }} diff --git a/resources/views/livewire/platform-stats.blade.php b/resources/views/livewire/platform-stats.blade.php index 567c1f31a..f8f149e6e 100644 --- a/resources/views/livewire/platform-stats.blade.php +++ b/resources/views/livewire/platform-stats.blade.php @@ -23,7 +23,7 @@ --}} - + Total tests @@ -31,7 +31,7 @@

{{ $this->platformStats['total'] }}

- + Total completed tests @@ -39,7 +39,7 @@

{{ $this->platformStats['completed'] }}

- + Total failed tests From b21f0f18d760b73ed90ff00e6fa3efca8b9a3174 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Wed, 17 Dec 2025 09:30:30 -0500 Subject: [PATCH 33/63] Hotfix v1.13.1 (fix for ping failing) (#2561) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Actions/PingHostname.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Actions/PingHostname.php b/app/Actions/PingHostname.php index 7707f1abe..5bbf15ddf 100644 --- a/app/Actions/PingHostname.php +++ b/app/Actions/PingHostname.php @@ -2,6 +2,7 @@ namespace App\Actions; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; use Spatie\Ping\Ping; use Spatie\Ping\PingResult; @@ -22,6 +23,11 @@ public function handle(?string $hostname = null, int $count = 1): PingResult count: $count, ))->run(); + Log::info('Pinged hostname', [ + 'host' => $hostname, + 'data' => $ping->toArray(), + ]); + return $ping; } } From 43a88f551f37bd6b6997ef23d942a712ef1af32b Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Thu, 18 Dec 2025 09:00:57 -0500 Subject: [PATCH 34/63] add selection details option to speedtest command (#2572) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- app/Jobs/Ookla/RunSpeedtestJob.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Jobs/Ookla/RunSpeedtestJob.php b/app/Jobs/Ookla/RunSpeedtestJob.php index 486faab6f..61b371939 100644 --- a/app/Jobs/Ookla/RunSpeedtestJob.php +++ b/app/Jobs/Ookla/RunSpeedtestJob.php @@ -60,6 +60,7 @@ public function handle(): void 'speedtest', '--accept-license', '--accept-gdpr', + '--selection-details', '--format=json', $this->result->server_id ? '--server-id='.$this->result->server_id : null, config('speedtest.interface') ? '--interface='.config('speedtest.interface') : null, From 376b2065f63357b0af2e89aa215c15d6331d37ef Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Thu, 18 Dec 2025 09:19:05 -0500 Subject: [PATCH 35/63] remove platform stats from metrics dashboard (#2574) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- resources/views/dashboard.blade.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index e1b26abed..8b54445cb 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -2,10 +2,6 @@
- @auth - - @endauth -
From dc4dd2ff660246da1b450bea620760e0535a8169 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Thu, 18 Dec 2025 09:24:20 -0500 Subject: [PATCH 36/63] Release v1.13.2 (#2575) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index b68a6a040..946b93f75 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-16'), + 'build_date' => Carbon::parse('2025-12-18'), - 'build_version' => 'v1.13.0', + 'build_version' => 'v1.13.2', 'content_width' => env('CONTENT_WIDTH', '7xl'), From eb9a3e676ecb242b8854b7bfb9e074b03570b9aa Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Fri, 19 Dec 2025 17:26:43 +0100 Subject: [PATCH 37/63] Chore: Improve Apprise Logic & UI Text (#2579) --- .../SendAppriseTestNotification.php | 77 ++++++++++++++++--- app/Filament/Pages/Settings/Notification.php | 5 +- app/Notifications/AppriseChannel.php | 33 ++++---- app/Rules/ContainsString.php | 24 ++++++ lang/en/settings/notifications.php | 13 ++-- 5 files changed, 118 insertions(+), 34 deletions(-) create mode 100644 app/Rules/ContainsString.php diff --git a/app/Actions/Notifications/SendAppriseTestNotification.php b/app/Actions/Notifications/SendAppriseTestNotification.php index 062202b06..64bba779b 100644 --- a/app/Actions/Notifications/SendAppriseTestNotification.php +++ b/app/Actions/Notifications/SendAppriseTestNotification.php @@ -3,15 +3,17 @@ namespace App\Actions\Notifications; use App\Notifications\Apprise\TestNotification; +use App\Settings\NotificationSettings; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Notification as FacadesNotification; use Lorisleiva\Actions\Concerns\AsAction; +use Throwable; class SendAppriseTestNotification { use AsAction; - public function handle(array $channel_urls) + public function handle(array $channel_urls): void { if (! count($channel_urls)) { Notification::make() @@ -22,19 +24,41 @@ public function handle(array $channel_urls) return; } - foreach ($channel_urls as $row) { - $channelUrl = $row['channel_url'] ?? null; - if (! $channelUrl) { - Notification::make() - ->title('Skipping missing channel URL!') - ->warning() - ->send(); + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); - continue; + if (empty($appriseUrl)) { + Notification::make() + ->title('Apprise Server URL is not configured') + ->body('Please configure the Apprise Server URL in the settings above.') + ->danger() + ->send(); + + return; + } + + try { + foreach ($channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + continue; + } + + // Use notifyNow() to send synchronously even though notification implements ShouldQueue + // This allows us to catch exceptions and show them in the UI immediately + FacadesNotification::route('apprise_urls', $channelUrl) + ->notifyNow(new TestNotification); } + } catch (Throwable $e) { + $errorMessage = $this->cleanErrorMessage($e); + + Notification::make() + ->title('Failed to send Apprise test notification') + ->body($errorMessage) + ->danger() + ->send(); - FacadesNotification::route('apprise_urls', $channelUrl) - ->notify(new TestNotification); + return; } Notification::make() @@ -42,4 +66,35 @@ public function handle(array $channel_urls) ->success() ->send(); } + + /** + * Clean up error message for display in UI. + */ + protected function cleanErrorMessage(Throwable $e): string + { + $message = $e->getMessage(); + + // Get the full Apprise server URL for error messages + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + + // Handle connection errors - extract just the important part + if (str_contains($message, 'cURL error')) { + if (str_contains($message, 'Could not resolve host')) { + return "Could not connect to Apprise server at {$appriseUrl}"; + } + + if (str_contains($message, 'Connection refused')) { + return "Connection refused by Apprise server at {$appriseUrl}"; + } + + if (str_contains($message, 'Operation timed out')) { + return "Connection to Apprise server at {$appriseUrl} timed out"; + } + + return "Failed to connect to Apprise server at {$appriseUrl}"; + } + + return $message; + } } diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 9a5decb00..7e1c1a58a 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -14,6 +14,7 @@ use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; use App\Rules\AppriseScheme; +use App\Rules\ContainsString; use App\Settings\NotificationSettings; use CodeWithDennis\SimpleAlert\Components\SimpleAlert; use Filament\Actions\Action; @@ -233,10 +234,12 @@ public function form(Schema $schema): Schema ->schema([ TextInput::make('apprise_server_url') ->label(__('settings/notifications.apprise_server_url')) - ->placeholder('http://localhost:8000') + ->placeholder('http://localhost:8000/notify') + ->helperText(__('settings/notifications.apprise_server_url_helper')) ->maxLength(2000) ->required() ->url() + ->rule(new ContainsString('/notify')) ->columnSpanFull(), Checkbox::make('apprise_verify_ssl') ->label(__('settings/notifications.apprise_verify_ssl')) diff --git a/app/Notifications/AppriseChannel.php b/app/Notifications/AppriseChannel.php index 3cd2592a1..0e9ed1087 100644 --- a/app/Notifications/AppriseChannel.php +++ b/app/Notifications/AppriseChannel.php @@ -3,9 +3,11 @@ namespace App\Notifications; use App\Settings\NotificationSettings; +use Exception; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Throwable; class AppriseChannel { @@ -22,7 +24,7 @@ public function send(object $notifiable, Notification $notification): void } $settings = app(NotificationSettings::class); - $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + $appriseUrl = $settings->apprise_server_url ?? ''; if (empty($appriseUrl)) { Log::warning('Apprise notification skipped: No Server URL configured'); @@ -41,7 +43,7 @@ public function send(object $notifiable, Notification $notification): void $request = $request->withoutVerifying(); } - $response = $request->post("{$appriseUrl}/notify", [ + $response = $request->post($appriseUrl, [ 'urls' => $message->urls, 'title' => $message->title, 'body' => $message->body, @@ -50,26 +52,25 @@ public function send(object $notifiable, Notification $notification): void 'tag' => $message->tag ?? null, ]); - if ($response->failed()) { - Log::error('Apprise notification failed', [ - 'channel' => $message->urls, - 'instance' => $appriseUrl, - 'status' => $response->status(), - 'body' => $response->body(), - ]); - } else { - Log::info('Apprise notification sent', [ - 'channel' => $message->urls, - 'instance' => $appriseUrl, - ]); + // Only accept 200 OK responses as successful + if ($response->status() !== 200) { + throw new Exception('Apprise returned an error, please check Apprise logs for details'); } - } catch (\Throwable $e) { - Log::error('Apprise notification exception', [ + + Log::info('Apprise notification sent', [ + 'channel' => $message->urls, + 'instance' => $appriseUrl, + ]); + } catch (Throwable $e) { + Log::error('Apprise notification failed', [ 'channel' => $message->urls ?? 'unknown', 'instance' => $appriseUrl, 'message' => $e->getMessage(), 'exception' => get_class($e), ]); + + // Re-throw the exception so it can be handled by the queue + throw $e; } } } diff --git a/app/Rules/ContainsString.php b/app/Rules/ContainsString.php new file mode 100644 index 000000000..370a499c6 --- /dev/null +++ b/app/Rules/ContainsString.php @@ -0,0 +1,24 @@ +caseSensitive ? $value : strtolower($value); + $needle = $this->caseSensitive ? $this->needle : strtolower($this->needle); + + if (! str_contains($haystack, $needle)) { + $fail("The :attribute must contain '{$this->needle}'."); + } + } +} diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 203590b23..788f31d91 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -19,19 +19,20 @@ 'enable_apprise_notifications' => 'Enable Apprise notifications', 'apprise_server' => 'Apprise Server', 'apprise_server_url' => 'Apprise Server URL', + 'apprise_server_url_helper' => 'The URL of your Apprise Server. The URL must end on /notify', 'apprise_verify_ssl' => 'Verify SSL', - 'apprise_channels' => 'Apprise Channels', - 'apprise_channel_url' => 'Channel URL', - 'apprise_hint_description' => 'For more information on setting up Apprise, view the documentation.', - 'apprise_channel_url_helper' => 'Provide the service endpoint URL for notifications.', + 'apprise_channels' => 'Notification Channels', + 'apprise_channel_url' => 'Service URL', + 'apprise_hint_description' => 'Apprise allows you to send notifications to 90+ services. You need to run an Apprise server and configure service URLs below.', + 'apprise_channel_url_helper' => 'Use Apprise URL format. Examples: discord://WebhookID/Token, slack://TokenA/TokenB/TokenC', 'test_apprise_channel' => 'Test Apprise', - 'apprise_channel_url_validation_error' => 'The Apprise channel URL must not start with "http" or "https". Please provide a valid Apprise URL scheme.', + 'apprise_channel_url_validation_error' => 'Invalid Apprise URL. Must use Apprise format (e.g., discord://, slack://), not http:// or https://. See the Apprise documentation for more information', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', 'test_webhook_channel' => 'Test webhook channel', - 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation.', + 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation. For services like Discord, Ntfy etc please use Apprise.', // Common notification messages 'notify_on_every_speedtest_run' => 'Notify on every scheduled speedtest run', From 045a5191d20cc8fa1f220e90c6da9db5884b353f Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Fri, 19 Dec 2025 17:38:22 +0100 Subject: [PATCH 38/63] chore: deprecate notification channel alarm on dashboard and notifications (#2577) --- .../DeprecatedNotificationChannelsBanner.php | 66 +++++++++++++++++++ resources/views/dashboard.blade.php | 2 + .../discord/speedtest-completed.blade.php | 2 + .../discord/speedtest-threshold.blade.php | 2 + .../views/filament/pages/dashboard.blade.php | 2 + .../gotify/speedtest-completed.blade.php | 2 + .../gotify/speedtest-threshold.blade.php | 2 + ...ted-notification-channels-banner.blade.php | 25 +++++++ .../views/ntfy/speedtest-completed.blade.php | 2 + .../views/ntfy/speedtest-threshold.blade.php | 2 + .../pushover/speedtest-completed.blade.php | 2 + .../pushover/speedtest-threshold.blade.php | 2 + .../views/slack/speedtest-completed.blade.php | 2 + .../views/slack/speedtest-threshold.blade.php | 2 + .../telegram/speedtest-completed.blade.php | 2 + .../telegram/speedtest-threshold.blade.php | 2 + 16 files changed, 119 insertions(+) create mode 100644 app/Livewire/DeprecatedNotificationChannelsBanner.php create mode 100644 resources/views/livewire/deprecated-notification-channels-banner.blade.php diff --git a/app/Livewire/DeprecatedNotificationChannelsBanner.php b/app/Livewire/DeprecatedNotificationChannelsBanner.php new file mode 100644 index 000000000..84533de19 --- /dev/null +++ b/app/Livewire/DeprecatedNotificationChannelsBanner.php @@ -0,0 +1,66 @@ +discord_enabled + || $settings->gotify_enabled + || $settings->healthcheck_enabled + || $settings->ntfy_enabled + || $settings->pushover_enabled + || $settings->slack_enabled + || $settings->telegram_enabled; + } + + #[Computed] + public function deprecatedChannelsList(): array + { + $settings = app(NotificationSettings::class); + $channels = []; + + if ($settings->discord_enabled) { + $channels[] = 'Discord'; + } + + if ($settings->gotify_enabled) { + $channels[] = 'Gotify'; + } + + if ($settings->healthcheck_enabled) { + $channels[] = 'Healthchecks'; + } + + if ($settings->ntfy_enabled) { + $channels[] = 'Ntfy'; + } + + if ($settings->pushover_enabled) { + $channels[] = 'Pushover'; + } + + if ($settings->slack_enabled) { + $channels[] = 'Slack'; + } + + if ($settings->telegram_enabled) { + $channels[] = 'Telegram'; + } + + return $channels; + } + + public function render() + { + return view('livewire.deprecated-notification-channels-banner'); + } +} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 8b54445cb..967996d47 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,5 +1,7 @@
+ + diff --git a/resources/views/discord/speedtest-completed.blade.php b/resources/views/discord/speedtest-completed.blade.php index 6a8cdf9a5..851a8200a 100644 --- a/resources/views/discord/speedtest-completed.blade.php +++ b/resources/views/discord/speedtest-completed.blade.php @@ -1,3 +1,5 @@ +**Deprecation Notice: The Discord notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Discord and many other services.** + **Speedtest Completed - #{{ $id }}** A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}**. diff --git a/resources/views/discord/speedtest-threshold.blade.php b/resources/views/discord/speedtest-threshold.blade.php index 95dc4bf01..b0716b50b 100644 --- a/resources/views/discord/speedtest-threshold.blade.php +++ b/resources/views/discord/speedtest-threshold.blade.php @@ -1,3 +1,5 @@ +**Deprecation Notice: The Discord notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Discord and many other services.** + **Speedtest Threshold Breached - #{{ $id }}** A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. diff --git a/resources/views/filament/pages/dashboard.blade.php b/resources/views/filament/pages/dashboard.blade.php index b92cde2a9..2d9d68211 100644 --- a/resources/views/filament/pages/dashboard.blade.php +++ b/resources/views/filament/pages/dashboard.blade.php @@ -1,5 +1,7 @@
+ + diff --git a/resources/views/gotify/speedtest-completed.blade.php b/resources/views/gotify/speedtest-completed.blade.php index 910b9347c..c17d46daf 100644 --- a/resources/views/gotify/speedtest-completed.blade.php +++ b/resources/views/gotify/speedtest-completed.blade.php @@ -1,3 +1,5 @@ +**Deprecation Notice: The Gotify notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Gotify and many other services.** + **Speedtest Completed - #{{ $id }}** A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}**. diff --git a/resources/views/gotify/speedtest-threshold.blade.php b/resources/views/gotify/speedtest-threshold.blade.php index dd86bcf8a..e731a0104 100644 --- a/resources/views/gotify/speedtest-threshold.blade.php +++ b/resources/views/gotify/speedtest-threshold.blade.php @@ -1,3 +1,5 @@ +**Deprecation Notice: The Gotify notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Gotify and many other services.** + **Speedtest Threshold Breached - #{{ $id }}** A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. diff --git a/resources/views/livewire/deprecated-notification-channels-banner.blade.php b/resources/views/livewire/deprecated-notification-channels-banner.blade.php new file mode 100644 index 000000000..a96ed0441 --- /dev/null +++ b/resources/views/livewire/deprecated-notification-channels-banner.blade.php @@ -0,0 +1,25 @@ +
+ @if ($this->hasDeprecatedChannels) +
+
+
+ +
+ +
+

+ Deprecated Notification Channels +

+
+

+ You are currently using the following deprecated notification channels: {{ implode(', ', $this->deprecatedChannelsList) }}. +

+

+ These channels will be removed at the end of January 2026. As of that moment you will no longer receive notifications. Please migrate to Apprise which supports all these services and more. +

+
+
+
+
+ @endif +
diff --git a/resources/views/ntfy/speedtest-completed.blade.php b/resources/views/ntfy/speedtest-completed.blade.php index 24c67a908..64501e794 100644 --- a/resources/views/ntfy/speedtest-completed.blade.php +++ b/resources/views/ntfy/speedtest-completed.blade.php @@ -1,3 +1,5 @@ +Deprecation Notice: The Ntfy notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Ntfy and many other services. + Speedtest Completed - #{{ $id }} A new speedtest on {{ config('app.name') }} was completed using {{ $service }}. diff --git a/resources/views/ntfy/speedtest-threshold.blade.php b/resources/views/ntfy/speedtest-threshold.blade.php index 8ad956c03..7e9ef3c93 100644 --- a/resources/views/ntfy/speedtest-threshold.blade.php +++ b/resources/views/ntfy/speedtest-threshold.blade.php @@ -1,3 +1,5 @@ +Deprecation Notice: The Ntfy notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Ntfy and many other services. + Speedtest Threshold Breached - #{{ $id }} A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. diff --git a/resources/views/pushover/speedtest-completed.blade.php b/resources/views/pushover/speedtest-completed.blade.php index 8760eb5de..dfae02299 100644 --- a/resources/views/pushover/speedtest-completed.blade.php +++ b/resources/views/pushover/speedtest-completed.blade.php @@ -1,3 +1,5 @@ +Deprecation Notice: The Pushover notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Pushover and many other services. + Speedtest Completed - #{{ $id }} A new speedtest on {{ config('app.name') }} was completed using {{ $service }}. diff --git a/resources/views/pushover/speedtest-threshold.blade.php b/resources/views/pushover/speedtest-threshold.blade.php index 8ad956c03..6f222fa19 100644 --- a/resources/views/pushover/speedtest-threshold.blade.php +++ b/resources/views/pushover/speedtest-threshold.blade.php @@ -1,3 +1,5 @@ +Deprecation Notice: The Pushover notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Pushover and many other services. + Speedtest Threshold Breached - #{{ $id }} A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. diff --git a/resources/views/slack/speedtest-completed.blade.php b/resources/views/slack/speedtest-completed.blade.php index f93d8daf4..c1abcefc5 100644 --- a/resources/views/slack/speedtest-completed.blade.php +++ b/resources/views/slack/speedtest-completed.blade.php @@ -1,3 +1,5 @@ +*Deprecation Notice: The Slack notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Slack and many other services.* + *Speedtest Completed - #{{ $id }}* A new speedtest on *{{ config('app.name') }}* was completed using *{{ $service }}*. diff --git a/resources/views/slack/speedtest-threshold.blade.php b/resources/views/slack/speedtest-threshold.blade.php index 612475a28..2d4a31fc5 100644 --- a/resources/views/slack/speedtest-threshold.blade.php +++ b/resources/views/slack/speedtest-threshold.blade.php @@ -1,3 +1,5 @@ +*Deprecation Notice: The Slack notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Slack and many other services.* + **Speedtest Threshold Breached - #{{ $id }}** A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. diff --git a/resources/views/telegram/speedtest-completed.blade.php b/resources/views/telegram/speedtest-completed.blade.php index 497c3bf12..ba168cb10 100644 --- a/resources/views/telegram/speedtest-completed.blade.php +++ b/resources/views/telegram/speedtest-completed.blade.php @@ -1,3 +1,5 @@ +**Deprecation Notice: The Telegram notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Telegram and many other services.** + *Speedtest Completed - #{{ $id }}* A new speedtest on *{{ config('app.name') }}* was completed using *{{ $service }}*. diff --git a/resources/views/telegram/speedtest-threshold.blade.php b/resources/views/telegram/speedtest-threshold.blade.php index 95dc4bf01..81ef8bb74 100644 --- a/resources/views/telegram/speedtest-threshold.blade.php +++ b/resources/views/telegram/speedtest-threshold.blade.php @@ -1,3 +1,5 @@ +**Deprecation Notice: The Telegram notification channel will stop working at the end of January 2026. Please migrate to Apprise which supports Telegram and many other services.** + **Speedtest Threshold Breached - #{{ $id }}** A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. From 15258b7ceb7783ac07b603b4cee6f281a33d14a5 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 19 Dec 2025 11:42:50 -0500 Subject: [PATCH 39/63] Release v1.13.3 (#2580) --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 946b93f75..94ebcaa84 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-18'), + 'build_date' => Carbon::parse('2025-12-19'), - 'build_version' => 'v1.13.2', + 'build_version' => 'v1.13.3', 'content_width' => env('CONTENT_WIDTH', '7xl'), From b068b3a45b09617d53952fd33b1ba9855278f69c Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Sun, 21 Dec 2025 13:28:46 -0500 Subject: [PATCH 40/63] New Crowdin updates (#2496) --- lang/de_DE/auth.php | 1 + lang/de_DE/general.php | 13 +++++++++++++ lang/de_DE/results.php | 4 ---- lang/de_DE/settings/data_integration.php | 2 +- lang/de_DE/settings/notifications.php | 13 +++++++++++++ lang/es_ES/auth.php | 1 + lang/es_ES/general.php | 15 +++++++++++++++ lang/es_ES/results.php | 7 +++---- lang/es_ES/settings/data_integration.php | 9 ++++++++- lang/es_ES/settings/notifications.php | 24 ++++++++++++++++-------- lang/fr_FR/auth.php | 1 + lang/fr_FR/general.php | 13 +++++++++++++ lang/fr_FR/results.php | 4 ---- lang/fr_FR/settings/data_integration.php | 2 +- lang/fr_FR/settings/notifications.php | 13 +++++++++++++ lang/nl_NL/auth.php | 1 + lang/nl_NL/general.php | 13 +++++++++++++ lang/nl_NL/results.php | 4 ---- lang/nl_NL/settings/data_integration.php | 2 +- lang/nl_NL/settings/notifications.php | 13 +++++++++++++ lang/pt_BR/auth.php | 1 + lang/pt_BR/general.php | 13 +++++++++++++ lang/pt_BR/results.php | 4 ---- lang/pt_BR/settings/data_integration.php | 2 +- lang/pt_BR/settings/notifications.php | 13 +++++++++++++ 25 files changed, 155 insertions(+), 33 deletions(-) diff --git a/lang/de_DE/auth.php b/lang/de_DE/auth.php index 6d83d009e..ed5c9f604 100644 --- a/lang/de_DE/auth.php +++ b/lang/de_DE/auth.php @@ -13,6 +13,7 @@ | */ + 'sign_in' => 'Anmelden', 'failed' => 'Diese Zugangsdaten stimmen nicht mit unseren Datensätzen überein.', 'password' => 'Das angegebene Passwort ist falsch.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', diff --git a/lang/de_DE/general.php b/lang/de_DE/general.php index 86af86298..4cbb97ede 100644 --- a/lang/de_DE/general.php +++ b/lang/de_DE/general.php @@ -1,6 +1,11 @@ 'Aktuelle Version', + 'latest_version' => 'Neueste Version', + 'github' => 'GitHub', + 'repository' => 'Repository', + // Common actions 'save' => 'Speichern', 'cancel' => 'Abbrechen', @@ -32,6 +37,10 @@ 'created_at' => 'Erstellt am', 'updated_at' => 'Aktualisiert am', 'url' => 'URL', + 'server' => 'Server', + 'servers' => 'Server', + 'stats' => 'Statistiken', + 'statistics' => 'Statistiken', // Navigation 'dashboard' => 'Dashboard', @@ -42,6 +51,7 @@ 'view_documentation' => 'Dokumentation anzeigen', 'links' => 'Links', 'donate' => 'Spenden', + 'donations' => 'Spenden', // Roles 'admin' => 'Admin', @@ -54,12 +64,15 @@ 'last_month' => 'Letzten Monat', // Metrics + 'metrics' => 'Metriken', 'average' => 'Durchschnitt', 'high' => 'Hoch', 'low' => 'Niedrig', 'faster' => 'schneller', 'slower' => 'langsamer', 'healthy' => 'Gesund', + 'not_measured' => 'Nicht gemessen', + 'unhealthy' => 'fehlerhaft', // Units 'ms' => 'M', diff --git a/lang/de_DE/results.php b/lang/de_DE/results.php index 127d85b81..a506bce62 100644 --- a/lang/de_DE/results.php +++ b/lang/de_DE/results.php @@ -55,9 +55,6 @@ // Actions 'update_comments' => 'Kommentare aktualisieren', - 'truncate_results' => 'Ergebnisse kürzen', - 'truncate_results_description' => 'Sind Sie sicher, dass Sie alle Ergebnisse kürzen möchten? Diese Aktion kann nicht rückgängig gemacht werden.', - 'truncate_results_success' => 'Ergebnistabelle abgeschnitten!', 'view_on_speedtest_net' => 'Auf Speedtest.net anzeigen', // Notifications @@ -72,7 +69,6 @@ // Run Speedtest Action 'speedtest' => 'Schnelligkeit', - 'public_dashboard' => 'Öffentliches Dashboard', 'select_server' => 'Server auswählen', 'select_server_helper' => 'Leer lassen, um den Speedtest auszuführen, ohne einen Server anzugeben. Blockierte Server werden übersprungen.', 'manual_servers' => 'Manuelle Server', diff --git a/lang/de_DE/settings/data_integration.php b/lang/de_DE/settings/data_integration.php index 83464fa4d..71d1936dc 100644 --- a/lang/de_DE/settings/data_integration.php +++ b/lang/de_DE/settings/data_integration.php @@ -28,7 +28,7 @@ 'influxdb_test_success_body' => 'Testdaten wurden an InfluxDB gesendet. Überprüfen Sie, ob die Daten empfangen wurden.', // Bulk write notifications - 'influxdb_bulk_write_failed' => 'Fehler beim Erstellen des Schreibens auf Influxdb.', + 'influxdb_bulk_write_failed' => 'Fehler beim Schreiben von Massendaten in InfluxDB.', 'influxdb_bulk_write_failed_body' => 'Überprüfen Sie die Protokolle für weitere Details.', 'influxdb_bulk_write_success' => 'Massendatenlade für Influxdb abgeschlossen.', 'influxdb_bulk_write_success_body' => 'Daten wurden an InfluxDB gesendet. Überprüfen Sie, ob die Daten empfangen wurden.', diff --git a/lang/de_DE/settings/notifications.php b/lang/de_DE/settings/notifications.php index 6d6623a47..8ec3ee6dc 100644 --- a/lang/de_DE/settings/notifications.php +++ b/lang/de_DE/settings/notifications.php @@ -14,6 +14,19 @@ 'recipients' => 'Empfänger', 'test_mail_channel' => 'Mail-Kanal testen', + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Apprise Benachrichtigungen aktivieren', + 'apprise_server' => 'Apprise Server', + 'apprise_server_url' => 'Apprise Server URL', + 'apprise_verify_ssl' => 'SSL verifizieren', + 'apprise_channels' => 'Apprise Kanäle', + 'apprise_channel_url' => 'Kanal URL', + 'apprise_hint_description' => 'Lesen Sie für weitere Informationen zum Einrichten von Apprise die Dokumentation.', + 'apprise_channel_url_helper' => 'Geben Sie die Service Endpoint URL für Benachrichtigung an.', + 'test_apprise_channel' => 'Apprise testen', + 'apprise_channel_url_validation_error' => 'Die Apprise Channel URL muss nicht mit "HTTP" oder "HTTPS" starten. Geben Sie ein valides Apprise URL Schema an.', + // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', diff --git a/lang/es_ES/auth.php b/lang/es_ES/auth.php index f4b03c1d5..b150afe6b 100644 --- a/lang/es_ES/auth.php +++ b/lang/es_ES/auth.php @@ -13,6 +13,7 @@ | */ + 'sign_in' => 'Iniciar sesión', 'failed' => 'Estas credenciales no coinciden con nuestros registros.', 'password' => 'La contraseña proporcionada es incorrecta.', 'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, inténtalo de nuevo en :seconds segundos.', diff --git a/lang/es_ES/general.php b/lang/es_ES/general.php index bf2e3a571..fddf7a982 100644 --- a/lang/es_ES/general.php +++ b/lang/es_ES/general.php @@ -1,6 +1,11 @@ 'Versión actual', + 'latest_version' => 'Última versión', + 'github' => 'GitHub', + 'repository' => 'Repositorio', + // Common actions 'save' => 'Guardar', 'cancel' => 'Cancelar', @@ -16,6 +21,7 @@ 'no' => 'Nu', 'options' => 'Opciones', 'details' => 'Detalles', + 'view' => 'Ver', // Common labels 'name' => 'Nombre', @@ -31,6 +37,10 @@ 'created_at' => 'Creado el', 'updated_at' => 'Actualizado el', 'url' => 'URL', + 'server' => 'Servidor', + 'servers' => 'Servidores', + 'stats' => 'Estadísticas', + 'statistics' => 'Estadísticas', // Navigation 'dashboard' => 'Tablero', @@ -38,8 +48,10 @@ 'settings' => 'Ajustes', 'users' => 'Usuarios', 'documentation' => 'Documentación', + 'view_documentation' => 'Ver documentación', 'links' => 'Enlaces', 'donate' => 'Donar', + 'donations' => 'Donaciones', // Roles 'admin' => 'Admin', @@ -52,12 +64,15 @@ 'last_month' => 'Último mes', // Metrics + 'metrics' => 'Métricas', 'average' => 'Promedio', 'high' => 'Alta', 'low' => 'Baja', 'faster' => 'más rápido', 'slower' => 'más lento', 'healthy' => 'Saludable', + 'not_measured' => 'No medido', + 'unhealthy' => 'Poco saludable', // Units 'ms' => 'm', diff --git a/lang/es_ES/results.php b/lang/es_ES/results.php index f41af5df8..89df7e984 100644 --- a/lang/es_ES/results.php +++ b/lang/es_ES/results.php @@ -55,21 +55,20 @@ // Actions 'update_comments' => 'Actualizar comentarios', - 'truncate_results' => 'Truncar resultados', - 'truncate_results_description' => '¿Está seguro que desea truncar todos los resultados? Esta acción es irreversible.', - 'truncate_results_success' => '¡Tabla de resultados truncada!', 'view_on_speedtest_net' => 'Ver en Speedtest.net', // Notifications + 'speedtest_benchmark_passed' => 'La prueba de rendimiento de velocidad ha pasado', + 'speedtest_benchmark_failed' => 'Prueba de rendimiento de velocidad fallida', 'speedtest_started' => 'Velocidad iniciada', 'speedtest_completed' => 'Velocidad completada', + 'speedtest_failed' => 'Error en la prueba de velocidad', 'download_threshold_breached' => '¡Umbral de descarga incumplido!', 'upload_threshold_breached' => '¡Umbral de subida infringido!', 'ping_threshold_breached' => '¡Umbral de ping infringido!', // Run Speedtest Action 'speedtest' => 'Velocidad', - 'public_dashboard' => 'Panel público', 'select_server' => 'Seleccionar Servidor', 'select_server_helper' => 'Dejar en blanco para ejecutar el test de velocidad sin especificar un servidor. Se omitirán los servidores bloqueados.', 'manual_servers' => 'Servidores manuales', diff --git a/lang/es_ES/settings/data_integration.php b/lang/es_ES/settings/data_integration.php index 1c3633559..a1b024848 100644 --- a/lang/es_ES/settings/data_integration.php +++ b/lang/es_ES/settings/data_integration.php @@ -28,11 +28,18 @@ 'influxdb_test_success_body' => 'Los datos de prueba han sido enviados a InfluxDB, compruebe si los datos han sido recibidos.', // Bulk write notifications - 'influxdb_bulk_write_failed' => 'Error al construir escritura en Influxdb.', + 'influxdb_bulk_write_failed' => 'Error al escribir en masa a Influxdb.', 'influxdb_bulk_write_failed_body' => 'Revisa los registros para más detalles.', 'influxdb_bulk_write_success' => 'Carga de datos en masa a Influxdb.', 'influxdb_bulk_write_success_body' => 'Los datos han sido enviados a InfluxDB, compruebe si los datos han sido recibidos.', + // Prometheus + 'prometheus' => 'Prometeo', + 'prometheus_enabled' => 'Activar', + 'prometheus_enabled_helper_text' => 'Cuando está activado, las métricas para cada prueba de velocidad nueva estarán disponibles en el punto final /prometheus.', + 'prometheus_allowed_ips' => 'Direcciones IP permitidas', + 'prometheus_allowed_ips_helper' => 'Lista de direcciones IP o rangos CIDR (por ejemplo, 192.168.1.0/24) permitieron acceder al extremo de las métricas. Dejar en blanco para permitir todas las IPs.', + // Common labels 'org' => 'Org', 'bucket' => 'Cubo', diff --git a/lang/es_ES/settings/notifications.php b/lang/es_ES/settings/notifications.php index ddbb10dca..83d8bbf20 100644 --- a/lang/es_ES/settings/notifications.php +++ b/lang/es_ES/settings/notifications.php @@ -7,27 +7,35 @@ // Database notifications 'database' => 'Base de datos', 'database_description' => 'Las notificaciones enviadas a este canal se mostrarán bajo el icono :belell: en el encabezado.', - 'database_on_speedtest_run' => 'Notificar en cada prueba de velocidad', - 'database_on_threshold_failure' => 'Notificar en los umbrales de fallos', 'test_database_channel' => 'Probar canal de base de datos', // Mail notifications 'mail' => 'Correo', 'recipients' => 'Destinatarios', - 'mail_on_speedtest_run' => 'Notificar en cada prueba de velocidad', - 'mail_on_threshold_failure' => 'Notificar en los umbrales de fallos', 'test_mail_channel' => 'Canal de prueba de correo', + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Habilitar notificaciones Apprise', + 'apprise_server' => 'Servidor Apprise', + 'apprise_server_url' => 'URL del servidor', + 'apprise_verify_ssl' => 'Verificar SSL', + 'apprise_channels' => 'Canales de expedición', + 'apprise_channel_url' => 'URL del canal', + 'apprise_hint_description' => 'Para más información sobre cómo configurar Apprise, vea la documentación.', + 'apprise_channel_url_helper' => 'Proporcionar la URL de los puntos finales del servicio para las notificaciones.', + 'test_apprise_channel' => 'Prueba de aviso', + 'apprise_channel_url_validation_error' => 'La URL del canal Apprise no debe comenzar con "http" o "https". Por favor, proporcione un esquema de URL de Apprise válido.', + // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', - 'webhook_on_speedtest_run' => 'Notificar en cada prueba de velocidad', - 'webhook_on_threshold_failure' => 'Notificar en los umbrales de fallos', 'test_webhook_channel' => 'Probar canal webhook', + 'webhook_hint_description' => 'Estos son webhooks genéricos. Para ejemplos de payload y detalles de la implementación, vea la documentación.', // Common notification messages - 'notify_on_every_speedtest_run' => 'Notificar en cada prueba de velocidad', - 'notify_on_threshold_failures' => 'Notificar en los umbrales de fallos', + 'notify_on_every_speedtest_run' => 'Notificar en cada prueba de velocidad programada', + 'notify_on_threshold_failures' => 'Notificar fallos de umbral para pruebas de velocidad programadas', // Test notification messages 'test_notifications' => [ diff --git a/lang/fr_FR/auth.php b/lang/fr_FR/auth.php index c1a529c3a..fc8d1390b 100644 --- a/lang/fr_FR/auth.php +++ b/lang/fr_FR/auth.php @@ -13,6 +13,7 @@ | */ + 'sign_in' => 'Se connecter', 'failed' => 'Ces identifiants ne correspondent pas à nos enregistrements.', 'password' => 'Le mot de passe fourni est incorrect.', 'throttle' => 'Trop de tentatives de connexion. Veuillez réessayer dans :seconds secondes.', diff --git a/lang/fr_FR/general.php b/lang/fr_FR/general.php index adeba836d..4b9422d7f 100644 --- a/lang/fr_FR/general.php +++ b/lang/fr_FR/general.php @@ -1,6 +1,11 @@ 'Version actuelle', + 'latest_version' => 'Dernière version', + 'github' => 'GitHub', + 'repository' => 'Dépôt', + // Common actions 'save' => 'Enregistrer', 'cancel' => 'Abandonner', @@ -32,6 +37,10 @@ 'created_at' => 'Créé le', 'updated_at' => 'Mis à jour le', 'url' => 'URL', + 'server' => 'Serveur', + 'servers' => 'Serveurs', + 'stats' => 'Stats', + 'statistics' => 'Statistiques', // Navigation 'dashboard' => 'Tableau de bord', @@ -42,6 +51,7 @@ 'view_documentation' => 'Afficher la documentation', 'links' => 'Liens', 'donate' => 'Faire un don', + 'donations' => 'Dons', // Roles 'admin' => 'Administrateur', @@ -54,12 +64,15 @@ 'last_month' => 'Le mois dernier', // Metrics + 'metrics' => 'Métriques', 'average' => 'Moyenne', 'high' => 'Élevé', 'low' => 'Bas', 'faster' => 'rapide', 'slower' => 'lent', 'healthy' => 'Sain', + 'not_measured' => 'Non mesuré', + 'unhealthy' => 'Malsain', // Units 'ms' => 'ms', diff --git a/lang/fr_FR/results.php b/lang/fr_FR/results.php index 7284dcbbc..ab2715930 100644 --- a/lang/fr_FR/results.php +++ b/lang/fr_FR/results.php @@ -55,9 +55,6 @@ // Actions 'update_comments' => 'Actualiser les commentaires', - 'truncate_results' => 'Tronquer les résultats', - 'truncate_results_description' => 'Êtes-vous sûr de vouloir tronquer tous les résultats ? Cette action est irréversible.', - 'truncate_results_success' => 'Tableau des résultats tronqué !', 'view_on_speedtest_net' => 'Voir sur Speedtest.net', // Notifications @@ -72,7 +69,6 @@ // Run Speedtest Action 'speedtest' => 'Test de vitesse', - 'public_dashboard' => 'Tableau de bord public', 'select_server' => 'Sélectionner un serveur', 'select_server_helper' => 'Laisser vide pour exécuter le test de vitesse sans spécifier de serveur. Les serveurs bloqués seront ignorés.', 'manual_servers' => 'Serveurs manuels', diff --git a/lang/fr_FR/settings/data_integration.php b/lang/fr_FR/settings/data_integration.php index 007362964..ce0af0775 100644 --- a/lang/fr_FR/settings/data_integration.php +++ b/lang/fr_FR/settings/data_integration.php @@ -28,7 +28,7 @@ 'influxdb_test_success_body' => 'Les données de test ont été envoyées à InfluxDB, vérifiez si les données ont été reçues.', // Bulk write notifications - 'influxdb_bulk_write_failed' => 'Échec de la construction de l\'écriture sur Influxdb.', + 'influxdb_bulk_write_failed' => 'Impossible d\'écrire en masse sur Influxdb.', 'influxdb_bulk_write_failed_body' => 'Consultez les journaux pour plus de détails.', 'influxdb_bulk_write_success' => 'Charge de données en masse terminée sur Influxdb.', 'influxdb_bulk_write_success_body' => 'Les données ont été envoyées à InfluxDB, vérifiez si les données ont été reçues.', diff --git a/lang/fr_FR/settings/notifications.php b/lang/fr_FR/settings/notifications.php index 778334423..359a1424a 100644 --- a/lang/fr_FR/settings/notifications.php +++ b/lang/fr_FR/settings/notifications.php @@ -14,6 +14,19 @@ 'recipients' => 'Destinataires', 'test_mail_channel' => 'Tester le canal de messagerie', + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Activer les notifications de base de données', + 'apprise_server' => 'Serveur Apprise', + 'apprise_server_url' => 'Serveur Apprise', + 'apprise_verify_ssl' => 'Vérifier SSL', + 'apprise_channels' => 'Canaux Apprise', + 'apprise_channel_url' => 'URL du canal de mise à jour', + 'apprise_hint_description' => 'Pour plus d\'informations sur la configuration d\'Apprise, consultez la documentation.', + 'apprise_channel_url_helper' => 'Fournir l\'URL de terminaison du service pour les notifications.', + 'test_apprise_channel' => 'Apprise de test', + 'apprise_channel_url_validation_error' => 'L\'URL du canal Apprise ne doit pas commencer par "http" ou "https". Veuillez fournir un schéma d\'URL Apprise valide.', + // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', diff --git a/lang/nl_NL/auth.php b/lang/nl_NL/auth.php index 7667d5e81..2c9909f81 100644 --- a/lang/nl_NL/auth.php +++ b/lang/nl_NL/auth.php @@ -13,6 +13,7 @@ | */ + 'sign_in' => 'Aanmelden', 'failed' => 'Deze gegevens komen niet overeen met onze administratie.', 'password' => 'Het opgegeven wachtwoord is onjuist.', 'throttle' => 'Te veel inlogpogingen. Probeer het over :seconds seconden opnieuw.', diff --git a/lang/nl_NL/general.php b/lang/nl_NL/general.php index 28e36b0f9..e17828565 100644 --- a/lang/nl_NL/general.php +++ b/lang/nl_NL/general.php @@ -1,6 +1,11 @@ 'Huidige versie', + 'latest_version' => 'Laatste versie', + 'github' => 'GitHub', + 'repository' => 'Repository', + // Common actions 'save' => 'Opslaan', 'cancel' => 'Annuleren', @@ -32,6 +37,10 @@ 'created_at' => 'Aangemaakt op', 'updated_at' => 'Bijgewerkt op', 'url' => 'URL', + 'server' => 'Server', + 'servers' => 'Servers', + 'stats' => 'Statistieken', + 'statistics' => 'Statistieken', // Navigation 'dashboard' => 'Dashboard', @@ -42,6 +51,7 @@ 'view_documentation' => 'Bekijk documentatie', 'links' => 'Koppelingen', 'donate' => 'Doneren', + 'donations' => 'Donaties', // Roles 'admin' => 'Beheerder', @@ -54,12 +64,15 @@ 'last_month' => 'Vorige maand', // Metrics + 'metrics' => 'Statistieken', 'average' => 'Gemiddeld', 'high' => 'Hoog', 'low' => 'Laag', 'faster' => 'sneller', 'slower' => 'langzamer', 'healthy' => 'Gezond', + 'not_measured' => 'Niet gemeten', + 'unhealthy' => 'Ongezond', // Units 'ms' => 'ms', diff --git a/lang/nl_NL/results.php b/lang/nl_NL/results.php index 69befafa6..4a17ed6c8 100644 --- a/lang/nl_NL/results.php +++ b/lang/nl_NL/results.php @@ -55,9 +55,6 @@ // Actions 'update_comments' => 'Reacties bijwerken', - 'truncate_results' => 'Afkappen resultaten', - 'truncate_results_description' => 'Weet je zeker dat je alle resultaten wilt afbreken? Deze actie is onomkeerbaar.', - 'truncate_results_success' => 'Resultatentabel afgekapt!', 'view_on_speedtest_net' => 'Bekijk op Speedtest.net', // Notifications @@ -72,7 +69,6 @@ // Run Speedtest Action 'speedtest' => 'Snelheidstest', - 'public_dashboard' => 'Openbaar Dashboard', 'select_server' => 'Selecteer Server', 'select_server_helper' => 'Laat leeg om de snelheidstest uit te voeren zonder een server op te geven. Geblokkeerde servers zullen worden overgeslagen.', 'manual_servers' => 'Handmatige servers', diff --git a/lang/nl_NL/settings/data_integration.php b/lang/nl_NL/settings/data_integration.php index 2508fdf6b..c95902fac 100644 --- a/lang/nl_NL/settings/data_integration.php +++ b/lang/nl_NL/settings/data_integration.php @@ -28,7 +28,7 @@ 'influxdb_test_success_body' => 'Test gegevens zijn verzonden naar de InfluxDB, controleer of de gegevens zijn ontvangen.', // Bulk write notifications - 'influxdb_bulk_write_failed' => 'Kan schrijven naar Influxdb niet maken.', + 'influxdb_bulk_write_failed' => 'Bulk schrijven naar Influxdb mislukt.', 'influxdb_bulk_write_failed_body' => 'Bekijk de logs voor meer details.', 'influxdb_bulk_write_success' => 'Alle resultaten naar InfluxDB sturen afgerond.', 'influxdb_bulk_write_success_body' => 'Gegevens zijn verzonden naar InfluxDB, controleer of de gegevens zijn ontvangen.', diff --git a/lang/nl_NL/settings/notifications.php b/lang/nl_NL/settings/notifications.php index 9e1942078..e7eff7a97 100644 --- a/lang/nl_NL/settings/notifications.php +++ b/lang/nl_NL/settings/notifications.php @@ -14,6 +14,19 @@ 'recipients' => 'Ontvangers', 'test_mail_channel' => 'Test e-mailkanaal', + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Inschakelen Apprise meldingen', + 'apprise_server' => 'Apprise Server', + 'apprise_server_url' => 'Appprise Server-URL', + 'apprise_verify_ssl' => 'Controleer SSL', + 'apprise_channels' => 'Apprise Kanalen', + 'apprise_channel_url' => 'Kanaal URL', + 'apprise_hint_description' => 'Voor meer informatie over het instellen van Apprise, bekijk de documentatie.', + 'apprise_channel_url_helper' => 'Geef de service eindpunt URL voor meldingen.', + 'test_apprise_channel' => 'Test Apprise', + 'apprise_channel_url_validation_error' => 'De URL van het Apprise kanaal mag niet beginnen met "http" of "https". Geef een geldig URL-schema op.', + // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php index 16ae2cdb5..13625436a 100644 --- a/lang/pt_BR/auth.php +++ b/lang/pt_BR/auth.php @@ -13,6 +13,7 @@ | */ + 'sign_in' => 'Entrar', 'failed' => 'Credenciais não correspondem aos nossos registros.', 'password' => 'A senha fornecida está incorreta.', 'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.', diff --git a/lang/pt_BR/general.php b/lang/pt_BR/general.php index 57ee664d6..7a8b67af2 100644 --- a/lang/pt_BR/general.php +++ b/lang/pt_BR/general.php @@ -1,6 +1,11 @@ 'Versão atual', + 'latest_version' => 'Versão mais recente', + 'github' => 'GitHub', + 'repository' => 'Repositório', + // Common actions 'save' => 'Salvar', 'cancel' => 'Cancelar', @@ -32,6 +37,10 @@ 'created_at' => 'Criado em', 'updated_at' => 'Atualizado em', 'url' => 'URL', + 'server' => 'Servidor', + 'servers' => 'Servidores', + 'stats' => 'Estatísticas', + 'statistics' => 'Estatísticas', // Navigation 'dashboard' => 'Painel', @@ -42,6 +51,7 @@ 'view_documentation' => 'Ver documentação', 'links' => 'Links', 'donate' => 'Doar', + 'donations' => 'Doações', // Roles 'admin' => 'Admin', @@ -54,12 +64,15 @@ 'last_month' => 'Mês anterior', // Metrics + 'metrics' => 'Métricas', 'average' => 'Média', 'high' => 'Alta', 'low' => 'Baixa', 'faster' => 'mais rápido', 'slower' => 'lento', 'healthy' => 'Saudável', + 'not_measured' => 'Não medido', + 'unhealthy' => 'Não saudável', // Units 'ms' => 'ms', diff --git a/lang/pt_BR/results.php b/lang/pt_BR/results.php index bc725b402..5d3c62055 100644 --- a/lang/pt_BR/results.php +++ b/lang/pt_BR/results.php @@ -55,9 +55,6 @@ // Actions 'update_comments' => 'Atualizar comentários', - 'truncate_results' => 'Truncar resultados', - 'truncate_results_description' => 'Tem certeza que deseja truncar todos os resultados? Esta ação é irreversível.', - 'truncate_results_success' => 'Tabela de resultados truncada!', 'view_on_speedtest_net' => 'Ver em Speedtest.net', // Notifications @@ -72,7 +69,6 @@ // Run Speedtest Action 'speedtest' => 'Teste de velocidade', - 'public_dashboard' => 'Painel público', 'select_server' => 'Selecionar servidor', 'select_server_helper' => 'Deixe em branco para executar o acelerador sem especificar um servidor. Os servidores bloqueados serão ignorados.', 'manual_servers' => 'Servidores manuais', diff --git a/lang/pt_BR/settings/data_integration.php b/lang/pt_BR/settings/data_integration.php index 2b5dd5ae2..02cc2a6fd 100644 --- a/lang/pt_BR/settings/data_integration.php +++ b/lang/pt_BR/settings/data_integration.php @@ -28,7 +28,7 @@ 'influxdb_test_success_body' => 'Dados de teste enviados para InfluxDB, verifique se os dados foram recebidos.', // Bulk write notifications - 'influxdb_bulk_write_failed' => 'Falha ao escrever no Influxdb.', + 'influxdb_bulk_write_failed' => 'Falha ao escrever em massa no Influxdb.', 'influxdb_bulk_write_failed_body' => 'Confira os logs para mais detalhes.', 'influxdb_bulk_write_success' => 'Carga massiva de dados concluída para o Influxdb.', 'influxdb_bulk_write_success_body' => 'Os dados foram enviados para InfluxDB, verifique se os dados foram recebidos.', diff --git a/lang/pt_BR/settings/notifications.php b/lang/pt_BR/settings/notifications.php index 6d90ed8f4..827ce15f8 100644 --- a/lang/pt_BR/settings/notifications.php +++ b/lang/pt_BR/settings/notifications.php @@ -14,6 +14,19 @@ 'recipients' => 'Destinatários', 'test_mail_channel' => 'Testar canal de e-mail', + // Apprise notifications + 'apprise' => 'Informar', + 'enable_apprise_notifications' => 'Habilitar notificações Apprise', + 'apprise_server' => 'Servidor Apprise', + 'apprise_server_url' => 'URL do Servidor Apprise', + 'apprise_verify_ssl' => 'Verificar SSL', + 'apprise_channels' => 'Canais Apprise', + 'apprise_channel_url' => 'URL do Canal', + 'apprise_hint_description' => 'Para obter mais informações sobre como configurar o Apprise, veja a documentação.', + 'apprise_channel_url_helper' => 'Forneça o URL do serviço endpoint para notificações.', + 'test_apprise_channel' => 'Testar Apprise', + 'apprise_channel_url_validation_error' => 'O URL do canal Apprise não deve começar com "http" ou "https". Por favor, forneça um esquema válido de URL Apprise.', + // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', From 62f13fb009f6589fe1e976910a587f77bb57de39 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 26 Dec 2025 15:11:39 -0500 Subject: [PATCH 41/63] Mark speedtest as scheduled when triggered using the API (#2597) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .../Api/V1/SpeedtestController.php | 1 + app/Listeners/ProcessCompletedSpeedtest.php | 51 ++------- app/Listeners/ProcessFailedSpeedtest.php | 35 +----- app/Listeners/ProcessUnhealthySpeedtest.php | 48 +------- app/Listeners/UserNotificationSubscriber.php | 104 ++++++++++++++++++ app/Models/Result.php | 11 ++ 6 files changed, 132 insertions(+), 118 deletions(-) create mode 100644 app/Listeners/UserNotificationSubscriber.php diff --git a/app/Http/Controllers/Api/V1/SpeedtestController.php b/app/Http/Controllers/Api/V1/SpeedtestController.php index 19519f61e..d605d945c 100644 --- a/app/Http/Controllers/Api/V1/SpeedtestController.php +++ b/app/Http/Controllers/Api/V1/SpeedtestController.php @@ -37,6 +37,7 @@ public function __invoke(Request $request) } $result = RunSpeedtestAction::run( + scheduled: true, serverId: $request->input('server_id'), dispatchedBy: $request->user()->id, ); diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 1b72f3053..ae5d544e1 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -31,11 +31,17 @@ public function handle(SpeedtestCompleted $event): void { $result = $event->result; - $result->loadMissing(['dispatchedBy']); + if ($result->healthy === false) { + return; + } + + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } $this->notifyAppriseChannels($result); $this->notifyDatabaseChannels($result); - $this->notifyDispatchingUser($result); $this->notifyMailChannels($result); $this->notifyWebhookChannels($result); } @@ -45,11 +51,6 @@ 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 === false) { - return; - } - // Check if Apprise notifications are enabled. if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_speedtest_run) { return; @@ -97,11 +98,6 @@ private function notifyAppriseChannels(Result $result): void */ 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 === false) { - return; - } - // Check if database notifications are enabled. if (! $this->notificationSettings->database_enabled || ! $this->notificationSettings->database_on_speedtest_run) { return; @@ -120,37 +116,11 @@ private function notifyDatabaseChannels(Result $result): void } } - /** - * Notify the user who dispatched the speedtest. - */ - private function notifyDispatchingUser(Result $result): void - { - if (empty($result->dispatched_by) || ! $result->healthy) { - return; - } - - $result->dispatchedBy->notify( - FilamentNotification::make() - ->title(__('results.speedtest_completed')) - ->actions([ - Action::make('view') - ->label(__('general.view')) - ->url(route('filament.admin.resources.results.index')), - ]) - ->success() - ->toDatabase(), - ); - } - /** * Notify mail channels. */ private function notifyMailChannels(Result $result): void { - if (filled($result->dispatched_by) || $result->healthy === false) { - return; - } - if (! $this->notificationSettings->mail_enabled || ! $this->notificationSettings->mail_on_speedtest_run) { return; } @@ -172,11 +142,6 @@ private function notifyMailChannels(Result $result): void */ 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 === false) { - return; - } - // Check if webhook notifications are enabled. if (! $this->notificationSettings->webhook_enabled || ! $this->notificationSettings->webhook_on_speedtest_run) { return; diff --git a/app/Listeners/ProcessFailedSpeedtest.php b/app/Listeners/ProcessFailedSpeedtest.php index 8ba154387..3cc4d9cd8 100644 --- a/app/Listeners/ProcessFailedSpeedtest.php +++ b/app/Listeners/ProcessFailedSpeedtest.php @@ -4,8 +4,6 @@ use App\Events\SpeedtestFailed; use App\Models\Result; -use Filament\Actions\Action; -use Filament\Notifications\Notification; class ProcessFailedSpeedtest { @@ -16,10 +14,12 @@ public function handle(SpeedtestFailed $event): void { $result = $event->result; - $result->loadMissing(['dispatchedBy']); + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } // $this->notifyAppriseChannels($result); - $this->notifyDispatchingUser($result); } /** @@ -27,33 +27,6 @@ public function handle(SpeedtestFailed $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; - } - // } - - /** - * 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_failed')) - ->actions([ - Action::make('view') - ->label(__('general.view')) - ->url(route('filament.admin.resources.results.index')), - ]) - ->warning() - ->toDatabase(), - ); - } } diff --git a/app/Listeners/ProcessUnhealthySpeedtest.php b/app/Listeners/ProcessUnhealthySpeedtest.php index 4f51e4267..f2e0d419c 100644 --- a/app/Listeners/ProcessUnhealthySpeedtest.php +++ b/app/Listeners/ProcessUnhealthySpeedtest.php @@ -33,11 +33,13 @@ public function handle(SpeedtestBenchmarkFailed $event): void { $result = $event->result; - $result->loadMissing(['dispatchedBy']); + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } $this->notifyAppriseChannels($result); $this->notifyDatabaseChannels($result); - $this->notifyDispatchingUser($result); $this->notifyMailChannels($result); $this->notifyWebhookChannels($result); } @@ -47,11 +49,6 @@ 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; - } - if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_threshold_failure) { return; } @@ -132,11 +129,6 @@ private function formatMetricValue(string $metric, Result $result): string */ 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; @@ -155,38 +147,11 @@ private function notifyDatabaseChannels(Result $result): void } } - /** - * Notify the user who dispatched the speedtest. - */ - private function notifyDispatchingUser(Result $result): void - { - if (empty($result->dispatched_by)) { - return; - } - - $result->dispatchedBy->notify( - FilamentNotification::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 mail 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; @@ -210,11 +175,6 @@ private function notifyMailChannels(Result $result): void */ 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; diff --git a/app/Listeners/UserNotificationSubscriber.php b/app/Listeners/UserNotificationSubscriber.php new file mode 100644 index 000000000..4ff7dc7c8 --- /dev/null +++ b/app/Listeners/UserNotificationSubscriber.php @@ -0,0 +1,104 @@ +result; + + if (empty($result->dispatched_by)) { + return; + } + + $result->loadMissing('dispatchedBy'); + + Notification::make() + ->title(__('results.speedtest_completed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->success() + ->sendToDatabase($result->dispatchedBy); + } + + /** + * Handle the event. + */ + public function handleBenchmarkFailed(SpeedtestBenchmarkFailed $event): void + { + $result = $event->result; + + if (empty($result->dispatched_by)) { + return; + } + + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } + + $result->loadMissing('dispatchedBy'); + + Notification::make() + ->title(__('results.speedtest_benchmark_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->warning() + ->sendToDatabase($result->dispatchedBy); + } + + /** + * Handle the event. + */ + public function handleFailed(SpeedtestFailed $event): void + { + $result = $event->result; + + if (empty($result->dispatched_by)) { + return; + } + + $result->loadMissing('dispatchedBy'); + + Notification::make() + ->title(__('results.speedtest_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->warning() + ->sendToDatabase($result->dispatchedBy); + } + + /** + * Register the listeners for the subscriber. + * + * @return array + */ + public function subscribe(Dispatcher $events): array + { + return [ + SpeedtestCompleted::class => 'handleCompleted', + SpeedtestBenchmarkFailed::class => 'handleBenchmarkFailed', + SpeedtestFailed::class => 'handleFailed', + ]; + } +} diff --git a/app/Models/Result.php b/app/Models/Result.php index bba1a37d9..084c04097 100644 --- a/app/Models/Result.php +++ b/app/Models/Result.php @@ -6,6 +6,7 @@ use App\Enums\ResultStatus; use App\Models\Traits\ResultDataAttributes; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; @@ -54,4 +55,14 @@ public function dispatchedBy(): BelongsTo { return $this->belongsTo(User::class, 'dispatched_by'); } + + /** + * Determine if the result was unscheduled. + */ + protected function unscheduled(): Attribute + { + return Attribute::make( + get: fn (): bool => ! $this->scheduled, + ); + } } From ae3eb321c16167bf9c25bf7c59d7f01e9050de78 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 26 Dec 2025 15:14:30 -0500 Subject: [PATCH 42/63] New Crowdin updates (#2598) --- lang/nl_NL/settings/notifications.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lang/nl_NL/settings/notifications.php b/lang/nl_NL/settings/notifications.php index e7eff7a97..7a37c850a 100644 --- a/lang/nl_NL/settings/notifications.php +++ b/lang/nl_NL/settings/notifications.php @@ -19,19 +19,20 @@ 'enable_apprise_notifications' => 'Inschakelen Apprise meldingen', 'apprise_server' => 'Apprise Server', 'apprise_server_url' => 'Appprise Server-URL', + 'apprise_server_url_helper' => 'De URL van uw Apprise Server. De URL moet eindigen op /notify', 'apprise_verify_ssl' => 'Controleer SSL', - 'apprise_channels' => 'Apprise Kanalen', - 'apprise_channel_url' => 'Kanaal URL', - 'apprise_hint_description' => 'Voor meer informatie over het instellen van Apprise, bekijk de documentatie.', - 'apprise_channel_url_helper' => 'Geef de service eindpunt URL voor meldingen.', + 'apprise_channels' => 'Notificatie kanalen', + 'apprise_channel_url' => 'Service URL', + 'apprise_hint_description' => 'Met Apprise kan je meldingen verzenden naar meer dan 90 diensten. Je moet een Apprise server hebben draaien en onderstaande service URL\'s configureren.', + 'apprise_channel_url_helper' => 'Gebruik Apprise URL formaat. Bijvoorbeeld discord://WebhookID/Token, slack://TokenA/TokenB/TokenC', 'test_apprise_channel' => 'Test Apprise', - 'apprise_channel_url_validation_error' => 'De URL van het Apprise kanaal mag niet beginnen met "http" of "https". Geef een geldig URL-schema op.', + 'apprise_channel_url_validation_error' => 'Ongeldige Apprise URL. De URL moet gebruik maken van Apprise formaat (bijv. discord://, slack://), niet http:// of https://. Zie de Apprise documentatie voor meer informatie', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', 'test_webhook_channel' => 'Test webhook kanaal', - 'webhook_hint_description' => 'Dit zijn generieke webhooks. Raadpleeg de documentatie voor voorbeelden van payloads en implementatiedetails.', + 'webhook_hint_description' => 'Dit zijn algemene webhooks. Voor payload voorbeelden en implementatiegegevens, bekijk de documentatie. Voor diensten zoals Discord, Ntfy etc. gebruik Apprise.', // Common notification messages 'notify_on_every_speedtest_run' => 'Notificatie bij elke geplande snelheidstest', From 94159115693c897e3c2e56bd57022ab436c2d7a3 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 26 Dec 2025 15:16:31 -0500 Subject: [PATCH 43/63] Release v1.13.4 (#2605) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 94ebcaa84..79c2e84b4 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-19'), + 'build_date' => Carbon::parse('2025-12-26'), - 'build_version' => 'v1.13.3', + 'build_version' => 'v1.13.4', 'content_width' => env('CONTENT_WIDTH', '7xl'), From fd7a034235cca2150cea114d373a3c4ef7b93c7d Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Fri, 26 Dec 2025 20:16:11 -0500 Subject: [PATCH 44/63] Rename benchmark healthy/unhealthy event classes and update references (#2607) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- ...tBenchmarkFailed.php => SpeedtestBenchmarkHealthy.php} | 2 +- ...enchmarkPassed.php => SpeedtestBenchmarkUnhealthy.php} | 2 +- app/Jobs/Ookla/BenchmarkSpeedtestJob.php | 8 ++++---- app/Listeners/ProcessUnhealthySpeedtest.php | 4 ++-- app/Listeners/UserNotificationSubscriber.php | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename app/Events/{SpeedtestBenchmarkFailed.php => SpeedtestBenchmarkHealthy.php} (90%) rename app/Events/{SpeedtestBenchmarkPassed.php => SpeedtestBenchmarkUnhealthy.php} (90%) diff --git a/app/Events/SpeedtestBenchmarkFailed.php b/app/Events/SpeedtestBenchmarkHealthy.php similarity index 90% rename from app/Events/SpeedtestBenchmarkFailed.php rename to app/Events/SpeedtestBenchmarkHealthy.php index b00175b13..2838d442b 100644 --- a/app/Events/SpeedtestBenchmarkFailed.php +++ b/app/Events/SpeedtestBenchmarkHealthy.php @@ -6,7 +6,7 @@ use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class SpeedtestBenchmarkFailed +class SpeedtestBenchmarkHealthy { use Dispatchable, SerializesModels; diff --git a/app/Events/SpeedtestBenchmarkPassed.php b/app/Events/SpeedtestBenchmarkUnhealthy.php similarity index 90% rename from app/Events/SpeedtestBenchmarkPassed.php rename to app/Events/SpeedtestBenchmarkUnhealthy.php index ab8e9ae82..92b706e35 100644 --- a/app/Events/SpeedtestBenchmarkPassed.php +++ b/app/Events/SpeedtestBenchmarkUnhealthy.php @@ -6,7 +6,7 @@ use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class SpeedtestBenchmarkPassed +class SpeedtestBenchmarkUnhealthy { use Dispatchable, SerializesModels; diff --git a/app/Jobs/Ookla/BenchmarkSpeedtestJob.php b/app/Jobs/Ookla/BenchmarkSpeedtestJob.php index c1298a147..6f6f0751a 100644 --- a/app/Jobs/Ookla/BenchmarkSpeedtestJob.php +++ b/app/Jobs/Ookla/BenchmarkSpeedtestJob.php @@ -3,9 +3,9 @@ namespace App\Jobs\Ookla; use App\Enums\ResultStatus; -use App\Events\SpeedtestBenchmarkFailed; +use App\Events\SpeedtestBenchmarkHealthy; use App\Events\SpeedtestBenchmarking; -use App\Events\SpeedtestBenchmarkPassed; +use App\Events\SpeedtestBenchmarkUnhealthy; use App\Helpers\Benchmark; use App\Models\Result; use App\Settings\ThresholdSettings; @@ -70,8 +70,8 @@ public function handle(): void ]); $this->healthy - ? SpeedtestBenchmarkPassed::dispatch($this->result) - : SpeedtestBenchmarkFailed::dispatch($this->result); + ? SpeedtestBenchmarkHealthy::dispatch($this->result) + : SpeedtestBenchmarkUnhealthy::dispatch($this->result); } private function benchmark(Result $result, ThresholdSettings $settings): array diff --git a/app/Listeners/ProcessUnhealthySpeedtest.php b/app/Listeners/ProcessUnhealthySpeedtest.php index f2e0d419c..288f60739 100644 --- a/app/Listeners/ProcessUnhealthySpeedtest.php +++ b/app/Listeners/ProcessUnhealthySpeedtest.php @@ -2,7 +2,7 @@ namespace App\Listeners; -use App\Events\SpeedtestBenchmarkFailed; +use App\Events\SpeedtestBenchmarkUnhealthy; use App\Helpers\Number; use App\Mail\UnhealthySpeedtestMail; use App\Models\Result; @@ -29,7 +29,7 @@ public function __construct( /** * Handle the event. */ - public function handle(SpeedtestBenchmarkFailed $event): void + public function handle(SpeedtestBenchmarkUnhealthy $event): void { $result = $event->result; diff --git a/app/Listeners/UserNotificationSubscriber.php b/app/Listeners/UserNotificationSubscriber.php index 4ff7dc7c8..aefa0eaae 100644 --- a/app/Listeners/UserNotificationSubscriber.php +++ b/app/Listeners/UserNotificationSubscriber.php @@ -2,7 +2,7 @@ namespace App\Listeners; -use App\Events\SpeedtestBenchmarkFailed; +use App\Events\SpeedtestBenchmarkUnhealthy; use App\Events\SpeedtestCompleted; use App\Events\SpeedtestFailed; use Filament\Actions\Action; @@ -38,7 +38,7 @@ public function handleCompleted(SpeedtestCompleted $event): void /** * Handle the event. */ - public function handleBenchmarkFailed(SpeedtestBenchmarkFailed $event): void + public function handleBenchmarkFailed(SpeedtestBenchmarkUnhealthy $event): void { $result = $event->result; @@ -97,7 +97,7 @@ public function subscribe(Dispatcher $events): array { return [ SpeedtestCompleted::class => 'handleCompleted', - SpeedtestBenchmarkFailed::class => 'handleBenchmarkFailed', + SpeedtestBenchmarkUnhealthy::class => 'handleBenchmarkFailed', SpeedtestFailed::class => 'handleFailed', ]; } From 190b7ef486885ee6efba09e725bbbd6da3e12077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:02:43 -0500 Subject: [PATCH 45/63] gh actions: bump actions/create-github-app-token from 1 to 2 (#2621) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 73817d100..b383b2c34 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Generate GitHub App token id: generate_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} From 1ea93d2b3b3ce3fe0faa1f1f74e24022319fd1b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:03:51 -0500 Subject: [PATCH 46/63] gh actions: bump actions/cache from 4 to 5 (#2622) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Justesen --- .github/workflows/ci.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abe74743b..1e6db763d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -57,7 +57,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -65,7 +65,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -115,7 +115,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -123,7 +123,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -173,7 +173,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -181,7 +181,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -231,7 +231,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -239,7 +239,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -289,7 +289,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -297,7 +297,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -347,7 +347,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -355,7 +355,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -405,7 +405,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -413,7 +413,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -454,7 +454,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -462,7 +462,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} From f9be7c6cc2afab778bb5813d9de728f13d363d23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:05:14 -0500 Subject: [PATCH 47/63] gh actions: bump peter-evans/repository-dispatch from 3 to 4 (#2623) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Justesen --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index b383b2c34..f6e40687f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -19,7 +19,7 @@ jobs: repositories: docker-speedtest-tracker - name: Trigger docker-speedtest-tracker build - uses: peter-evans/repository-dispatch@v3 + uses: peter-evans/repository-dispatch@v4 with: token: ${{ steps.generate_token.outputs.token }} repository: alexjustesen/docker-speedtest-tracker From 0e405e3f63d62b3fc936c847fa820d51c332c49b Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Fri, 2 Jan 2026 22:07:57 +0100 Subject: [PATCH 48/63] chore: update bug report template with support guidelines (#2591) Co-authored-by: Alex Justesen --- .github/ISSUE_TEMPLATE/bug_report.yml | 79 ++++++++++++++++------ .github/ISSUE_TEMPLATE/feature_request.yml | 37 +++++++--- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cf8b5abcb..ab4407dcc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,67 +2,100 @@ name: Bug Report description: Use this template to report a bug or issue. title: "[Question] " labels: ["question", "needs review"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to report this issue! We appreciate your help in improving the project. If this report is confirmed as a bug, we’ll update its type accordingly. - Please note: - - For **feature requests or changes**, use the [feature request form](https://github.com/alexjustesen/speedtest-tracker/issues/new?template=feature_request.yml). - - For **general questions**, **setup or configuration help**, or if you’re not sure this is a bug, please use **[GitHub Discussions](https://github.com/alexjustesen/speedtest-tracker/discussions)** instead. - - Any isseus with translations should be reported/solved within the [crowdin project](https://crowdin.com/project/speedtest-tracker). +body: - type: checkboxes + id: terms attributes: - label: Pre-work + label: Welcome! description: | - Before opening an issue make sure you've checked the resources below first, any issues that could have been solved by reading the docs or existing issues will be closed. + The issue tracker is for reporting bugs and feature requests only. For end-user related support questions, please use the **[GitHub Discussions](https://github.com/alexjustesen/speedtest-tracker/discussions)** instead + + Please note: + - For translation-related issues or requests, please use the [Crowdin project](https://crowdin.com/project/speedtest-tracker). + - Any issues that can be resolved by consulting the documentation or by reviewing existing open or closed issues will be closed. + - We only support installations that follow the methods described in the documentation. Installations using third-party or undocumented methods are not supported by the project. + options: - - label: I have read the [docs](https://docs.speedtest-tracker.dev). + - label: I have read the [documentation](https://docs.speedtest-tracker.dev) and my problem was not listed in the help section. + required: true + - label: I have searched open and closed issues and my problem was not mentioned before. required: true - - label: I have searched open and closed issues. + - label: I have verified I am using the latest version available. You can check the latest release [here](https://github.com/alexjustesen/speedtest-tracker/releases). required: true - label: I agree to follow this project's [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md). required: true + - type: textarea id: description attributes: - label: Description - description: Explain the issue you experienced, please be clear and concise. - placeholder: I went to the coffee pot and it was empty. + label: What did you do? + description: | + How to write a good bug report? + + - Respect the issue template as much as possible. + - The title should be short and descriptive. + - Explain the conditions which led you to report this issue: the context. + - The context should lead to something, a problem that you’re facing. + - Remain clear and concise. + - Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown) validations: required: true + - type: textarea id: expected-behavior attributes: label: Expected Behavior - description: In a perfect world, what should have happened? + description: | + In a perfect world, what should have happened? + **Important:** Be specific. Vague descriptions like "it should work" are not helpful. placeholder: When I got to the coffee pot, it should have been full. validations: required: true + - type: textarea id: steps-to-reproduce attributes: label: Steps to Reproduce - description: Describe how to reproduce the issue in repeatable steps. + description: | + Provide detailed, numbered steps that someone else can follow to reproduce the issue. + **Important:** Vague descriptions like "it doesn't work" or "it's broken" will result in the issue being closed. + Include specific actions, URLs, button clicks, and any relevant data or configuration. placeholder: | 1. Go to the coffee pot. 2. Make more coffee. 3. Pour it into a cup. + 4. Observe that the cup is empty instead of full. validations: required: true + - type: dropdown id: deployment-environment attributes: label: Deployment Environment - description: How did you deploy the application? + description: How did you deploy the application? Only supported deployment methods are listed. options: - Docker Compose - Docker Run - - Other default: 0 validations: required: true + + - type: textarea + id: environment-configuration + attributes: + label: What is your environment & configuration? + description: Please add your docker compose file or docker run command used to deploy the application. + placeholder: Add information here. + value: | + ```yaml + # (paste your configuration here) + ``` + + Add more configuration information here. + validations: + required: true + - type: textarea id: application-information attributes: @@ -71,6 +104,7 @@ body: render: json validations: required: true + - type: input id: browsers attributes: @@ -78,9 +112,12 @@ body: placeholder: Chrome, Firefox, Safari, etc. validations: required: true + - type: textarea id: logs attributes: label: Logs - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. If you are unsure which logs to include, include all logs. You can get the logs by running `docker logs `. render: shell + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index aa13ebc08..bdd144689 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,22 +2,28 @@ name: Feature Request description: Use this template for requesting a new feature or change. title: "[Feature] " labels: ["feature", "needs review"] + body: - - type: markdown - attributes: - value: | - You should only use this form to request a change or new feature, to report a bug or issue use the [bug report form](https://github.com/alexjustesen/speedtest-tracker). - Any reqeusts for new translations should be reqeusted within the [crowdin project](https://crowdin.com/project/speedtest-tracker). - type: checkboxes attributes: - label: Pre-work + label: Welcome! + description: | + The issue tracker is for reporting bugs and feature requests only. For end-user related support questions, please use the **[GitHub Discussions](https://github.com/alexjustesen/speedtest-tracker/discussions)** instead + + Please note: + - For **Bug reports**, use the [Bug Form](https://github.com/alexjustesen/speedtest-tracker/issues/new?template=bug_report.yml). + - Any requests for new translations should be requested within the [crowdin project](https://crowdin.com/project/speedtest-tracker). + options: - - label: I have searched open and closed feature request to make sure this or similar feature request does not already exist. + - label: I have searched open and closed feature requests to make sure this or similar feature request does not already exist. + required: true + - label: I have reviewed the [Milestones](https://github.com/alexjustesen/speedtest-tracker/milestones) to ensure that this feature request, or a similar one, has not already been proposed. required: true - - label: I have reviewed the [milestones](https://github.com/alexjustesen/speedtest-tracker/milestones) to ensure that this feature request, or a similar one, has not already been proposed. + - label: This is a feature request, not a bug report or support question. required: true - - label: I agree to follow this project's [Code of Conduct](). + - label: I agree to follow this project's [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md). required: true + - type: dropdown id: idea-section attributes: @@ -28,14 +34,23 @@ body: - Notifications - Speedtest - Web UI/UX + - Other default: 0 validations: required: true + - type: textarea id: description attributes: label: Description - description: Describe the solution or feature you'd like, you should also mention if this solves a problem. - placeholder: Be sure to keep it clear and concise. + description: | + Describe the solution or feature you'd like. Explain what problem this solves or what value it adds. + **Important:** Be specific and detailed. Vague requests like "make it better" will be closed. + placeholder: | + Example: + - What is the feature? + - What problem does it solve? + - How should it work? + - Why would this be valuable? validations: required: true From 07aaf84a4bcd0a43cc43818bf73175ae96ce6476 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Sat, 3 Jan 2026 14:09:51 +0100 Subject: [PATCH 49/63] Fix: Apprise test button shown without saving (#2627) --- app/Filament/Pages/Settings/Notification.php | 7 ++++++- lang/en/settings/notifications.php | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 7e1c1a58a..27d26bc39 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -257,6 +257,7 @@ public function form(Schema $schema): Schema ]), Repeater::make('apprise_channel_urls') ->label(__('settings/notifications.apprise_channels')) + ->helperText(__('settings/notifications.apprise_save_to_test')) ->schema([ TextInput::make('channel_url') ->label(__('settings/notifications.apprise_channel_url')) @@ -274,7 +275,11 @@ public function form(Schema $schema): Schema ->action(fn (Get $get) => SendAppriseTestNotification::run( channel_urls: $get('apprise_channel_urls'), )) - ->hidden(fn (Get $get) => ! count($get('apprise_channel_urls'))), + ->hidden(function () { + $settings = app(NotificationSettings::class); + + return empty($settings->apprise_server_url) || ! count($settings->apprise_channel_urls ?? []); + }), ]), ]), ]), diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 788f31d91..87c82e7da 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -25,6 +25,7 @@ 'apprise_channel_url' => 'Service URL', 'apprise_hint_description' => 'Apprise allows you to send notifications to 90+ services. You need to run an Apprise server and configure service URLs below.', 'apprise_channel_url_helper' => 'Use Apprise URL format. Examples: discord://WebhookID/Token, slack://TokenA/TokenB/TokenC', + 'apprise_save_to_test' => 'Save your settings to test the notification.', 'test_apprise_channel' => 'Test Apprise', 'apprise_channel_url_validation_error' => 'Invalid Apprise URL. Must use Apprise format (e.g., discord://, slack://), not http:// or https://. See the Apprise documentation for more information', From d0377e15a711ec68b9e8a2a985959280de987b9f Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Sat, 3 Jan 2026 14:14:31 +0100 Subject: [PATCH 50/63] Fix: Skip SkipSpeedtestJob when env var is empty (#2626) Co-authored-by: Alex Justesen --- app/Jobs/Ookla/SkipSpeedtestJob.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/Ookla/SkipSpeedtestJob.php b/app/Jobs/Ookla/SkipSpeedtestJob.php index 948fe792d..773d4a793 100644 --- a/app/Jobs/Ookla/SkipSpeedtestJob.php +++ b/app/Jobs/Ookla/SkipSpeedtestJob.php @@ -40,9 +40,9 @@ public function middleware(): array public function handle(): void { /** - * Only skip IPs for scheduled tests. + * Skip if test is not scheduled or no IPs are configured to skip. */ - if ($this->result->scheduled === false) { + if ($this->result->scheduled === false || empty(config('speedtest.preflight.skip_ips'))) { return; } From 5bb28c23a9ed7c6deb2faefb4ae39501bfbb77f2 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Sun, 4 Jan 2026 15:31:00 +0100 Subject: [PATCH 51/63] Fix: Increase timeout for Apprise notifications (#2628) Co-authored-by: Alex Justesen --- app/Notifications/AppriseChannel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Notifications/AppriseChannel.php b/app/Notifications/AppriseChannel.php index 0e9ed1087..c6fe6a1c3 100644 --- a/app/Notifications/AppriseChannel.php +++ b/app/Notifications/AppriseChannel.php @@ -33,7 +33,7 @@ public function send(object $notifiable, Notification $notification): void } try { - $request = Http::timeout(5) + $request = Http::timeout(30) ->withHeaders([ 'Content-Type' => 'application/json', ]); From 82fdd6fa79cfb773050b17470b0bc586d03de307 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Sun, 4 Jan 2026 15:32:11 +0100 Subject: [PATCH 52/63] Chore: clean up ping logs (#2629) Co-authored-by: Alex Justesen --- app/Actions/PingHostname.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Actions/PingHostname.php b/app/Actions/PingHostname.php index 5bbf15ddf..04884fe31 100644 --- a/app/Actions/PingHostname.php +++ b/app/Actions/PingHostname.php @@ -23,9 +23,12 @@ public function handle(?string $hostname = null, int $count = 1): PingResult count: $count, ))->run(); - Log::info('Pinged hostname', [ + $data = $ping->toArray(); + unset($data['raw_output'], $data['lines']); + + Log::debug('Pinged hostname', [ 'host' => $hostname, - 'data' => $ping->toArray(), + 'data' => $data, ]); return $ping; From f1480e4c0529ecd5bd4098b59e795ade1d366234 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Thu, 8 Jan 2026 23:20:46 +0100 Subject: [PATCH 53/63] chore: add missing translation strings (#2639) --- lang/en/general.php | 4 ++++ resources/views/dashboard.blade.php | 2 +- resources/views/livewire/latest-result-stats.blade.php | 2 +- resources/views/livewire/next-speedtest-banner.blade.php | 2 +- resources/views/livewire/platform-stats.blade.php | 6 +++--- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lang/en/general.php b/lang/en/general.php index 9b8aa7d95..2d39844c2 100644 --- a/lang/en/general.php +++ b/lang/en/general.php @@ -73,6 +73,10 @@ 'healthy' => 'Healthy', 'not_measured' => 'Not measured', 'unhealthy' => 'Unhealthy', + 'last_results' => 'Last results', + 'total_failed' => 'Total failed tests', + 'total_complted' => 'Total completed tests', + 'total' => 'Total', // Units 'ms' => 'ms', diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 967996d47..c66c27c1c 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -9,7 +9,7 @@

- Metrics + {{ __('general.metrics') }}

@livewire(\App\Filament\Widgets\RecentDownloadChartWidget::class) diff --git a/resources/views/livewire/latest-result-stats.blade.php b/resources/views/livewire/latest-result-stats.blade.php index c1c69eeec..f6997016a 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -6,7 +6,7 @@

- Latest result + {{ __('general.last_results') }}

{{ $this->latestResult->created_at->timezone(config('app.display_timezone'))->format(config('app.datetime_format')) }}

diff --git a/resources/views/livewire/next-speedtest-banner.blade.php b/resources/views/livewire/next-speedtest-banner.blade.php index e66d3c1ee..f7f9211c0 100644 --- a/resources/views/livewire/next-speedtest-banner.blade.php +++ b/resources/views/livewire/next-speedtest-banner.blade.php @@ -8,7 +8,7 @@

- Next scheduled test at {{ $this->nextSpeedtest->timezone(config('app.display_timezone'))->format(config('app.datetime_format')) }}. + {{ __('dashboard.next_speedtest_at') }} {{ $this->nextSpeedtest->timezone(config('app.display_timezone'))->format(config('app.datetime_format')) }}.

diff --git a/resources/views/livewire/platform-stats.blade.php b/resources/views/livewire/platform-stats.blade.php index f8f149e6e..00caf7a17 100644 --- a/resources/views/livewire/platform-stats.blade.php +++ b/resources/views/livewire/platform-stats.blade.php @@ -25,7 +25,7 @@ - Total tests + {{ __('general.total') }}

{{ $this->platformStats['total'] }}

@@ -33,7 +33,7 @@ - Total completed tests + {{ __('general.total_complted') }}

{{ $this->platformStats['completed'] }}

@@ -41,7 +41,7 @@ - Total failed tests + {{ __('general.total_failed') }}

{{ $this->platformStats['failed'] }}

From 0e104db343df968a303556e1e60002ed97bcc870 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Thu, 8 Jan 2026 23:23:11 +0100 Subject: [PATCH 54/63] Chore: Add helpertext for notification triggers (#2640) Co-authored-by: Alex Justesen --- app/Filament/Pages/Settings/Notification.php | 23 ++++++++++++-------- lang/en/settings/notifications.php | 4 +++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 27d26bc39..c01ee4e1f 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -86,10 +86,11 @@ public function form(Schema $schema): Schema ->columns(1) ->schema([ Checkbox::make('database_on_speedtest_run') - ->label(__('settings/notifications.notify_on_every_speedtest_run')), - + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')), Checkbox::make('database_on_threshold_failure') - ->label(__('settings/notifications.notify_on_threshold_failures')), + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')), ]), Actions::make([ @@ -118,10 +119,11 @@ public function form(Schema $schema): Schema ->columns(1) ->schema([ Checkbox::make('mail_on_speedtest_run') - ->label(__('settings/notifications.notify_on_every_speedtest_run')), - + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')), Checkbox::make('mail_on_threshold_failure') - ->label(__('settings/notifications.notify_on_threshold_failures')), + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')), ]), Repeater::make('mail_recipients') @@ -176,10 +178,11 @@ public function form(Schema $schema): Schema ->columns(1) ->schema([ Checkbox::make('webhook_on_speedtest_run') - ->label(__('settings/notifications.notify_on_every_speedtest_run')), - + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')), Checkbox::make('webhook_on_threshold_failure') - ->label(__('settings/notifications.notify_on_threshold_failures')), + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')), ]), Repeater::make('webhook_urls') @@ -250,9 +253,11 @@ public function form(Schema $schema): Schema ->schema([ Checkbox::make('apprise_on_speedtest_run') ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')) ->columnSpanFull(), Checkbox::make('apprise_on_threshold_failure') ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')) ->columnSpanFull(), ]), Repeater::make('apprise_channel_urls') diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 87c82e7da..8c3145532 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -36,8 +36,10 @@ 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation. For services like Discord, Ntfy etc please use Apprise.', // Common notification messages - 'notify_on_every_speedtest_run' => 'Notify on every scheduled speedtest run', + 'notify_on_every_speedtest_run' => 'Notify on every completed scheduled speedtest run', + 'notify_on_every_speedtest_run_helper' => 'This will send a notification for every completed scheduled speedtest run, only for healthy or unbenchmarked tests', 'notify_on_threshold_failures' => 'Notify on threshold failures for scheduled speedtests', + 'notify_on_threshold_failures_helper' => 'This will send a notification when a scheduled speedtest fails any configured thresholds', // Test notification messages 'test_notifications' => [ From cc0f0d36e3ce64eadce286c909f5a76353b77298 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Thu, 8 Jan 2026 17:34:20 -0500 Subject: [PATCH 55/63] Release v1.13.5 (#2641) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 79c2e84b4..4226f5a0a 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-12-26'), + 'build_date' => Carbon::parse('2026-01-08'), - 'build_version' => 'v1.13.4', + 'build_version' => 'v1.13.5', 'content_width' => env('CONTENT_WIDTH', '7xl'), From 7d74172de9f91549af0bca02fcce9ffc59683f9e Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Mon, 12 Jan 2026 18:33:03 +0100 Subject: [PATCH 56/63] chore: improve dev container (#2648) --- .devcontainer/devcontainer.json | 6 +++--- .env.example | 9 +++++++++ compose.yaml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7ec51536b..04653336d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,8 @@ // https://aka.ms/devcontainer.json { - "name": "Existing Docker Compose (Extend)", + "name": "Speedtest Tracker Dev Environment", "dockerComposeFile": [ - "../docker-compose.yml" + "../compose.yaml" ], "service": "laravel.test", "workspaceFolder": "/var/www/html", @@ -20,7 +20,7 @@ } }, "remoteUser": "sail", - "postCreateCommand": "chown -R 1000:1000 /var/www/html 2>/dev/null || true" + "postCreateCommand": "composer install && npm install && npm run build && touch database/database.sqlite && php artisan migrate:fresh --force" // "forwardPorts": [], // "runServices": [], // "shutdownAction": "none", diff --git a/.env.example b/.env.example index 1a762a1ce..b09ac0014 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,11 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=sqlite +#DB_HOST= +#DB_PORT= +#DB_DATABASE= +#DB_USERNAME= +#DB_PASSWORD= SESSION_DRIVER=cookie SESSION_LIFETIME=10080 @@ -42,3 +47,7 @@ MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="Speedtest Tracker" VITE_APP_NAME="${APP_NAME}" + +# For the Dev Container +# WWWUSER=1000 +# WWWGROUP=1000 diff --git a/compose.yaml b/compose.yaml index acd813d9b..a4ccb9e57 100644 --- a/compose.yaml +++ b/compose.yaml @@ -25,7 +25,7 @@ services: - mailpit - apprise pgsql: - image: 'postgres:17-alpine' + image: 'postgres:18-alpine' ports: - '${FORWARD_DB_PORT:-5432}:5432' environment: From b7846b0b1d08d7c24073c91ed96bd60267eba140 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Wed, 21 Jan 2026 15:05:38 +0100 Subject: [PATCH 57/63] chore: update webhook payload (#2636) --- .../Notifications/SendWebhookTestNotification.php | 2 +- app/Jobs/Ookla/BenchmarkSpeedtestJob.php | 10 +++++++--- app/Listeners/ProcessCompletedSpeedtest.php | 7 ++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/Actions/Notifications/SendWebhookTestNotification.php b/app/Actions/Notifications/SendWebhookTestNotification.php index 9ae6d922d..45aa2abc5 100644 --- a/app/Actions/Notifications/SendWebhookTestNotification.php +++ b/app/Actions/Notifications/SendWebhookTestNotification.php @@ -37,7 +37,7 @@ public function handle(array $webhooks) 'ping' => $fakeResult->ping, 'download' => $fakeResult->download, 'upload' => $fakeResult->upload, - 'packetLoss' => $fakeResult->data['packetLoss'], + 'packet_loss' => $fakeResult->data['packetLoss'], 'speedtest_url' => $fakeResult->data['result']['url'], 'url' => url('/admin/results'), ]) diff --git a/app/Jobs/Ookla/BenchmarkSpeedtestJob.php b/app/Jobs/Ookla/BenchmarkSpeedtestJob.php index 6f6f0751a..683fe395b 100644 --- a/app/Jobs/Ookla/BenchmarkSpeedtestJob.php +++ b/app/Jobs/Ookla/BenchmarkSpeedtestJob.php @@ -7,6 +7,7 @@ use App\Events\SpeedtestBenchmarking; use App\Events\SpeedtestBenchmarkUnhealthy; use App\Helpers\Benchmark; +use App\Helpers\Number; use App\Models\Result; use App\Settings\ThresholdSettings; use Illuminate\Bus\Batchable; @@ -83,7 +84,8 @@ private function benchmark(Result $result, ThresholdSettings $settings): array 'bar' => 'min', 'passed' => Benchmark::bitrate($result->download, ['value' => $settings->absolute_download, 'unit' => 'mbps']), 'type' => 'absolute', - 'value' => $settings->absolute_download, + 'test_value' => Number::bitsToMagnitude(bits: $result->download_bits, precision: 0, magnitude: 'mbit'), + 'benchmark_value' => $settings->absolute_download, 'unit' => 'mbps', ]); @@ -97,7 +99,8 @@ private function benchmark(Result $result, ThresholdSettings $settings): array 'bar' => 'min', 'passed' => filter_var(Benchmark::bitrate($result->upload, ['value' => $settings->absolute_upload, 'unit' => 'mbps']), FILTER_VALIDATE_BOOLEAN), 'type' => 'absolute', - 'value' => $settings->absolute_upload, + 'test_value' => Number::bitsToMagnitude(bits: $result->upload_bits, precision: 0, magnitude: 'mbit'), + 'benchmark_value' => $settings->absolute_upload, 'unit' => 'mbps', ]); @@ -111,7 +114,8 @@ private function benchmark(Result $result, ThresholdSettings $settings): array 'bar' => 'max', 'passed' => Benchmark::ping($result->ping, ['value' => $settings->absolute_ping]), 'type' => 'absolute', - 'value' => $settings->absolute_ping, + 'test_value' => round($result->ping), + 'benchmark_value' => $settings->absolute_ping, 'unit' => 'ms', ]); diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index ae5d544e1..87b7b7794 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -162,10 +162,11 @@ private function notifyWebhookChannels(Result $result): void 'site_name' => config('app.name'), 'server_name' => Arr::get($result->data, 'server.name'), 'server_id' => Arr::get($result->data, 'server.id'), + 'status' => $result->status, 'isp' => Arr::get($result->data, 'isp'), - 'ping' => $result->ping, - 'download' => $result->downloadBits, - 'upload' => $result->uploadBits, + 'ping' => round($result->ping), + 'download' => Number::bitsToMagnitude(bits: $result->download_bits, precision: 0, magnitude: 'mbit'), + 'upload' => Number::bitsToMagnitude(bits: $result->upload_bits, precision: 0, magnitude: 'mbit'), 'packet_loss' => Arr::get($result->data, 'packetLoss'), 'speedtest_url' => Arr::get($result->data, 'result.url'), 'url' => url('/admin/results'), From 40eca53bcbc4204d547bbe909712918ef757d2d0 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 3 Feb 2026 14:42:14 +0100 Subject: [PATCH 58/63] Chore: Add missing translations (#2669) --- app/Filament/Widgets/Concerns/HasChartFilters.php | 6 +++--- lang/en/general.php | 2 ++ resources/views/filament/pages/dashboard.blade.php | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/Filament/Widgets/Concerns/HasChartFilters.php b/app/Filament/Widgets/Concerns/HasChartFilters.php index ce12d9384..a792e07c9 100644 --- a/app/Filament/Widgets/Concerns/HasChartFilters.php +++ b/app/Filament/Widgets/Concerns/HasChartFilters.php @@ -7,9 +7,9 @@ trait HasChartFilters protected function getFilters(): ?array { return [ - '24h' => 'Last 24 hours', - 'week' => 'Last 7 days', - 'month' => 'Last 30 days', + '24h' => __('general.last_24h'), + 'week' => __('general.last_week'), + 'month' => __('general.last_month'), ]; } } diff --git a/lang/en/general.php b/lang/en/general.php index 2d39844c2..65ffa88ee 100644 --- a/lang/en/general.php +++ b/lang/en/general.php @@ -48,10 +48,12 @@ 'settings' => 'Settings', 'users' => 'Users', 'documentation' => 'Documentation', + 'documentation_description' => 'Need help getting started or configuring your speedtests?', 'view_documentation' => 'View documentation', 'links' => 'Links', 'donate' => 'Donate', 'donations' => 'Donations', + 'donations_description' => 'Support the development and maintenance of Speedtest Tracker by making a donation.', // Roles 'admin' => 'Admin', diff --git a/resources/views/filament/pages/dashboard.blade.php b/resources/views/filament/pages/dashboard.blade.php index 2d9d68211..4dd7514ee 100644 --- a/resources/views/filament/pages/dashboard.blade.php +++ b/resources/views/filament/pages/dashboard.blade.php @@ -20,7 +20,7 @@ class="col-span-1"
-

Need help getting started or configuring your speedtests?

+

{{ __('general.documentation_description') }}

@@ -45,7 +45,7 @@ class="col-span-1"
-

Support the development and maintenance of Speedtest Tracker by making a donation.

+

{{ __('general.donations_description') }}

From 3fa025d19f6c773f5726168ffde4b4551c354ba0 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 3 Feb 2026 14:50:33 +0100 Subject: [PATCH 59/63] Chore: add webhook logging (#2647) Co-authored-by: Alex Justesen --- .../SendWebhookTestNotification.php | 2 +- app/Listeners/LogWebhookFailure.php | 21 +++++++++++++++++++ lang/en/settings/notifications.php | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/Listeners/LogWebhookFailure.php diff --git a/app/Actions/Notifications/SendWebhookTestNotification.php b/app/Actions/Notifications/SendWebhookTestNotification.php index 45aa2abc5..e34dcce0a 100644 --- a/app/Actions/Notifications/SendWebhookTestNotification.php +++ b/app/Actions/Notifications/SendWebhookTestNotification.php @@ -42,7 +42,7 @@ public function handle(array $webhooks) 'url' => url('/admin/results'), ]) ->doNotSign() - ->dispatch(); + ->dispatchSync(); } Notification::make() diff --git a/app/Listeners/LogWebhookFailure.php b/app/Listeners/LogWebhookFailure.php new file mode 100644 index 000000000..d9884b873 --- /dev/null +++ b/app/Listeners/LogWebhookFailure.php @@ -0,0 +1,21 @@ + $event->webhookUrl, + 'error_type' => $event->errorType, + 'error_message' => $event->errorMessage, + ]); + } +} diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 8c3145532..8ccd940a2 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -56,6 +56,7 @@ 'webhook' => [ 'add' => 'Add webhook URLs!', 'sent' => 'Test webhook notification sent.', + 'failed' => 'Webhook notification failed.', 'payload' => 'Testing webhook notification', ], ], From c3bf5e5ed23c519618981d7657d40bbab9c11ea7 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 3 Feb 2026 14:53:12 +0100 Subject: [PATCH 60/63] Chore: Add debug logs for apprise (#2654) Co-authored-by: Alex Justesen --- app/Notifications/AppriseChannel.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Notifications/AppriseChannel.php b/app/Notifications/AppriseChannel.php index c6fe6a1c3..b5885202b 100644 --- a/app/Notifications/AppriseChannel.php +++ b/app/Notifications/AppriseChannel.php @@ -32,6 +32,11 @@ public function send(object $notifiable, Notification $notification): void return; } + Log::debug('Attempting to send Apprise notification', [ + 'channel' => $message->urls, + 'instance' => $appriseUrl, + ]); + try { $request = Http::timeout(30) ->withHeaders([ @@ -57,7 +62,7 @@ public function send(object $notifiable, Notification $notification): void throw new Exception('Apprise returned an error, please check Apprise logs for details'); } - Log::info('Apprise notification sent', [ + Log::debug('Apprise notification sent', [ 'channel' => $message->urls, 'instance' => $appriseUrl, ]); From d5ebe7ca57876451d5835e2631272729eaa4ccbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:55:11 -0500 Subject: [PATCH 61/63] gh actions: bump klaasnicolaas/action-inactivity-lock from 1.1.3 to 2.0.1 (#2677) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Justesen --- .github/workflows/inactivity-actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/inactivity-actions.yml b/.github/workflows/inactivity-actions.yml index e4f4ab5b9..8cf93b3c6 100644 --- a/.github/workflows/inactivity-actions.yml +++ b/.github/workflows/inactivity-actions.yml @@ -14,7 +14,7 @@ jobs: name: Lock Inactive Issues runs-on: ubuntu-24.04 steps: - - uses: klaasnicolaas/action-inactivity-lock@v1.1.3 + - uses: klaasnicolaas/action-inactivity-lock@v2.0.1 id: lock with: days-inactive-issues: 14 From edff46859c781630b42498f3167f8f5a63d3990c Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 3 Feb 2026 15:19:17 +0100 Subject: [PATCH 62/63] Bug: fix packet loss rounding in notifications (#2670) --- app/Listeners/ProcessCompletedSpeedtest.php | 2 +- app/Mail/CompletedSpeedtestMail.php | 2 +- resources/views/apprise/speedtest-completed.blade.php | 2 ++ resources/views/mail/speedtest/completed.blade.php | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index 87b7b7794..c1399c076 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -72,7 +72,7 @@ private function notifyAppriseChannels(Result $result): void 'ping' => round($result->ping).' ms', 'download' => Number::toBitRate(bits: $result->download_bits, precision: 2), 'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2), - 'packetLoss' => $result->packet_loss, + 'packetLoss' => $result->packet_loss ? round($result->packet_loss, precision: 2) : '', 'speedtest_url' => $result->result_url, 'url' => url('/admin/results'), ])->render(); diff --git a/app/Mail/CompletedSpeedtestMail.php b/app/Mail/CompletedSpeedtestMail.php index 109d95360..cfd7cecf6 100644 --- a/app/Mail/CompletedSpeedtestMail.php +++ b/app/Mail/CompletedSpeedtestMail.php @@ -51,7 +51,7 @@ public function content(): Content 'ping' => round($this->result->ping, 2).' ms', 'download' => Number::toBitRate(bits: $this->result->download_bits, precision: 2), 'upload' => Number::toBitRate(bits: $this->result->upload_bits, precision: 2), - 'packetLoss' => is_numeric($this->result->packet_loss) ? $this->result->packet_loss : 'n/a', + 'packetLoss' => $result->packet_loss ? round($result->packet_loss, precision: 2) : '', 'speedtest_url' => $this->result->result_url, 'url' => url('/admin/results'), ], diff --git a/resources/views/apprise/speedtest-completed.blade.php b/resources/views/apprise/speedtest-completed.blade.php index 2efca5492..13a29162b 100644 --- a/resources/views/apprise/speedtest-completed.blade.php +++ b/resources/views/apprise/speedtest-completed.blade.php @@ -6,7 +6,9 @@ - **Ping:** {{ $ping }} - **Download:** {{ $download }} - **Upload:** {{ $upload }} +@filled($packetLoss) - **Packet Loss:** {{ $packetLoss }}% +@endfilled ### Links - [View Ookla Results]({{ $speedtest_url }}) diff --git a/resources/views/mail/speedtest/completed.blade.php b/resources/views/mail/speedtest/completed.blade.php index ac40c0e0f..cb0413e47 100644 --- a/resources/views/mail/speedtest/completed.blade.php +++ b/resources/views/mail/speedtest/completed.blade.php @@ -12,7 +12,9 @@ | Ping | {{ $ping }} | | Download | {{ $download }} | | Upload | {{ $upload }} | +@filled($packetLoss) | Packet Loss | {{ $packetLoss }} **%** | +@endfilled From 1ada95c4f9085530ee5aeff2a0c79fc389ddccb7 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 3 Feb 2026 15:21:29 +0100 Subject: [PATCH 63/63] bug: add server name and id to test wehbook (#2676) Co-authored-by: Alex Justesen --- .../Notifications/SendWebhookTestNotification.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/Actions/Notifications/SendWebhookTestNotification.php b/app/Actions/Notifications/SendWebhookTestNotification.php index e34dcce0a..7842c6ba7 100644 --- a/app/Actions/Notifications/SendWebhookTestNotification.php +++ b/app/Actions/Notifications/SendWebhookTestNotification.php @@ -2,6 +2,7 @@ namespace App\Actions\Notifications; +use App\Helpers\Number; use App\Models\Result; use App\Services\SpeedtestFakeResultGenerator; use Filament\Notifications\Notification; @@ -33,10 +34,12 @@ public function handle(array $webhooks) ->payload([ 'result_id' => Str::uuid(), 'site_name' => __('settings/notifications.test_notifications.webhook.payload'), + 'server_name' => $fakeResult->data['server']['name'], + 'server_id' => $fakeResult->data['server']['id'], 'isp' => $fakeResult->data['isp'], - 'ping' => $fakeResult->ping, - 'download' => $fakeResult->download, - 'upload' => $fakeResult->upload, + 'ping' => round($fakeResult->ping), + 'download' => Number::bitsToMagnitude(bits: $fakeResult->upload, precision: 0, magnitude: 'mbit'), + 'upload' => Number::bitsToMagnitude(bits: $fakeResult->download, precision: 0, magnitude: 'mbit'), 'packet_loss' => $fakeResult->data['packetLoss'], 'speedtest_url' => $fakeResult->data['result']['url'], 'url' => url('/admin/results'),