Show banner for next scheduled test (#2507)

Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com>
This commit is contained in:
Alex Justesen
2025-12-06 11:16:29 -05:00
committed by GitHub
parent 9a101363d5
commit a06b231eb6
8 changed files with 143 additions and 36 deletions
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Livewire;
use App\Services\ScheduledSpeedtestService;
use Carbon\Carbon;
use Livewire\Attributes\Computed;
use Livewire\Component;
class NextSpeedtestBanner extends Component
{
#[Computed]
public function nextSpeedtest(): ?Carbon
{
return ScheduledSpeedtestService::getNextScheduledTest();
}
public function render()
{
return view('livewire.next-speedtest-banner');
}
}
-14
View File
@@ -4,26 +4,12 @@ namespace App\Livewire;
use App\Enums\ResultStatus;
use App\Models\Result;
use Carbon\Carbon;
use Cron\CronExpression;
use Illuminate\Support\Number;
use Livewire\Attributes\Computed;
use Livewire\Component;
class PlatformStats extends Component
{
#[Computed]
public function nextSpeedtest(): ?Carbon
{
if ($schedule = config('speedtest.schedule')) {
$cronExpression = new CronExpression($schedule);
return Carbon::parse(time: $cronExpression->getNextRunDate(timeZone: config('app.display_timezone')));
}
return null;
}
#[Computed]
public function platformStats(): array
{
@@ -0,0 +1,29 @@
<?php
namespace App\Services;
use Carbon\Carbon;
use Cron\CronExpression;
class ScheduledSpeedtestService
{
/**
* Assess if there are scheduled speedtests and return the next scheduled time.
*
* @return Carbon|null Returns null if no tests are scheduled, or Carbon instance with next scheduled test
*/
public static function getNextScheduledTest(): ?Carbon
{
$schedule = config('speedtest.schedule');
if (blank($schedule) || $schedule === false) {
return null;
}
$cronExpression = new CronExpression($schedule);
return Carbon::parse(
time: $cronExpression->getNextRunDate(timeZone: config('app.display_timezone'))
);
}
}
+2
View File
@@ -1,5 +1,7 @@
<x-app-layout title="Dashboard">
<div class="space-y-6 md:space-y-12 dashboard-page">
<livewire:next-speedtest-banner />
@auth
<livewire:platform-stats />
@endauth
@@ -1,5 +1,7 @@
<x-filament-panels::page class="dashboard-page">
<div class="space-y-6 md:space-y-12">
<livewire:next-speedtest-banner />
<livewire:platform-stats />
<livewire:latest-result-stats />
@@ -0,0 +1,17 @@
<div wire:poll.60s>
@if ($this->nextSpeedtest)
<div class="rounded-md bg-blue-50 dark:bg-blue-500/10 p-4 outline outline-blue-500/20">
<div class="flex">
<div class="shrink-0">
<x-tabler-info-circle class="size-5 text-blue-400" />
</div>
<div class="ml-3 flex-1">
<p class="text-sm text-blue-700 dark:text-blue-300">
Next scheduled test at <span class="font-medium">{{ $this->nextSpeedtest->timezone(config('app.display_timezone'))->format('F jS, Y, g:i a') }}</span>.
</p>
</div>
</div>
</div>
@endif
</div>
@@ -1,5 +1,5 @@
<div wire:poll.60s>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<h2 class="flex items-center gap-x-2 text-base md:text-lg font-semibold text-zinc-900 dark:text-zinc-100 col-span-full">
<x-tabler-chart-bar class="size-5" />
{{ __('general.statistics') }}
@@ -23,25 +23,7 @@
</div>
</x-filament::section> --}}
@filled($this->nextSpeedtest)
<x-filament::section class="col-span-1">
<x-slot name="heading">
Next Speedtest in
</x-slot>
<p class="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100" title="{{ $this->nextSpeedtest->format('F jS, Y g:i A') }}">{{ $this->nextSpeedtest->diffForHumans() }}</p>
</x-filament::section>
@else
<x-filament::section class="col-span-1 bg-zinc-100 shadow-none">
<x-slot name="heading">
Next Speedtest in
</x-slot>
<p class="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">No scheduled speedtests</p>
</x-filament::section>
@endfilled
<x-filament::section class="col-span-1">
<x-filament::section class="col-span-1" icon="tabler-hash">
<x-slot name="heading">
Total tests
</x-slot>
@@ -49,7 +31,7 @@
<p class="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">{{ $this->platformStats['total'] }}</p>
</x-filament::section>
<x-filament::section class="col-span-1">
<x-filament::section class="col-span-1" icon="tabler-circle-check">
<x-slot name="heading">
Total completed tests
</x-slot>
@@ -57,7 +39,7 @@
<p class="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">{{ $this->platformStats['completed'] }}</p>
</x-filament::section>
<x-filament::section class="col-span-1">
<x-filament::section class="col-span-1" icon="tabler-alert-circle">
<x-slot name="heading">
Total failed tests
</x-slot>
@@ -0,0 +1,67 @@
<?php
use App\Services\ScheduledSpeedtestService;
use Carbon\Carbon;
test('returns null when schedule config is null', function () {
config()->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();
});