diff --git a/app/Filament/Resources/Results/Tables/ResultTable.php b/app/Filament/Resources/Results/Tables/ResultTable.php index 7c1f4765e..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; @@ -102,26 +101,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 +111,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() @@ -175,8 +134,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')) @@ -293,30 +250,15 @@ 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]) - ->deferLoading() ->poll('60s'); } } 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/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/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 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/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')) }} 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(); +});