From a2c23d74efca52ce7ca2f34a9a92f21e8a102be5 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Mon, 1 Dec 2025 00:07:16 +0100 Subject: [PATCH 1/7] chore: remove duplicate translation strings (#2470) --- app/Filament/Pages/Settings/Notification.php | 12 ++++++------ lang/en/settings/notifications.php | 10 ++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 6e06ca495..915225265 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -83,10 +83,10 @@ public function form(Schema $schema): Schema ->columns(1) ->schema([ Checkbox::make('database_on_speedtest_run') - ->label(__('settings/notifications.database_on_speedtest_run')), + ->label(__('settings/notifications.notify_on_every_speedtest_run')), Checkbox::make('database_on_threshold_failure') - ->label(__('settings/notifications.database_on_threshold_failure')), + ->label(__('settings/notifications.notify_on_threshold_failures')), ]), Actions::make([ @@ -115,10 +115,10 @@ public function form(Schema $schema): Schema ->columns(1) ->schema([ Checkbox::make('mail_on_speedtest_run') - ->label(__('settings/notifications.mail_on_speedtest_run')), + ->label(__('settings/notifications.notify_on_every_speedtest_run')), Checkbox::make('mail_on_threshold_failure') - ->label(__('settings/notifications.mail_on_threshold_failure')), + ->label(__('settings/notifications.notify_on_threshold_failures')), ]), Repeater::make('mail_recipients') @@ -173,10 +173,10 @@ public function form(Schema $schema): Schema ->columns(1) ->schema([ Checkbox::make('webhook_on_speedtest_run') - ->label(__('settings/notifications.webhook_on_speedtest_run')), + ->label(__('settings/notifications.notify_on_every_speedtest_run')), Checkbox::make('webhook_on_threshold_failure') - ->label(__('settings/notifications.webhook_on_threshold_failure')), + ->label(__('settings/notifications.notify_on_threshold_failures')), ]), Repeater::make('webhook_urls') diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 8a10d3d23..ddc1baae8 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -7,28 +7,22 @@ // Database notifications 'database' => 'Database', 'database_description' => 'Notifications sent to this channel will show up under the 🔔 icon in the header.', - 'database_on_speedtest_run' => 'Notify on every speedtest run', - 'database_on_threshold_failure' => 'Notify on threshold failures', 'test_database_channel' => 'Test database channel', // Mail notifications 'mail' => 'Mail', 'recipients' => 'Recipients', - 'mail_on_speedtest_run' => 'Notify on every speedtest run', - 'mail_on_threshold_failure' => 'Notify on threshold failures', 'test_mail_channel' => 'Test mail channel', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', - 'webhook_on_speedtest_run' => 'Notify on every speedtest run', - 'webhook_on_threshold_failure' => 'Notify on threshold failures', 'test_webhook_channel' => 'Test webhook channel', 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation.', // Common notification messages - 'notify_on_every_speedtest_run' => 'Notify on every speedtest run', - 'notify_on_threshold_failures' => 'Notify on threshold failures', + 'notify_on_every_speedtest_run' => 'Notify on every scheduled speedtest run', + 'notify_on_threshold_failures' => 'Notify on threshold failures for scheduled speedtests', // Test notification messages 'test_notifications' => [ From 50fd68173d3340b8cc23fb2326dba41e12ae004a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:22:58 -0500 Subject: [PATCH 2/7] gh actions: bump actions/checkout from 5 to 6 (#2471) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/validate-openapi.yml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16f5552d3..abe74743b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -49,7 +49,7 @@ jobs: options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -107,7 +107,7 @@ jobs: options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -165,7 +165,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -223,7 +223,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -281,7 +281,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -339,7 +339,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -397,7 +397,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -446,7 +446,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/validate-openapi.yml b/.github/workflows/validate-openapi.yml index 5465145ea..45226a32c 100644 --- a/.github/workflows/validate-openapi.yml +++ b/.github/workflows/validate-openapi.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout PR branch - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} From 727ee6d9d299f843fd2b836c89285777f55cbc73 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Tue, 2 Dec 2025 11:55:01 -0500 Subject: [PATCH 3/7] Add GitHub Actions workflow to trigger Docker image build (#2474) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- .github/workflows/cd.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..73817d100 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,27 @@ +name: Trigger Docker Image Build + +on: + release: + types: [published] + +jobs: + trigger-docker-build: + runs-on: ubuntu-24.04 + + steps: + - name: Generate GitHub App token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: alexjustesen + repositories: docker-speedtest-tracker + + - name: Trigger docker-speedtest-tracker build + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ steps.generate_token.outputs.token }} + repository: alexjustesen/docker-speedtest-tracker + event-type: speedtest-tracker-release + client-payload: '{"tag_name": "${{ github.event.release.tag_name }}"}' From d27000f05fa6af314fd6218946fb12cfda2b3bf7 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 2 Dec 2025 22:31:40 +0100 Subject: [PATCH 4/7] feat: Add Prometheus (#2440) Co-authored-by: Alex Justesen --- .../Pages/Settings/DataIntegration.php | 36 ++- app/Http/Controllers/MetricsController.php | 28 ++ .../PrometheusAllowedIpMiddleware.php | 43 +++ .../ProcessSpeedtestDataIntegrations.php | 5 + app/Services/PrometheusMetricsService.php | 249 ++++++++++++++++++ app/Settings/DataIntegrationSettings.php | 4 + composer.json | 1 + composer.lock | 68 +++++ ...1_25_191005_create_prometheus_settings.php | 12 + lang/en/settings/data_integration.php | 7 + routes/web.php | 6 + tests/Feature/MetricsEndpointTest.php | 126 +++++++++ 12 files changed, 577 insertions(+), 8 deletions(-) create mode 100644 app/Http/Controllers/MetricsController.php create mode 100644 app/Http/Middleware/PrometheusAllowedIpMiddleware.php create mode 100644 app/Services/PrometheusMetricsService.php create mode 100644 database/settings/2025_11_25_191005_create_prometheus_settings.php create mode 100644 tests/Feature/MetricsEndpointTest.php diff --git a/app/Filament/Pages/Settings/DataIntegration.php b/app/Filament/Pages/Settings/DataIntegration.php index b3d4dec33..5930293d8 100644 --- a/app/Filament/Pages/Settings/DataIntegration.php +++ b/app/Filament/Pages/Settings/DataIntegration.php @@ -7,15 +7,18 @@ use App\Settings\DataIntegrationSettings; use Filament\Actions\Action; use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Pages\SettingsPage; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Grid; -use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; +use Filament\Support\Icons\Heroicon; use Illuminate\Support\Facades\Auth; class DataIntegration extends SettingsPage @@ -52,16 +55,14 @@ public function form(Schema $schema): Schema { return $schema ->components([ - Grid::make([ - 'default' => 1, - 'md' => 3, - ]) + Tabs::make() ->schema([ - Section::make(__('settings/data_integration.influxdb_v2')) - ->description(__('settings/data_integration.influxdb_v2_description')) + Tab::make(__('settings/data_integration.influxdb_v2')) + ->icon(Heroicon::OutlinedCircleStack) ->schema([ Toggle::make('influxdb_v2_enabled') ->label(__('settings/data_integration.influxdb_v2_enabled')) + ->helpertext(__('settings/data_integration.influxdb_v2_description')) ->reactive() ->columnSpanFull(), Grid::make(['default' => 1, 'md' => 3]) @@ -127,7 +128,26 @@ public function form(Schema $schema): Schema ]), ]), ]) - ->compact() + ->columnSpanFull(), + Tab::make(__('settings/data_integration.prometheus')) + ->icon(Heroicon::OutlinedChartBar) + ->schema([ + Toggle::make('prometheus_enabled') + ->label(__('settings/data_integration.prometheus_enabled')) + ->helperText(__('settings/data_integration.influxdb_v2_description')) + ->reactive() + ->columnSpanFull(), + Grid::make(['default' => 1, 'md' => 3]) + ->hidden(fn (Get $get) => $get('prometheus_enabled') !== true) + ->schema([ + TagsInput::make('prometheus_allowed_ips') + ->label(__('settings/data_integration.prometheus_allowed_ips')) + ->helperText(__('settings/data_integration.prometheus_allowed_ips_helper')) + ->placeholder('192.168.1.100') + ->splitKeys(['Tab', ',', ' ']) + ->columnSpanFull(), + ]), + ]) ->columnSpanFull(), ]) ->columnSpanFull(), diff --git a/app/Http/Controllers/MetricsController.php b/app/Http/Controllers/MetricsController.php new file mode 100644 index 000000000..984be20d8 --- /dev/null +++ b/app/Http/Controllers/MetricsController.php @@ -0,0 +1,28 @@ +settings->prometheus_enabled) { + abort(404); + } + + $metrics = $this->metricsService->generateMetrics(); + + return response($metrics, 200, [ + 'Content-Type' => 'text/plain; version=0.0.4; charset=utf-8', + ]); + } +} diff --git a/app/Http/Middleware/PrometheusAllowedIpMiddleware.php b/app/Http/Middleware/PrometheusAllowedIpMiddleware.php new file mode 100644 index 000000000..c599b8a6f --- /dev/null +++ b/app/Http/Middleware/PrometheusAllowedIpMiddleware.php @@ -0,0 +1,43 @@ +settings->prometheus_allowed_ips)) { + return $next($request); + } + + $clientIp = $request->ip(); + $allowedIps = $this->settings->prometheus_allowed_ips; + + foreach ($allowedIps as $allowedIp) { + if (str_contains($allowedIp, '/')) { + if (Network::ipInRange($clientIp, $allowedIp)) { + return $next($request); + } + } elseif ($clientIp === $allowedIp) { + return $next($request); + } + } + + abort(403); + } +} diff --git a/app/Listeners/ProcessSpeedtestDataIntegrations.php b/app/Listeners/ProcessSpeedtestDataIntegrations.php index effb9bdbd..8b2360b9f 100644 --- a/app/Listeners/ProcessSpeedtestDataIntegrations.php +++ b/app/Listeners/ProcessSpeedtestDataIntegrations.php @@ -6,6 +6,7 @@ use App\Events\SpeedtestFailed; use App\Jobs\Influxdb\v2\WriteResult; use App\Settings\DataIntegrationSettings; +use Illuminate\Support\Facades\Cache; class ProcessSpeedtestDataIntegrations { @@ -24,5 +25,9 @@ public function handle(SpeedtestCompleted|SpeedtestFailed $event): void if ($this->settings->influxdb_v2_enabled) { WriteResult::dispatch($event->result); } + + if ($this->settings->prometheus_enabled) { + Cache::forever('prometheus:latest_result', $event->result->id); + } } } diff --git a/app/Services/PrometheusMetricsService.php b/app/Services/PrometheusMetricsService.php new file mode 100644 index 000000000..7b433369d --- /dev/null +++ b/app/Services/PrometheusMetricsService.php @@ -0,0 +1,249 @@ +emptyMetrics(); + } + + $lastResult = Result::find($resultId); + + if (! $lastResult) { + return $this->emptyMetrics(); + } + + $this->registerMetrics($registry, $lastResult); + + $renderer = new RenderTextFormat; + + return $renderer->render($registry->getMetricFamilySamples()); + } + + protected function registerMetrics(CollectorRegistry $registry, Result $result): void + { + $labels = $this->buildLabels($result); + $labelNames = array_keys($labels); + $labelValues = array_values($labels); + + // Download speed in bytes + $downloadBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_bytes', + 'Download speed in bytes per second', + $labelNames + ); + $downloadBytesGauge->set($result->download, $labelValues); + + // Upload speed in bytes + $uploadBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_bytes', + 'Upload speed in bytes per second', + $labelNames + ); + $uploadBytesGauge->set($result->upload, $labelValues); + + // Download speed in bits per second + $downloadBitsGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_bits', + 'Download speed in bits per second', + $labelNames + ); + $downloadBitsGauge->set(toBits($result->download), $labelValues); + + // Upload speed in bits per second + $uploadBitsGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_bits', + 'Upload speed in bits per second', + $labelNames + ); + $uploadBitsGauge->set(toBits($result->upload), $labelValues); + + // Ping latency in milliseconds + $pingGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_ms', + 'Ping latency in milliseconds', + $labelNames + ); + $pingGauge->set($result->ping, $labelValues); + + // Ping jitter + $pingJitterGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_jitter_ms', + 'Ping jitter in milliseconds', + $labelNames + ); + $pingJitterGauge->set($result->ping_jitter ?? 0, $labelValues); + + // Download jitter + $downloadJitterGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_jitter_ms', + 'Download jitter in milliseconds', + $labelNames + ); + $downloadJitterGauge->set($result->download_jitter ?? 0, $labelValues); + + // Upload jitter + $uploadJitterGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_jitter_ms', + 'Upload jitter in milliseconds', + $labelNames + ); + $uploadJitterGauge->set($result->upload_jitter ?? 0, $labelValues); + + // Packet loss + $packetLossGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'packet_loss_percent', + 'Packet loss percentage', + $labelNames + ); + $packetLossGauge->set($result->packet_loss ?? 0, $labelValues); + + // Ping latency low/high + $pingLowGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_low_ms', + 'Ping low latency in milliseconds', + $labelNames + ); + $pingLowGauge->set($result->ping_low ?? 0, $labelValues); + + $pingHighGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_high_ms', + 'Ping high latency in milliseconds', + $labelNames + ); + $pingHighGauge->set($result->ping_high ?? 0, $labelValues); + + // Download latency metrics (IQM = Interquartile Mean - more reliable than average) + $downloadLatencyIqmGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_latency_iqm_ms', + 'Download latency interquartile mean in milliseconds', + $labelNames + ); + $downloadLatencyIqmGauge->set($result->downloadlatencyiqm ?? 0, $labelValues); + + $downloadLatencyLowGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_latency_low_ms', + 'Download latency low in milliseconds', + $labelNames + ); + $downloadLatencyLowGauge->set($result->downloadlatency_low ?? 0, $labelValues); + + $downloadLatencyHighGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_latency_high_ms', + 'Download latency high in milliseconds', + $labelNames + ); + $downloadLatencyHighGauge->set($result->downloadlatency_high ?? 0, $labelValues); + + // Upload latency metrics + $uploadLatencyIqmGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_latency_iqm_ms', + 'Upload latency interquartile mean in milliseconds', + $labelNames + ); + $uploadLatencyIqmGauge->set($result->uploadlatencyiqm ?? 0, $labelValues); + + $uploadLatencyLowGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_latency_low_ms', + 'Upload latency low in milliseconds', + $labelNames + ); + $uploadLatencyLowGauge->set($result->uploadlatency_low ?? 0, $labelValues); + + $uploadLatencyHighGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_latency_high_ms', + 'Upload latency high in milliseconds', + $labelNames + ); + $uploadLatencyHighGauge->set($result->uploadlatency_high ?? 0, $labelValues); + + // Bytes transferred during test + $downloadedBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'downloaded_bytes', + 'Total bytes downloaded during test', + $labelNames + ); + $downloadedBytesGauge->set($result->downloaded_bytes ?? 0, $labelValues); + + $uploadedBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'uploaded_bytes', + 'Total bytes uploaded during test', + $labelNames + ); + $uploadedBytesGauge->set($result->uploaded_bytes ?? 0, $labelValues); + + // Test duration + $downloadElapsedGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_elapsed_ms', + 'Download test duration in milliseconds', + $labelNames + ); + $downloadElapsedGauge->set($result->download_elapsed ?? 0, $labelValues); + + $uploadElapsedGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_elapsed_ms', + 'Upload test duration in milliseconds', + $labelNames + ); + $uploadElapsedGauge->set($result->upload_elapsed ?? 0, $labelValues); + } + + protected function buildLabels(Result $result): array + { + return [ + 'server_id' => (string) ($result->server_id ?? ''), + 'server_name' => $result->server_name ?? '', + 'server_country' => $result->server_country ?? '', + 'server_location' => $result->server_location ?? '', + 'isp' => $result->isp ?? '', + 'scheduled' => $result->scheduled ? 'true' : 'false', + 'healthy' => $result->healthy ? 'true' : 'false', + 'status' => $result->status->value, + 'app_name' => config('app.name', 'Speedtest Tracker'), + ]; + } + + protected function emptyMetrics(): string + { + return "# no data available\n"; + } +} diff --git a/app/Settings/DataIntegrationSettings.php b/app/Settings/DataIntegrationSettings.php index 2cc29515e..31811c768 100644 --- a/app/Settings/DataIntegrationSettings.php +++ b/app/Settings/DataIntegrationSettings.php @@ -18,6 +18,10 @@ class DataIntegrationSettings extends Settings public bool $influxdb_v2_verify_ssl; + public bool $prometheus_enabled; + + public array $prometheus_allowed_ips = []; + public static function group(): string { return 'dataintegration'; diff --git a/composer.json b/composer.json index 3c107977b..e8fa378f0 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "livewire/livewire": "^3.6.4", "lorisleiva/laravel-actions": "^2.9.1", "maennchen/zipstream-php": "^2.4", + "promphp/prometheus_client_php": "^2.14", "saloonphp/laravel-plugin": "^3.0", "secondnetwork/blade-tabler-icons": "^3.35.0", "spatie/laravel-json-api-paginate": "^1.16.3", diff --git a/composer.lock b/composer.lock index 56e6d0ef7..4077fe867 100644 --- a/composer.lock +++ b/composer.lock @@ -5462,6 +5462,74 @@ }, "time": "2025-09-19T23:02:26+00:00" }, + { + "name": "promphp/prometheus_client_php", + "version": "v2.14.1", + "source": { + "type": "git", + "url": "https://github.com/PromPHP/prometheus_client_php.git", + "reference": "a283aea8269287dc35313a0055480d950c59ac1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/a283aea8269287dc35313a0055480d950c59ac1f", + "reference": "a283aea8269287dc35313a0055480d950c59ac1f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4|^8.0" + }, + "replace": { + "endclothing/prometheus_client_php": "*", + "jimdo/prometheus_client_php": "*", + "lkaemmerling/prometheus_client_php": "*" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.3|^7.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5.4", + "phpstan/phpstan-phpunit": "^1.1.0", + "phpstan/phpstan-strict-rules": "^1.1.0", + "phpunit/phpunit": "^9.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/polyfill-apcu": "^1.6" + }, + "suggest": { + "ext-apc": "Required if using APCu.", + "ext-pdo": "Required if using PDO.", + "ext-redis": "Required if using Redis.", + "promphp/prometheus_push_gateway_php": "An easy client for using Prometheus PushGateway.", + "symfony/polyfill-apcu": "Required if you use APCu on PHP8.0+" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Prometheus\\": "src/Prometheus/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Lukas Kämmerling", + "email": "kontakt@lukas-kaemmerling.de" + } + ], + "description": "Prometheus instrumentation library for PHP applications.", + "support": { + "issues": "https://github.com/PromPHP/prometheus_client_php/issues", + "source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.14.1" + }, + "time": "2025-04-14T07:59:43+00:00" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/database/settings/2025_11_25_191005_create_prometheus_settings.php b/database/settings/2025_11_25_191005_create_prometheus_settings.php new file mode 100644 index 000000000..da7c8025f --- /dev/null +++ b/database/settings/2025_11_25_191005_create_prometheus_settings.php @@ -0,0 +1,12 @@ +migrator->add('dataintegration.prometheus_enabled', false); + $this->migrator->add('dataintegration.prometheus_allowed_ips', []); + } +} diff --git a/lang/en/settings/data_integration.php b/lang/en/settings/data_integration.php index 0f3887671..f55a77441 100644 --- a/lang/en/settings/data_integration.php +++ b/lang/en/settings/data_integration.php @@ -33,6 +33,13 @@ 'influxdb_bulk_write_success' => 'Finished bulk data load to Influxdb.', 'influxdb_bulk_write_success_body' => 'Data has been sent to InfluxDB, check if the data was received.', + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Enable', + 'prometheus_enabled_helper_text' => 'When enabled, metrics for each new speedtest will be available at the /prometheus endpoint.', + 'prometheus_allowed_ips' => 'Allowed IP Addresses', + 'prometheus_allowed_ips_helper' => 'List of IP addresses or CIDR ranges (e.g., 192.168.1.0/24) allowed to access the metrics endpoint. Leave empty to allow all IPs.', + // Common labels 'org' => 'Org', 'bucket' => 'Bucket', diff --git a/routes/web.php b/routes/web.php index 140056470..19ca32a2a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,8 @@ middleware(['getting-started', 'public-dashboard']) ->name('home'); +Route::get('/prometheus', MetricsController::class) + ->middleware(PrometheusAllowedIpMiddleware::class) + ->name('prometheus'); + Route::view('/getting-started', 'getting-started') ->name('getting-started'); diff --git a/tests/Feature/MetricsEndpointTest.php b/tests/Feature/MetricsEndpointTest.php new file mode 100644 index 000000000..cadf0ce28 --- /dev/null +++ b/tests/Feature/MetricsEndpointTest.php @@ -0,0 +1,126 @@ +fill(['prometheus_enabled' => false])->save(); + + $response = $this->get('/prometheus'); + + $response->assertNotFound(); + }); + + test('returns metrics when prometheus is enabled and no IP restrictions', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => [], + ])->save(); + + Result::factory()->create(); + + $response = $this->get('/prometheus'); + + $response->assertSuccessful(); + $response->assertHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + }); + + test('returns 403 when IP is not in allowed list', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => ['192.168.1.100', '10.0.0.1'], + ])->save(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '192.168.1.50', + ]); + + $response->assertForbidden(); + }); + + test('returns metrics when IP is in allowed list', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => ['192.168.1.100', '10.0.0.1'], + ])->save(); + + Result::factory()->create(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '192.168.1.100', + ]); + + $response->assertSuccessful(); + $response->assertHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8'); + }); + + test('allows access with empty array', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => [], + ])->save(); + + Result::factory()->create(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '10.0.0.1', + ]); + + $response->assertSuccessful(); + }); + + test('allows access when IP is in CIDR range', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => ['192.168.1.0/24'], + ])->save(); + + Result::factory()->create(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '192.168.1.150', + ]); + + $response->assertSuccessful(); + }); + + test('denies access when IP is not in CIDR range', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => ['192.168.1.0/24'], + ])->save(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '192.168.2.1', + ]); + + $response->assertForbidden(); + }); + + test('supports mixed IP addresses and CIDR ranges', function () { + app(DataIntegrationSettings::class)->fill([ + 'prometheus_enabled' => true, + 'prometheus_allowed_ips' => ['10.0.0.1', '192.168.1.0/24'], + ])->save(); + + Result::factory()->create(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '192.168.1.50', + ]); + + $response->assertSuccessful(); + + $response = $this->get('/prometheus', [ + 'REMOTE_ADDR' => '10.0.0.1', + ]); + + $response->assertSuccessful(); + }); +}); From ff34938cf191f5b273548f768ee7321aa3eb7c4b Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Wed, 3 Dec 2025 16:37:14 +0100 Subject: [PATCH 5/7] bug: fix database notifications being sent double (#2477) --- app/Listeners/ProcessCompletedSpeedtest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php index c764cb275..228151750 100644 --- a/app/Listeners/ProcessCompletedSpeedtest.php +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -55,7 +55,7 @@ private function notifyAppriseChannels(Result $result): void private function notifyDatabaseChannels(Result $result): void { // Don't send database notification if dispatched by a user or test is unhealthy. - if (filled($result->dispatched_by) || ! $result->healthy === false) { + if (filled($result->dispatched_by) || $result->healthy === false) { return; } From 327d6846d88e0177257b1360e6448eabd6b3ad21 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Wed, 3 Dec 2025 20:21:55 -0500 Subject: [PATCH 6/7] New Crowdin updates (#2448) --- lang/de_DE/general.php | 2 + lang/de_DE/results.php | 3 + lang/de_DE/settings/data_integration.php | 7 ++ lang/de_DE/settings/notifications.php | 11 +-- lang/fr_FR/general.php | 2 + lang/fr_FR/results.php | 3 + lang/fr_FR/settings/data_integration.php | 7 ++ lang/fr_FR/settings/notifications.php | 11 +-- lang/nl_NL/general.php | 1 + lang/nl_NL/settings/data_integration.php | 7 ++ lang/nl_NL/settings/notifications.php | 11 +-- lang/pt_BR/api_tokens.php | 30 +++++++ lang/pt_BR/auth.php | 20 +++++ lang/pt_BR/dashboard.php | 14 +++ lang/pt_BR/enums.php | 21 +++++ lang/pt_BR/errors.php | 23 +++++ lang/pt_BR/general.php | 108 +++++++++++++++++++++++ lang/pt_BR/passwords.php | 20 +++++ lang/pt_BR/results.php | 82 +++++++++++++++++ lang/pt_BR/settings.php | 13 +++ lang/pt_BR/settings/data_integration.php | 46 ++++++++++ lang/pt_BR/settings/notifications.php | 48 ++++++++++ lang/pt_BR/settings/thresholds.php | 22 +++++ lang/pt_BR/tools.php | 6 ++ lang/pt_BR/users.php | 15 ++++ lang/pt_BR/validation.php | 91 +++++++++++++++++++ 26 files changed, 600 insertions(+), 24 deletions(-) create mode 100644 lang/pt_BR/api_tokens.php create mode 100644 lang/pt_BR/auth.php create mode 100644 lang/pt_BR/dashboard.php create mode 100644 lang/pt_BR/enums.php create mode 100644 lang/pt_BR/errors.php create mode 100644 lang/pt_BR/general.php create mode 100644 lang/pt_BR/passwords.php create mode 100644 lang/pt_BR/results.php create mode 100644 lang/pt_BR/settings.php create mode 100644 lang/pt_BR/settings/data_integration.php create mode 100644 lang/pt_BR/settings/notifications.php create mode 100644 lang/pt_BR/settings/thresholds.php create mode 100644 lang/pt_BR/tools.php create mode 100644 lang/pt_BR/users.php create mode 100644 lang/pt_BR/validation.php diff --git a/lang/de_DE/general.php b/lang/de_DE/general.php index 00ddf0d0d..86af86298 100644 --- a/lang/de_DE/general.php +++ b/lang/de_DE/general.php @@ -16,6 +16,7 @@ 'no' => 'Nein', 'options' => 'Optionen', 'details' => 'Details', + 'view' => 'Anzeigen', // Common labels 'name' => 'Name', @@ -38,6 +39,7 @@ 'settings' => 'Einstellungen', 'users' => 'Benutzer', 'documentation' => 'Dokumentation', + 'view_documentation' => 'Dokumentation anzeigen', 'links' => 'Links', 'donate' => 'Spenden', diff --git a/lang/de_DE/results.php b/lang/de_DE/results.php index 31a3d2317..127d85b81 100644 --- a/lang/de_DE/results.php +++ b/lang/de_DE/results.php @@ -61,8 +61,11 @@ 'view_on_speedtest_net' => 'Auf Speedtest.net anzeigen', // Notifications + 'speedtest_benchmark_passed' => 'Geschwindigkeits-Benchmark bestanden', + 'speedtest_benchmark_failed' => 'Geschwindigkeits-Benchmark fehlgeschlagen', 'speedtest_started' => 'Geschwindigkeit gestartet', 'speedtest_completed' => 'Geschwindigkeit, abgeschlossen', + 'speedtest_failed' => 'Geschwindigkeit fehlgeschlagen', 'download_threshold_breached' => 'Download-Schwelle gebrochen!', 'upload_threshold_breached' => 'Upload-Schwelle gebrochen!', 'ping_threshold_breached' => 'Ping-Schwelle gebrochen!', diff --git a/lang/de_DE/settings/data_integration.php b/lang/de_DE/settings/data_integration.php index 7f096962d..83464fa4d 100644 --- a/lang/de_DE/settings/data_integration.php +++ b/lang/de_DE/settings/data_integration.php @@ -33,6 +33,13 @@ 'influxdb_bulk_write_success' => 'Massendatenlade für Influxdb abgeschlossen.', 'influxdb_bulk_write_success_body' => 'Daten wurden an InfluxDB gesendet. Überprüfen Sie, ob die Daten empfangen wurden.', + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Aktivieren', + 'prometheus_enabled_helper_text' => 'Wenn aktiviert, werden neue Messungen für jeden neuen Geschwindigkeitstest am /prometheus Endpunkt verfügbar sein.', + 'prometheus_allowed_ips' => 'Erlaubte IP-Adressen', + 'prometheus_allowed_ips_helper' => 'Liste der IP-Adressen oder CIDR-Bereiche (z.B. 192.168.1.0/24) denen es erlaubt ist, auf den Mess-Endpunkt zuzugreifen. Leer lassen, um alle IPs zu erlauben.', + // Common labels 'org' => 'Org', 'bucket' => 'Eimer', diff --git a/lang/de_DE/settings/notifications.php b/lang/de_DE/settings/notifications.php index 446b1c3c2..6d6623a47 100644 --- a/lang/de_DE/settings/notifications.php +++ b/lang/de_DE/settings/notifications.php @@ -7,27 +7,22 @@ // Database notifications 'database' => 'Datenbank', 'database_description' => 'Benachrichtigungen, die an diesen Kanal gesendet werden, werden unter 🔔 Symbol in der Kopfzeile angezeigt.', - 'database_on_speedtest_run' => 'Bei jedem Schnelltest benachrichtigen', - 'database_on_threshold_failure' => 'Benachrichtigen bei Schwellenausfällen', 'test_database_channel' => 'Datenbankkanal testen', // Mail notifications 'mail' => 'Mail', 'recipients' => 'Empfänger', - 'mail_on_speedtest_run' => 'Bei jedem Schnelltest benachrichtigen', - 'mail_on_threshold_failure' => 'Benachrichtigen bei Schwellenausfällen', 'test_mail_channel' => 'Mail-Kanal testen', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', - 'webhook_on_speedtest_run' => 'Bei jedem Schnelltest benachrichtigen', - 'webhook_on_threshold_failure' => 'Benachrichtigen bei Schwellenausfällen', 'test_webhook_channel' => 'Webhook-Kanal testen', + 'webhook_hint_description' => 'Dies sind allgemeine Webhooks. Für Payload-Beispiele und Implementierungsdetails lesen Sie die Dokumentation.', // Common notification messages - 'notify_on_every_speedtest_run' => 'Bei jedem Schnelltest benachrichtigen', - 'notify_on_threshold_failures' => 'Benachrichtigen bei Schwellenausfällen', + 'notify_on_every_speedtest_run' => 'Benachrichtigung bei jedem geplanten Geschwindigkeitstest', + 'notify_on_threshold_failures' => 'Benachrichtigung bei Schwellenausfällen für geplante Geschwindigkeitstests', // Test notification messages 'test_notifications' => [ diff --git a/lang/fr_FR/general.php b/lang/fr_FR/general.php index d0e542de7..adeba836d 100644 --- a/lang/fr_FR/general.php +++ b/lang/fr_FR/general.php @@ -16,6 +16,7 @@ 'no' => 'Non', 'options' => 'Options', 'details' => 'Détails', + 'view' => 'Voir', // Common labels 'name' => 'Nom', @@ -38,6 +39,7 @@ 'settings' => 'Réglages', 'users' => 'Utilisateurs', 'documentation' => 'Documentation', + 'view_documentation' => 'Afficher la documentation', 'links' => 'Liens', 'donate' => 'Faire un don', diff --git a/lang/fr_FR/results.php b/lang/fr_FR/results.php index d0bb17353..7284dcbbc 100644 --- a/lang/fr_FR/results.php +++ b/lang/fr_FR/results.php @@ -61,8 +61,11 @@ 'view_on_speedtest_net' => 'Voir sur Speedtest.net', // Notifications + 'speedtest_benchmark_passed' => 'Le benchmark du test de vitesse a été passé', + 'speedtest_benchmark_failed' => 'Le benchmark du test de vitesse a échoué', 'speedtest_started' => 'Test de vitesse démarré', 'speedtest_completed' => 'Test de vitesse terminé', + 'speedtest_failed' => 'Le test de vitesse a échoué', 'download_threshold_breached' => 'Seuil de téléchargement dépassé !', 'upload_threshold_breached' => 'Seuil d\'envoi dépassé !', 'ping_threshold_breached' => 'Seuil de latence dépassé !', diff --git a/lang/fr_FR/settings/data_integration.php b/lang/fr_FR/settings/data_integration.php index 1ae9ffe1a..007362964 100644 --- a/lang/fr_FR/settings/data_integration.php +++ b/lang/fr_FR/settings/data_integration.php @@ -33,6 +33,13 @@ 'influxdb_bulk_write_success' => 'Charge de données en masse terminée sur Influxdb.', 'influxdb_bulk_write_success_body' => 'Les données ont été envoyées à InfluxDB, vérifiez si les données ont été reçues.', + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Activer', + 'prometheus_enabled_helper_text' => 'Lorsque cette option est activée, les métriques pour chaque nouveau test de vitesse seront disponibles au point de terminaison /prometheus.', + 'prometheus_allowed_ips' => 'Adresses IP autorisées', + 'prometheus_allowed_ips_helper' => 'Liste des adresses IP ou des plages CIDR (par exemple, 192.168.1.0/24) autorisés à accéder au point de terminaison des métriques. Laisser vide pour autoriser toutes les IPs.', + // Common labels 'org' => 'Org', 'bucket' => 'Seau', diff --git a/lang/fr_FR/settings/notifications.php b/lang/fr_FR/settings/notifications.php index 21b3875cd..778334423 100644 --- a/lang/fr_FR/settings/notifications.php +++ b/lang/fr_FR/settings/notifications.php @@ -7,27 +7,22 @@ // Database notifications 'database' => 'Base de données', 'database_description' => 'Les notifications envoyées à ce salon apparaîtront sous l\'icône 🔔 dans l\'entête.', - 'database_on_speedtest_run' => 'Notifier à chaque test de vitesse', - 'database_on_threshold_failure' => 'Notifier en cas de dépassement de seuil', 'test_database_channel' => 'Tester le canal de base de données', // Mail notifications 'mail' => 'Courrier', 'recipients' => 'Destinataires', - 'mail_on_speedtest_run' => 'Notifier à chaque test de vitesse', - 'mail_on_threshold_failure' => 'Notifier en cas de dépassement de seuil', 'test_mail_channel' => 'Tester le canal de messagerie', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', - 'webhook_on_speedtest_run' => 'Notifier à chaque test de vitesse', - 'webhook_on_threshold_failure' => 'Notifier en cas de dépassement de seuil', 'test_webhook_channel' => 'Tester le canal webhook', + 'webhook_hint_description' => 'Ce sont des webhooks génériques. Pour des exemples de charge utile et des détails d\'implémentation, consultez la documentation.', // Common notification messages - 'notify_on_every_speedtest_run' => 'Notifier à chaque test de vitesse', - 'notify_on_threshold_failures' => 'Notifier en cas de dépassement de seuil', + 'notify_on_every_speedtest_run' => 'Notifier à chaque test de vitesse programmé', + 'notify_on_threshold_failures' => 'Notifier les pannes de seuil pour les tests de vitesse programmés', // Test notification messages 'test_notifications' => [ diff --git a/lang/nl_NL/general.php b/lang/nl_NL/general.php index 75deb4f3a..28e36b0f9 100644 --- a/lang/nl_NL/general.php +++ b/lang/nl_NL/general.php @@ -39,6 +39,7 @@ 'settings' => 'Instellingen', 'users' => 'Gebruikers', 'documentation' => 'Documentatie', + 'view_documentation' => 'Bekijk documentatie', 'links' => 'Koppelingen', 'donate' => 'Doneren', diff --git a/lang/nl_NL/settings/data_integration.php b/lang/nl_NL/settings/data_integration.php index 3a223f4e5..2508fdf6b 100644 --- a/lang/nl_NL/settings/data_integration.php +++ b/lang/nl_NL/settings/data_integration.php @@ -33,6 +33,13 @@ 'influxdb_bulk_write_success' => 'Alle resultaten naar InfluxDB sturen afgerond.', 'influxdb_bulk_write_success_body' => 'Gegevens zijn verzonden naar InfluxDB, controleer of de gegevens zijn ontvangen.', + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Inschakelen', + 'prometheus_enabled_helper_text' => 'Wanneer ingeschakeld, zullen statistieken voor elke nieuwe snelheidstest beschikbaar zijn vanaf het /Prometheus eindpunt.', + 'prometheus_allowed_ips' => 'Toegestane IP-adressen', + 'prometheus_allowed_ips_helper' => 'Lijst van IP-adressen of CIDR (bijv. 192.168.1.0/24) toegestaan om het eindpunt van de statistieken te bekijken. Laat leeg om alle IP-adressen toe te staan.', + // Common labels 'org' => 'Org', 'bucket' => 'Emmer', diff --git a/lang/nl_NL/settings/notifications.php b/lang/nl_NL/settings/notifications.php index d36ad831d..9e1942078 100644 --- a/lang/nl_NL/settings/notifications.php +++ b/lang/nl_NL/settings/notifications.php @@ -7,27 +7,22 @@ // Database notifications 'database' => 'Database', 'database_description' => 'Meldingen die naar dit kanaal worden verzonden worden weergegeven onder de 🔔 icoon in de header.', - 'database_on_speedtest_run' => 'Notificatie bij elke snelheidstest uitgevoerd', - 'database_on_threshold_failure' => 'Melding bij limiet overschrijding', 'test_database_channel' => 'Test database notificaties', // Mail notifications 'mail' => 'E-mailen', 'recipients' => 'Ontvangers', - 'mail_on_speedtest_run' => 'Notificatie bij elke snelheidstest uitgevoerd', - 'mail_on_threshold_failure' => 'Melding bij limiet overschrijding', 'test_mail_channel' => 'Test e-mailkanaal', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', - 'webhook_on_speedtest_run' => 'Notificatie bij elke snelheidstest uitgevoerd', - 'webhook_on_threshold_failure' => 'Melding bij limiet overschrijding', 'test_webhook_channel' => 'Test webhook kanaal', + 'webhook_hint_description' => 'Dit zijn generieke webhooks. Raadpleeg de documentatie voor voorbeelden van payloads en implementatiedetails.', // Common notification messages - 'notify_on_every_speedtest_run' => 'Notificatie bij elke snelheidstest uitgevoerd', - 'notify_on_threshold_failures' => 'Melding bij drempelfouten', + 'notify_on_every_speedtest_run' => 'Notificatie bij elke geplande snelheidstest', + 'notify_on_threshold_failures' => 'Melding bij drempelfouten voor geplande snelheidstests', // Test notification messages 'test_notifications' => [ diff --git a/lang/pt_BR/api_tokens.php b/lang/pt_BR/api_tokens.php new file mode 100644 index 000000000..17ae5269c --- /dev/null +++ b/lang/pt_BR/api_tokens.php @@ -0,0 +1,30 @@ + 'Tokens de API', + 'label' => 'Tokens de API', + + // Token management + 'api_token' => 'Token de API', + 'api_tokens' => 'Tokens da API', + 'create_api_token' => 'Criar token de API', + 'your_token' => 'Seu token', + 'token_status' => 'Estado do token', + + // Token lists + 'active_tokens' => 'Tokens ativos', + 'expired_tokens' => 'Tokens expirados', + 'all_tokens' => 'Todos os tokens', + + // Token properties + 'expires_at' => 'Expira em', + 'expires_at_helper_text' => 'Deixe em branco se você não quiser uma data de validade', + 'last_used_at' => 'Última vez usado em', + + // Abilities/Permissions + 'abilities' => 'Habilidades', + 'read_results' => 'Ler resultados', + 'read_results_description' => 'O token terá permissão para ler resultados e estatísticas.', + 'run_speedtest_description' => 'O token terá permissão para executar o teste de velocidade.', + 'list_servers_description' => 'O token terá permissão para listar servidores.', +]; diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php new file mode 100644 index 000000000..16ae2cdb5 --- /dev/null +++ b/lang/pt_BR/auth.php @@ -0,0 +1,20 @@ + 'Credenciais não correspondem aos nossos registros.', + 'password' => 'A senha fornecida está incorreta.', + 'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.', + +]; diff --git a/lang/pt_BR/dashboard.php b/lang/pt_BR/dashboard.php new file mode 100644 index 000000000..16510f3e8 --- /dev/null +++ b/lang/pt_BR/dashboard.php @@ -0,0 +1,14 @@ + 'Painel', + 'no_speedtests_scheduled' => 'Nenhum teste de velocidade agendado.', + 'next_speedtest_at' => 'Próximo teste de velocidade', + + // Widgets + 'recent_results' => 'Resultados Recentes', + 'statistics' => 'Estatísticas', + 'latest_download' => 'Último download', + 'latest_upload' => 'Último upload', + 'latest_ping' => 'Último ping', +]; diff --git a/lang/pt_BR/enums.php b/lang/pt_BR/enums.php new file mode 100644 index 000000000..2518bf4a7 --- /dev/null +++ b/lang/pt_BR/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Benchmarking', + 'checking' => 'Verificando', + 'completed' => 'Concluído', + 'failed' => 'Falhou', + 'running' => 'Executando', + 'started' => 'Iniciado', + 'skipped' => 'Ignorado', + 'waiting' => 'Esperando', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Fake', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/pt_BR/errors.php b/lang/pt_BR/errors.php new file mode 100644 index 000000000..5e1993583 --- /dev/null +++ b/lang/pt_BR/errors.php @@ -0,0 +1,23 @@ + 'Erro no Servidor', + 'oops_server_error' => 'Opa, erro no servidor!', + 'error_message' => 'Mensagem de erro', + 'error_fetching_servers' => 'Erro ao buscar servidores', + 'servers_refreshed_successfully' => 'Servidores atualizados com sucesso', + 'copied_to_clipboard' => 'Copiado para o clipboard', + + // Speedtest specific errors + 'ookla_error' => 'Ocorreu um erro ao listar servidores de velocidade, verifique os logs.', + 'cron_invalid' => 'Expressão cron inválida', + + // Status fix command + 'status_fix' => [ + 'confirm' => 'Você deseja continuar?', + 'fail' => 'Comando abortado.', + 'finished' => '✅ concluído!', + 'info_1' => 'Isto irá verificar todos os resultados e corrigir o status para "concluído" ou "falhou" com base nos dados.', + 'info_2' => '📖 Leia a documentação: https://docs.speedtest-tracker.dev/other/commands', + ], +]; diff --git a/lang/pt_BR/general.php b/lang/pt_BR/general.php new file mode 100644 index 000000000..57ee664d6 --- /dev/null +++ b/lang/pt_BR/general.php @@ -0,0 +1,108 @@ + 'Salvar', + 'cancel' => 'Cancelar', + 'delete' => 'Excluir', + 'edit' => 'Alterar', + 'create' => 'Criar', + 'search' => 'Pesquisar', + 'filter' => 'Filtrar', + 'export' => 'Exportar', + 'actions' => 'Ações', + 'enable' => 'Habilitado', + 'yes' => 'Sim', + 'no' => 'Não', + 'options' => 'Opções', + 'details' => 'Detalhes', + 'view' => 'Visualizar', + + // Common labels + 'name' => 'Nome', + 'email' => 'E-mail', + 'email_address' => 'Endereço de e-mail', + 'password' => 'Senha', + 'password_confirmation' => 'Confirmação de senha', + 'id' => 'ID', + 'status' => 'Status', + 'message' => 'Mensagem', + 'comment' => 'Comentar', + 'comments' => 'Comentários', + 'created_at' => 'Criado em', + 'updated_at' => 'Atualizado em', + 'url' => 'URL', + + // Navigation + 'dashboard' => 'Painel', + 'results' => 'Resultados', + 'settings' => 'Confirgurações', + 'users' => 'Usuários', + 'documentation' => 'Documentação', + 'view_documentation' => 'Ver documentação', + 'links' => 'Links', + 'donate' => 'Doar', + + // Roles + 'admin' => 'Admin', + 'user' => 'Usuário', + 'role' => 'Funções', + + // Date ranges + 'last_24h' => 'Últimas 24 horas', + 'last_week' => 'Semana passada', + 'last_month' => 'Mês anterior', + + // Metrics + 'average' => 'Média', + 'high' => 'Alta', + 'low' => 'Baixa', + 'faster' => 'mais rápido', + 'slower' => 'lento', + 'healthy' => 'Saudável', + + // Units + 'ms' => 'ms', + 'mbps' => 'Mbps', + + // Speed test metrics + 'download' => 'Download', + 'upload' => 'Upload', + 'ping' => 'Latência', + 'jitter' => 'Jitter', + + // Metric labels with units + 'download_mbps' => 'Download (Mbps)', + 'upload_mbps' => 'Upload (Mbps)', + 'ping_ms' => 'Latência (ms)', + 'download_ms' => 'Download (ms)', + 'upload_ms' => 'Upload (ms)', + 'average_ms' => 'Média (ms)', + 'high_ms' => 'Alto (ms)', + 'low_ms' => 'Baixa (ms)', + 'ping_ms_label' => 'Latência (ms)', + + // Latency + 'download_latency' => 'Latência de Download', + 'upload_latency' => 'Latência de Upload', + + // Actions + 'run_speedtest' => 'Executar teste de velocidade', + 'list_servers' => 'Listar servidores', + 'export_current_results' => 'Exportar resultados atuais', + 'test' => 'Testar', + + // Common + 'token' => 'Token', + + // Application + 'speedtest_tracker' => 'Speedtest Tracker', + 'platform' => 'Plataforma', + + // Update status + 'update_available' => 'Atualização disponível!', + 'up_to_date' => 'Atualizado', + + // Notifications + 'token_created' => 'Token criado', +]; diff --git a/lang/pt_BR/passwords.php b/lang/pt_BR/passwords.php new file mode 100644 index 000000000..8fa4d5f00 --- /dev/null +++ b/lang/pt_BR/passwords.php @@ -0,0 +1,20 @@ + 'Sua senha foi redefinida!', + 'sent' => 'Enviamos um e-mail com o link para redefinir sua senha!', + 'password' => 'A senha e a confirmação devem corresponder e conter pelo menos seis caracteres.', + +]; diff --git a/lang/pt_BR/results.php b/lang/pt_BR/results.php new file mode 100644 index 000000000..bc725b402 --- /dev/null +++ b/lang/pt_BR/results.php @@ -0,0 +1,82 @@ + 'Resultados', + 'result_overview' => 'Visão geral do resultado', + 'error_message_title' => 'Mensagem de erro', + + // Metrics + 'download' => 'Download', + 'download_latency_high' => 'Latência de Download alta', + 'download_latency_low' => 'Latência de Download baixa', + 'download_latency_iqm' => 'Latência de Download IQM', + 'download_latency_jitter' => 'Jitter de latência de Download', + + 'upload' => 'Upload', + 'upload_latency_high' => 'Latência de Upload alta', + 'upload_latency_low' => 'Latência de Upload baixa', + 'upload_latency_iqm' => 'Latência de Upload IQM', + 'upload_latency_jitter' => 'Jitter de latência de Upload', + + 'ping' => 'Latência', + 'ping_details' => 'Detalhes do Ping', + 'ping_jitter' => 'Jitter do Ping', + 'ping_high' => 'Latência de ping alta', + 'ping_low' => 'Latência de ping baixa', + + 'packet_loss' => 'Perda de pacote', + 'iqm' => 'IQM', + + // Server & metadata + 'server_&_metadata' => 'Servidor e Metadados', + 'server_id' => 'ID do Servidor', + 'server_host' => 'Host do servidor', + 'server_name' => 'Nome do servidor', + 'server_location' => 'Localização do servidor', + 'service' => 'Serviço', + 'isp' => 'Provedor', + 'ip_address' => 'Endereço IP', + 'scheduled' => 'Agendado', + + // Filters + 'only_healthy_speedtests' => 'Apenas testes de velocidade saudáveis', + 'only_unhealthy_speedtests' => 'Apenas testes de velocidade não saudáveis', + 'only_manual_speedtests' => 'Apenas testes de velocidade manuais', + 'only_scheduled_speedtests' => 'Apenas testes de velocidade agendados', + 'created_from' => 'Criado por', + 'created_until' => 'Criado até', + + // Export + 'export_all_results' => 'Exportar todos resultados', + 'export_all_results_description' => 'Irá exportar todas as colunas para todos os resultados.', + 'export_completed' => 'Exportação concluída, :count :rows exportados.', + 'failed_export' => ':count :rows falhou ao exportar.', + 'row' => '{1} :count fileira [2,*] :count linhas', + + // Actions + 'update_comments' => 'Atualizar comentários', + 'truncate_results' => 'Truncar resultados', + 'truncate_results_description' => 'Tem certeza que deseja truncar todos os resultados? Esta ação é irreversível.', + 'truncate_results_success' => 'Tabela de resultados truncada!', + 'view_on_speedtest_net' => 'Ver em Speedtest.net', + + // Notifications + 'speedtest_benchmark_passed' => 'Referência do teste de velocidade aprovada', + 'speedtest_benchmark_failed' => 'Referência do teste de velocidade falhou', + 'speedtest_started' => 'Teste de velocidade iniciado', + 'speedtest_completed' => 'Teste de velocidade concluído', + 'speedtest_failed' => 'Teste de velocidade falhou', + 'download_threshold_breached' => 'Limite de Download violado!', + 'upload_threshold_breached' => 'Limite de Upload violado!', + 'ping_threshold_breached' => 'Limite de ping violado!', + + // Run Speedtest Action + 'speedtest' => 'Teste de velocidade', + 'public_dashboard' => 'Painel público', + 'select_server' => 'Selecionar servidor', + 'select_server_helper' => 'Deixe em branco para executar o acelerador sem especificar um servidor. Os servidores bloqueados serão ignorados.', + 'manual_servers' => 'Servidores manuais', + 'closest_servers' => 'Servidores mais próximos', + 'run_speedtest' => 'Executar teste de velocidade', + 'start' => 'Iniciar', +]; diff --git a/lang/pt_BR/settings.php b/lang/pt_BR/settings.php new file mode 100644 index 000000000..b557f3d08 --- /dev/null +++ b/lang/pt_BR/settings.php @@ -0,0 +1,13 @@ + 'Confirgurações', + 'label' => 'Confirgurações', + + // Common settings labels + 'triggers' => 'Gatilhos', + 'verify_ssl' => 'Verificar SSL', + 'username' => 'Usuário', + 'username_placeholder' => 'Nome de usuário para autenticação básica (opcional)', + 'password_placeholder' => 'Senha para Autenticação Básica (opcional)', +]; diff --git a/lang/pt_BR/settings/data_integration.php b/lang/pt_BR/settings/data_integration.php new file mode 100644 index 000000000..2b5dd5ae2 --- /dev/null +++ b/lang/pt_BR/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Integração de dados', + 'label' => 'Integração de dados', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'Quando ativado, todos os novos resultados de Speedtest também serão enviados para InfluxDB.', + 'influxdb_v2_enabled' => 'Habilitado', + 'influxdb_v2_url' => 'URL:', + 'influxdb_v2_url_placeholder' => 'http://sua-influxdb-instância', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Bucket', + 'influxdb_v2_bucket_placeholder' => 'speedtest-tracker', + 'influxdb_v2_token' => 'Token', + 'influxdb_v2_verify_ssl' => 'Verificar SSL', + + // Actions + 'test_connection' => 'Testar conexão', + 'starting_bulk_data_write_to_influxdb' => 'Iniciando dados em massa no InfluxDB', + 'sending_test_data_to_influxdb' => 'Enviando dados de teste para InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Falha no teste Influxdb', + 'influxdb_test_failed_body' => 'Confira os logs para mais detalhes.', + 'influxdb_test_success' => 'Dados de teste enviados com sucesso para o Influxdb', + 'influxdb_test_success_body' => 'Dados de teste enviados para InfluxDB, verifique se os dados foram recebidos.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Falha ao escrever no Influxdb.', + 'influxdb_bulk_write_failed_body' => 'Confira os logs para mais detalhes.', + 'influxdb_bulk_write_success' => 'Carga massiva de dados concluída para o Influxdb.', + 'influxdb_bulk_write_success_body' => 'Os dados foram enviados para InfluxDB, verifique se os dados foram recebidos.', + + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Habilitado', + 'prometheus_enabled_helper_text' => 'Quando ativado, as métricas para cada novo radar estarão disponíveis no ponto de extremidade do /prometheus.', + 'prometheus_allowed_ips' => 'Endereços de IP Permitidos', + 'prometheus_allowed_ips_helper' => 'Lista de endereços IP ou intervalos de CIDR (por exemplo, 192.168.1.0/24) permitidos de acessar o ponto de extremidade das métricas. Deixe em branco para permitir que todos os IPs.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Bucket', +]; diff --git a/lang/pt_BR/settings/notifications.php b/lang/pt_BR/settings/notifications.php new file mode 100644 index 000000000..6d90ed8f4 --- /dev/null +++ b/lang/pt_BR/settings/notifications.php @@ -0,0 +1,48 @@ + 'Notificações', + 'label' => 'Notificações', + + // Database notifications + 'database' => 'Banco de Dados', + 'database_description' => 'Notificações enviadas para este canal aparecerão sob o 🔔 ícone no cabeçalho.', + 'test_database_channel' => 'Testar canal do banco de dados', + + // Mail notifications + 'mail' => 'Correio', + 'recipients' => 'Destinatários', + 'test_mail_channel' => 'Testar canal de e-mail', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Testar canal webhook', + 'webhook_hint_description' => 'Estes são webhooks genéricos. Para exemplos de payload e detalhes de implementação, consulte a documentação.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Notificar a cada execução do teste de velocidade', + 'notify_on_threshold_failures' => 'Notificar sobre falhas nos limites de testes de velocidade agendados', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'Eu digo: ping', + 'pong' => 'Você diz: pong', + 'received' => 'Teste de notificação de banco de dados recebida!', + 'sent' => 'Teste de notificação do banco de dados enviada.', + ], + 'mail' => [ + 'add' => 'Adicione destinatários de email!', + 'sent' => 'Notificação de teste de email enviada.', + ], + 'webhook' => [ + 'add' => 'Adicionar URLs webhook!', + 'sent' => 'Notificação de teste webhook enviada.', + 'payload' => 'Testando notificação webhook', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Notificações de limite serão enviadas para a rota /fail na URL.', +]; diff --git a/lang/pt_BR/settings/thresholds.php b/lang/pt_BR/settings/thresholds.php new file mode 100644 index 000000000..5c67c55be --- /dev/null +++ b/lang/pt_BR/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Limites', + 'label' => 'Limites', + + // Absolute thresholds + 'absolute' => 'Absoluto', + 'absolute_description' => 'Os limites absolutos não levam em consideração o histórico anterior e podem ser acionados em cada teste.', + 'absolute_enabled' => 'Habilitar limites absolutos', + + // Metrics section + 'metrics' => 'Métricas', + 'metrics_helper_text' => 'Defina zero para desativar esta métrica.', + + // General threshold labels + 'thresholds' => 'Limites', + 'threshold_enabled' => 'Limite habilitado', + 'threshold_download' => 'Limite de download', + 'threshold_upload' => 'Limite de upload', + 'threshold_ping' => 'Limite de ping', +]; diff --git a/lang/pt_BR/tools.php b/lang/pt_BR/tools.php new file mode 100644 index 000000000..249d79aae --- /dev/null +++ b/lang/pt_BR/tools.php @@ -0,0 +1,6 @@ + 'Servidores Ookla', +]; diff --git a/lang/pt_BR/users.php b/lang/pt_BR/users.php new file mode 100644 index 000000000..ef4af9f81 --- /dev/null +++ b/lang/pt_BR/users.php @@ -0,0 +1,15 @@ + 'Usuários', + 'label' => 'Usuários', + + // User prompts and messages + 'user_change' => [ + 'info' => 'Função do usuário atualizada.', + 'password_updated_info' => ':email atualizado.', + 'what_is_password' => 'Qual é a nova senha?', + 'what_is_the_email_address' => 'Qual é o endereço de e-mail?', + 'what_role' => 'Qual papel o usuário deve ter?', + ], +]; diff --git a/lang/pt_BR/validation.php b/lang/pt_BR/validation.php new file mode 100644 index 000000000..34d05b329 --- /dev/null +++ b/lang/pt_BR/validation.php @@ -0,0 +1,91 @@ + [ + 'attribute-name' => [ + + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [ + 'address' => 'endereço', + 'age' => 'idade', + 'body' => 'conteúdo', + 'cell' => 'celular', + 'city' => 'cidade', + 'country' => 'país', + 'date' => 'data', + 'day' => 'dia', + 'excerpt' => 'resumo', + 'first_name' => 'primeiro nome', + 'gender' => 'sexo', + 'marital_status' => 'estado civil', + 'profession' => 'profissão', + 'nationality' => 'nacionalidade', + 'hour' => 'hora', + 'last_name' => 'último nome', + 'message' => 'mensagem', + 'minute' => 'minuto', + 'mobile' => 'celular', + 'month' => 'mês', + 'name' => 'nome', + 'zipcode' => 'CEP', + 'company_name' => 'nome da empresa', + 'neighborhood' => 'bairro', + 'number' => 'número', + 'password' => 'senha', + 'phone' => 'telefone', + 'second' => 'segundo', + 'sex' => 'sexo', + 'state' => 'estado', + 'street' => 'rua', + 'subject' => 'assunto', + 'text' => 'texto', + 'time' => 'horário', + 'title' => 'título', + 'username' => 'usuário', + 'year' => 'ano', + 'description' => 'descrição', + 'password_confirmation' => 'confirmação de senha', + 'current_password' => 'senha atual', + 'complement' => 'complemento', + 'modality' => 'modalidade', + 'category' => 'categoria', + 'blood_type' => 'tipo de sangue', + 'birth_date' => 'data de nascimento', + ], +]; From f6508fd50166288f9e0ea1e1a77147a6a7594eb0 Mon Sep 17 00:00:00 2001 From: Alex Justesen Date: Wed, 3 Dec 2025 20:27:47 -0500 Subject: [PATCH 7/7] Release v1.11.0 (#2479) Co-authored-by: Alex Justesen <1144087+alexjustesen@users.noreply.github.com> --- config/speedtest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/speedtest.php b/config/speedtest.php index 3e8e3de1c..b04305db3 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-11-30'), + 'build_date' => Carbon::parse('2025-12-03'), - 'build_version' => 'v1.10.3', + 'build_version' => 'v1.11.0', 'content_width' => env('CONTENT_WIDTH', '7xl'),