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]) 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') 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/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'), 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/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.', diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index b3d3762b7..e1b26abed 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,6 +1,10 @@
- + + + @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/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 a8a32bcc7..d6a18b12c 100644 --- a/resources/views/livewire/latest-result-stats.blade.php +++ b/resources/views/livewire/latest-result-stats.blade.php @@ -1,48 +1,28 @@
@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 +
+
+
+

+ + Latest result +

+ +

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

+
+ + @auth + + {{ __('general.view') }} + + @endauth
- +
@@ -57,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() }} @@ -94,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() }} @@ -131,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')) }} @@ -148,6 +134,17 @@ ms

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

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

+
@endfilled -
\ No newline at end of file +
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 c9ddfa9c9..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,15 +31,15 @@

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

- + - Total successful tests + Total completed tests

{{ $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(); +});