Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/Livewire/NextSpeedtestBanner.php
Original file line number Diff line number Diff line change
@@ -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 changes: 0 additions & 14 deletions app/Livewire/PlatformStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,12 @@

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
{
Expand Down
29 changes: 29 additions & 0 deletions app/Services/ScheduledSpeedtestService.php
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions resources/views/dashboard.blade.php
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions resources/views/filament/pages/dashboard.blade.php
Original file line number Diff line number Diff line change
@@ -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 />
Expand Down
17 changes: 17 additions & 0 deletions resources/views/livewire/next-speedtest-banner.blade.php
Original file line number Diff line number Diff line change
@@ -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>
26 changes: 4 additions & 22 deletions resources/views/livewire/platform-stats.blade.php
Original file line number Diff line number Diff line change
@@ -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') }}
Expand All @@ -23,41 +23,23 @@
</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>

<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>

<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>
Expand Down
67 changes: 67 additions & 0 deletions tests/Unit/Services/ScheduledSpeedtestServiceTest.php
Original file line number Diff line number Diff line change
@@ -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();
});