diff --git a/app/Filament/Exports/ResultExporter.php b/app/Filament/Exports/ResultExporter.php index 5acc680f5..7fc33a367 100644 --- a/app/Filament/Exports/ResultExporter.php +++ b/app/Filament/Exports/ResultExporter.php @@ -64,6 +64,30 @@ public static function getColumns(): array ->state(function (Result $record): ?string { return $record->ping_jitter; }), + ExportColumn::make('upload_latency_high') + ->state(function (Result $record): ?string { + return $record->upload_latency_high; + }), + ExportColumn::make('upload_latency_low') + ->state(function (Result $record): ?string { + return $record->upload_latency_low; + }), + ExportColumn::make('upload_latency_avg') + ->state(function (Result $record): ?string { + return $record->upload_latency_iqm; + }), + ExportColumn::make('download_latency_high') + ->state(function (Result $record): ?string { + return $record->download_latency_high; + }), + ExportColumn::make('download_latency_low') + ->state(function (Result $record): ?string { + return $record->download_latency_low; + }), + ExportColumn::make('download_latency_avg') + ->state(function (Result $record): ?string { + return $record->download_latency_iqm; + }), ExportColumn::make('comments') ->enabledByDefault(false), // ExportColumn::make('status'), // TODO: enable status when upgrading to PHP v8.3: https://php.watch/versions/8.3/dynamic-class-const-enum-member-syntax-support diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index 5b5b43b66..36f5def65 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -4,9 +4,11 @@ use App\Actions\Speedtests\RunOoklaSpeedtest; 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 App\Settings\GeneralSettings; use Filament\Actions\Action; @@ -67,6 +69,8 @@ protected function getHeaderWidgets(): array RecentUploadChartWidget::make(), RecentPingChartWidget::make(), RecentJitterChartWidget::make(), + RecentUploadLatencyChartWidget::make(), + RecentDownloadLatencyChartWidget::make(), ]; } } diff --git a/app/Filament/Resources/ResultResource.php b/app/Filament/Resources/ResultResource.php index 14101f699..a80cd87b7 100644 --- a/app/Filament/Resources/ResultResource.php +++ b/app/Filament/Resources/ResultResource.php @@ -68,8 +68,20 @@ public static function form(Form $form): Form ->label('Ping (ms)'), Forms\Components\TextInput::make('data.download.latency.jitter') ->label('Download Jitter (ms)'), + Forms\Components\TextInput::make('data.download.latency.high') + ->label('Download Latency High'), + Forms\Components\TextInput::make('data.download.latency.low') + ->label('Download Latency low'), + Forms\Components\TextInput::make('data.download.latency.iqm') + ->label('Download Latency iqm'), Forms\Components\TextInput::make('data.upload.latency.jitter') ->label('Upload Jitter (ms)'), + Forms\Components\TextInput::make('data.upload.latency.high') + ->label('Upload Latency High'), + Forms\Components\TextInput::make('data.upload.latency.low') + ->label('Upload Latency low'), + Forms\Components\TextInput::make('data.upload.latency.iqm') + ->label('Upload Latency iqm'), Forms\Components\TextInput::make('data.ping.jitter') ->label('Ping Jitter (ms)'), Forms\Components\TextInput::make('data.packetLoss') @@ -151,12 +163,48 @@ public static function table(Table $table): Table ->sortable(query: function (Builder $query, string $direction): Builder { return $query->orderBy('data->download->latency->jitter', $direction); }), + Tables\Columns\TextColumn::make('download_latency_high') + ->toggleable() + ->toggledHiddenByDefault() + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->orderBy('data->download->latency->high', $direction); + }), + Tables\Columns\TextColumn::make('download_latency_low') + ->toggleable() + ->toggledHiddenByDefault() + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->orderBy('data->download->latency->low', $direction); + }), + Tables\Columns\TextColumn::make('download_latency_iqm') + ->toggleable() + ->toggledHiddenByDefault() + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->orderBy('data->download->latency->iqm', $direction); + }), Tables\Columns\TextColumn::make('upload_jitter') ->toggleable() ->toggledHiddenByDefault() ->sortable(query: function (Builder $query, string $direction): Builder { return $query->orderBy('data->upload->latency->jitter', $direction); }), + Tables\Columns\TextColumn::make('upload_latency_high') + ->toggleable() + ->toggledHiddenByDefault() + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->orderBy('data->upload->latency->high', $direction); + }), + Tables\Columns\TextColumn::make('upload_latency_low') + ->toggleable() + ->toggledHiddenByDefault() + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->orderBy('data->upload->latency->low', $direction); + }), + Tables\Columns\TextColumn::make('upload_latency_iqm') + ->toggleable() + ->toggledHiddenByDefault() + ->sortable(query: function (Builder $query, string $direction): Builder { + return $query->orderBy('data->upload->latency->iqm', $direction); + }), Tables\Columns\TextColumn::make('ping_jitter') ->toggleable() ->toggledHiddenByDefault() diff --git a/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php b/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php new file mode 100644 index 000000000..40c6ee57b --- /dev/null +++ b/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php @@ -0,0 +1,103 @@ + 'Last 24h', + 'week' => 'Last week', + 'month' => 'Last month', + ]; + } + + protected function getData(): array + { + $settings = new GeneralSettings(); + + $results = Result::query() + ->select(['id', 'data', 'created_at']) + ->where('status', '=', ResultStatus::Completed) + ->when($this->filter == '24h', function ($query) { + $query->where('created_at', '>=', now()->subDay()); + }) + ->when($this->filter == 'week', function ($query) { + $query->where('created_at', '>=', now()->subWeek()); + }) + ->when($this->filter == 'month', function ($query) { + $query->where('created_at', '>=', now()->subMonth()); + }) + ->orderBy('created_at') + ->get(); + + return [ + 'datasets' => [ + [ + 'label' => 'Average (ms)', + 'data' => $results->map(fn ($item) => $item->download_latency_iqm ? number_format($item->pdownload_latency_iqm, 2) : 0), + 'borderColor' => '#10b981', + 'backgroundColor' => '#10b981', + 'fill' => false, + 'cubicInterpolationMode' => 'monotone', + 'tension' => 0.4, + ], + [ + 'label' => 'High (ms)', + 'data' => $results->map(fn ($item) => $item->download_latency_high ? number_format($item->download_latency_high, 2) : 0), + 'borderColor' => '#0ea5e9', + 'backgroundColor' => '#0ea5e9', + 'fill' => false, + 'cubicInterpolationMode' => 'monotone', + 'tension' => 0.4, + ], + [ + 'label' => 'Low (ms)', + 'data' => $results->map(fn ($item) => $item->download_latency_low ? number_format($item->download_latency_low, 2) : 0), + 'borderColor' => '#8b5cf6', + 'backgroundColor' => '#8b5cf6', + 'fill' => false, + 'cubicInterpolationMode' => 'monotone', + 'tension' => 0.4, + ], + ], + 'labels' => $results->map(fn ($item) => $item->created_at->timezone(TimeZoneHelper::displayTimeZone($settings))->format('M d - G:i')), + ]; + } + + protected function getOptions(): array + { + return [ + 'scales' => [ + 'y' => [ + 'beginAtZero' => true, + ], + ], + ]; + } + + protected function getType(): string + { + return 'line'; + } +} diff --git a/app/Filament/Widgets/RecentUploadLatencyChartWidget.php b/app/Filament/Widgets/RecentUploadLatencyChartWidget.php new file mode 100644 index 000000000..6128e34ba --- /dev/null +++ b/app/Filament/Widgets/RecentUploadLatencyChartWidget.php @@ -0,0 +1,103 @@ + 'Last 24h', + 'week' => 'Last week', + 'month' => 'Last month', + ]; + } + + protected function getData(): array + { + $settings = new GeneralSettings(); + + $results = Result::query() + ->select(['id', 'data', 'created_at']) + ->where('status', '=', ResultStatus::Completed) + ->when($this->filter == '24h', function ($query) { + $query->where('created_at', '>=', now()->subDay()); + }) + ->when($this->filter == 'week', function ($query) { + $query->where('created_at', '>=', now()->subWeek()); + }) + ->when($this->filter == 'month', function ($query) { + $query->where('created_at', '>=', now()->subMonth()); + }) + ->orderBy('created_at') + ->get(); + + return [ + 'datasets' => [ + [ + 'label' => 'Average (ms)', + 'data' => $results->map(fn ($item) => $item->upload_latency_iqm ? number_format($item->upload_latency_iqm, 2) : 0), + 'borderColor' => '#10b981', + 'backgroundColor' => '#10b981', + 'fill' => false, + 'cubicInterpolationMode' => 'monotone', + 'tension' => 0.4, + ], + [ + 'label' => 'High (ms)', + 'data' => $results->map(fn ($item) => $item->upload_latency_high ? number_format($item->upload_latency_high, 2) : 0), + 'borderColor' => '#0ea5e9', + 'backgroundColor' => '#0ea5e9', + 'fill' => false, + 'cubicInterpolationMode' => 'monotone', + 'tension' => 0.4, + ], + [ + 'label' => 'Low (ms)', + 'data' => $results->map(fn ($item) => $item->upload_latency_low ? number_format($item->upload_latency_low, 2) : 0), + 'borderColor' => '#8b5cf6', + 'backgroundColor' => '#8b5cf6', + 'fill' => false, + 'cubicInterpolationMode' => 'monotone', + 'tension' => 0.4, + ], + ], + 'labels' => $results->map(fn ($item) => $item->created_at->timezone(TimeZoneHelper::displayTimeZone($settings))->format('M d - G:i')), + ]; + } + + protected function getOptions(): array + { + return [ + 'scales' => [ + 'y' => [ + 'beginAtZero' => true, + ], + ], + ]; + } + + protected function getType(): string + { + return 'line'; + } +} diff --git a/app/Models/Result.php b/app/Models/Result.php index 7a913a949..6dcfe12f4 100644 --- a/app/Models/Result.php +++ b/app/Models/Result.php @@ -60,6 +60,12 @@ public function formatForInfluxDB2() 'ping_jitter' => $this->ping_jitter, 'download_jitter' => $this->download_jitter, 'upload_jitter' => $this->upload_jitter, + 'download_latency_avg' => $this->download_latency_iqm, + 'download_latency_low' => $this->download_latency_low, + 'download_latency_high' => $this->download_latency_high, + 'upload_latency_avg' => $this->upload_latency_iqm, + 'upload_latency_low' => $this->upload_latency_low, + 'upload_latency_high' => $this->upload_latency_high, 'server_id' => $this?->server_id, 'server_host' => $this?->server_host, 'server_name' => $this?->server_name, @@ -101,6 +107,36 @@ protected function downloadJitter(): Attribute ); } + /** + * Get the result's download latency high in milliseconds. + */ + protected function downloadlatencyHigh(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'download.latency.high'), + ); + } + + /** + * Get the result's download latency low in milliseconds. + */ + protected function downloadlatencyLow(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'download.latency.low'), + ); + } + + /** + * Get the result's download latency iqm in milliseconds. + */ + protected function downloadlatencyiqm(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'download.latency.iqm'), + ); + } + /** * Get the result's download jitter in milliseconds. */ @@ -212,4 +248,34 @@ protected function uploadJitter(): Attribute get: fn () => Arr::get($this->data, 'upload.latency.jitter'), ); } + + /** + * Get the result's upload latency high in milliseconds. + */ + protected function uploadlatencyHigh(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'upload.latency.high'), + ); + } + + /** + * Get the result's upload latency low in milliseconds. + */ + protected function uploadlatencyLow(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'upload.latency.low'), + ); + } + + /** + * Get the result's upload latency iqm in milliseconds. + */ + protected function uploadlatencyiqm(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'upload.latency.iqm'), + ); + } }