diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..7ec51536b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// https://aka.ms/devcontainer.json +{ + "name": "Existing Docker Compose (Extend)", + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "service": "laravel.test", + "workspaceFolder": "/var/www/html", + "customizations": { + "vscode": { + "extensions": [ + "aaron-bond.better-comments", + "austenc.laravel-blade-spacer", + "bmewburn.vscode-intelephense-client", + "laravel.vscode-laravel", + "mikestead.dotenv", + "streetsidesoftware.code-spell-checker" + ], + "settings": {} + } + }, + "remoteUser": "sail", + "postCreateCommand": "chown -R 1000:1000 /var/www/html 2>/dev/null || true" + // "forwardPorts": [], + // "runServices": [], + // "shutdownAction": "none", +} diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/inactivity-actions.yml similarity index 66% rename from .github/workflows/stale_issues.yml rename to .github/workflows/inactivity-actions.yml index f276873f9..ed80d8b91 100644 --- a/.github/workflows/stale_issues.yml +++ b/.github/workflows/inactivity-actions.yml @@ -1,24 +1,38 @@ -name: 'Close stale issues' +name: 'Issue and PR Maintenance' on: schedule: - - cron: '0 10 * * *' # Runs daily at 10:00 AM UTC + - cron: '0 0 * * *' # runs at midnight UTC workflow_dispatch: permissions: issues: write + pull-requests: write jobs: - manage-stale-issues: + lock-inactive: + name: Lock Inactive Issues + runs-on: ubuntu-24.04 + steps: + - uses: klaasnicolaas/action-inactivity-lock@v1.1.3 + id: lock + with: + days-inactive-issues: 14 + lock-reason-issues: "" + # Action can not skip PRs, set it to 100 years to cover it. + days-inactive-prs: 36524 + lock-reason-prs: "" + + close-stale: + name: Close Stale Issues runs-on: ubuntu-24.04 - steps: - name: Close Stale Issues uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - # Stale messaging + # Messaging stale-issue-message: > 👋 This issue has been automatically marked as stale due to inactivity. If this issue is still relevant, please comment to keep it open. @@ -32,17 +46,15 @@ jobs: days-before-issue-stale: 14 days-before-issue-close: 7 - # Label management + # Labels stale-issue-label: 'stale' remove-stale-when-updated: true - - # Targeting only `question`-labeled issues only-issue-labels: 'question' exempt-issue-labels: > bug, chore, confirmed, dependencies, help wanted, documentation, duplicate, feature, good first issue, needs review, wontfix - # Skip assigned or milestone-tracked issues + # Exemptions exempt-assignees: true exempt-milestones: true diff --git a/app/Actions/Influxdb/v2/BuildPointData.php b/app/Actions/Influxdb/v2/BuildPointData.php index f01991fe4..40a13e81d 100644 --- a/app/Actions/Influxdb/v2/BuildPointData.php +++ b/app/Actions/Influxdb/v2/BuildPointData.php @@ -39,6 +39,8 @@ public function handle(Result $result): Point ->addField('ping', Number::castToType($result->ping, 'float')) ->addField('download_bits', ! blank($result->download) ? Number::castToType(Bitrate::bytesToBits($result->download), 'int') : null) ->addField('upload_bits', ! blank($result->upload) ? Number::castToType(Bitrate::bytesToBits($result->upload), 'int') : null) + ->addField('download_elapsed', Number::castToType(Arr::get($result->data, 'download.elapsed'), 'float')) + ->addField('upload_elapsed', Number::castToType(Arr::get($result->data, 'upload.elapsed'), 'float')) ->addField('download_jitter', Number::castToType(Arr::get($result->data, 'download.latency.jitter'), 'float')) ->addField('upload_jitter', Number::castToType(Arr::get($result->data, 'upload.latency.jitter'), 'float')) ->addField('ping_jitter', Number::castToType(Arr::get($result->data, 'ping.jitter'), 'float')) @@ -48,6 +50,8 @@ public function handle(Result $result): Point ->addField('upload_latency_avg', Number::castToType(Arr::get($result->data, 'upload.latency.iqm'), 'float')) ->addField('upload_latency_high', Number::castToType(Arr::get($result->data, 'upload.latency.high'), 'float')) ->addField('upload_latency_low', Number::castToType(Arr::get($result->data, 'upload.latency.low'), 'float')) + ->addField('downloaded_bytes', Number::castToType($result->data, 'downloaded_bytes', 'int')) + ->addField('uploaded_bytes', Number::castToType($result->data, 'uploaded_bytes', 'int')) ->addField('packet_loss', Number::castToType(Arr::get($result->data, 'packetLoss'), 'float')) ->addField('log_message', Arr::get($result->data, 'message')); diff --git a/app/Filament/Exports/ResultExporter.php b/app/Filament/Exports/ResultExporter.php index ff4438586..15ff08118 100644 --- a/app/Filament/Exports/ResultExporter.php +++ b/app/Filament/Exports/ResultExporter.php @@ -21,136 +21,59 @@ public function getFormats(): array public static function getColumns(): array { - return [ - ExportColumn::make('id') - ->label('ID'), - ExportColumn::make('ip_address') - ->label('IP address') - ->state(function (Result $record): ?string { - return $record->ip_address; - }) - ->enabledByDefault(false), - ExportColumn::make('isp') - ->label('ISP') - ->state(function (Result $record): ?string { - return $record->isp; - }) - ->enabledByDefault(false), - ExportColumn::make('server_location') - ->label('Server Location') - ->state(function (Result $record): ?string { - return $record->server_location; - }) - ->enabledByDefault(false), - ExportColumn::make('service') - ->state(function (Result $record) { - return $record->service->getLabel(); - }), - ExportColumn::make('server_id') - ->label('Server ID') - ->state(function (Result $record): ?string { - return $record->server_id; - }) - ->enabledByDefault(false), - ExportColumn::make('server_name') - ->state(function (Result $record): ?string { - return $record->server_name; - }) - ->enabledByDefault(false), - - ExportColumn::make('download') - ->state(function (Result $record): ?string { - return $record->download_bits; - }), - ExportColumn::make('upload') - ->state(function (Result $record): ?string { - return $record->upload_bits; - }), - ExportColumn::make('ping'), - ExportColumn::make('packet_loss'), - ExportColumn::make('download_jitter') - ->state(function (Result $record): ?string { - return $record->download_jitter; - }) - ->enabledByDefault(false), - ExportColumn::make('upload_jitter') - ->state(function (Result $record): ?string { - return $record->upload_jitter; - }) - ->enabledByDefault(false), - ExportColumn::make('ping_jitter') - ->state(function (Result $record): ?string { - return $record->ping_jitter; - }) - ->enabledByDefault(false), - ExportColumn::make('ping_low') - ->state(function (Result $record): ?string { - return $record->ping_low; - }) - ->enabledByDefault(false), - ExportColumn::make('ping_high') - ->state(function (Result $record): ?string { - return $record->ping_high; - }) - ->enabledByDefault(false), - ExportColumn::make('upload_latency_high') - ->state(function (Result $record): ?string { - return $record->upload_latency_high; - }) - ->enabledByDefault(false), - ExportColumn::make('upload_latency_low') - ->state(function (Result $record): ?string { - return $record->upload_latency_low; - }) - ->enabledByDefault(false), - ExportColumn::make('upload_latency_avg') - ->state(function (Result $record): ?string { - return $record->upload_latency_iqm; - }) - ->enabledByDefault(false), - ExportColumn::make('download_latency_high') - ->state(function (Result $record): ?string { - return $record->download_latency_high; - }) - ->enabledByDefault(false), - ExportColumn::make('download_latency_low') - ->state(function (Result $record): ?string { - return $record->download_latency_low; - }) - ->enabledByDefault(false), - ExportColumn::make('download_latency_avg') - ->state(function (Result $record): ?string { - return $record->download_latency_iqm; - }) - ->enabledByDefault(false), - ExportColumn::make('result_url') - ->state(function (Result $record) { - return $record->result_url; - }), - ExportColumn::make('error_message') - ->state(function (Result $record) { - return $record->error_message; - }) - ->enabledByDefault(false), - ExportColumn::make('comments') - ->enabledByDefault(false), - ExportColumn::make('status') - ->state(function (Result $record) { - return $record->status->getLabel(); - }), - ExportColumn::make('scheduled') - ->state(function (Result $record): string { - return $record->scheduled ? 'Yes' : 'No'; - }), - ExportColumn::make('healthy') - ->state(function (Result $record): string { - return $record->healthy ? 'Yes' : 'No'; - }) - ->enabledByDefault(false), + $columns = [ + ExportColumn::make('id')->label('ID'), + ExportColumn::make('service')->state(fn (Result $r) => $r->service->getLabel()), + ExportColumn::make('status')->state(fn (Result $r) => $r->status->getLabel()), + ExportColumn::make('scheduled')->state(fn (Result $r) => $r->scheduled ? 'Yes' : 'No'), + ExportColumn::make('healthy')->state(fn (Result $r) => $r->healthy ? 'Yes' : 'No'), ExportColumn::make('created_at'), - ExportColumn::make('updated_at') - ->enabledByDefault(false), + ExportColumn::make('updated_at'), + ExportColumn::make('comments'), ]; + + $columns = array_merge($columns, self::generateDataColumns()); + + return $columns; + } + + protected static function generateDataColumns(): array + { + + $sample = Result::query()->whereNotNull('data')->first()?->data ?? []; + + $flattened = self::flatten($sample); + + $columns = []; + + foreach ($flattened as $key => $default) { + $columns[] = ExportColumn::make($key) + ->label(str_replace('_', ' ', ucfirst($key))) + ->state(function (Result $r) use ($key) { + $flattened = self::flatten($r->data ?? []); + + return $flattened[$key] ?? null; + }); + } + + return $columns; + } + + protected static function flatten(array $array, string $prefix = ''): array + { + $result = []; + + foreach ($array as $key => $value) { + $newKey = $prefix ? "{$prefix}_{$key}" : $key; + + if (is_array($value)) { + $result += self::flatten($value, $newKey); + } else { + $result[$newKey] = $value; + } + } + + return $result; } public static function getCompletedNotificationBody(Export $export): string diff --git a/app/Filament/Resources/ResultResource.php b/app/Filament/Resources/ResultResource.php index f47681567..6197d0b67 100644 --- a/app/Filament/Resources/ResultResource.php +++ b/app/Filament/Resources/ResultResource.php @@ -11,6 +11,7 @@ use Carbon\Carbon; use Filament\Forms; use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Section; @@ -27,6 +28,7 @@ use Filament\Tables\Actions\ViewAction; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\Filter; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Table; @@ -357,6 +359,22 @@ public static function table(Table $table): Table ->alignment(Alignment::End), ]) ->filters([ + Filter::make('created_at') + ->form([ + DatePicker::make('created_from'), + DatePicker::make('created_until'), + ]) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['created_from'], + fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date), + ) + ->when( + $data['created_until'], + fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date), + ); + }), SelectFilter::make('ip_address') ->label('IP address') ->multiple() @@ -446,6 +464,9 @@ public static function table(Table $table): Table ->headerActions([ ExportAction::make() ->exporter(ResultExporter::class) + ->columnMapping(false) + ->modalHeading('Export all Results') + ->modalDescription('This will export all columns for all results.') ->fileName(fn (): string => 'results-'.now()->timestamp), ActionGroup::make([ Action::make('truncate') diff --git a/app/Models/Traits/ResultDataAttributes.php b/app/Models/Traits/ResultDataAttributes.php index 805ca9d29..0e38bc2af 100644 --- a/app/Models/Traits/ResultDataAttributes.php +++ b/app/Models/Traits/ResultDataAttributes.php @@ -248,6 +248,26 @@ protected function uploadElapsed(): Attribute ); } + /** + * Get the result's uploaded bytes. + */ + public function uploadedBytes(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'upload.bytes'), + ); + } + + /** + * Get the result's downloaded bytes. + */ + public function downloadedBytes(): Attribute + { + return Attribute::make( + get: fn () => Arr::get($this->data, 'download.bytes'), + ); + } + /** * Get the result's server port. */ diff --git a/config/speedtest.php b/config/speedtest.php index 1aee5b60c..2703f4020 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -7,9 +7,9 @@ * General settings. */ - 'build_date' => Carbon::parse('2025-06-12'), + 'build_date' => Carbon::parse('2025-07-07'), - 'build_version' => 'v1.6.1', + 'build_version' => 'v1.6.2', 'content_width' => env('CONTENT_WIDTH', '7xl'), diff --git a/docker/8.3/Dockerfile b/docker/8.3/Dockerfile index 9ddcba2b9..1e8330947 100644 --- a/docker/8.3/Dockerfile +++ b/docker/8.3/Dockerfile @@ -6,6 +6,7 @@ ARG WWWGROUP ARG NODE_VERSION=22 ARG MYSQL_CLIENT="mysql-client" ARG POSTGRES_VERSION=17 +ARG SPEEDTEST_VERSION=1.2.0 WORKDIR /var/www/html @@ -45,8 +46,9 @@ RUN apt-get update && apt-get upgrade -y \ && apt-get update \ && apt-get install -y $MYSQL_CLIENT \ && apt-get install -y postgresql-client-$POSTGRES_VERSION \ - && curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash \ - && apt-get install -y speedtest-cli \ + && curl -o /tmp/speedtest-cli.tgz -L \ + "https://install.speedtest.net/app/cli/ookla-speedtest-$SPEEDTEST_VERSION-linux-x86_64.tgz" \ + && tar -xzf /tmp/speedtest-cli.tgz -C /usr/bin \ && apt-get -y autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*