├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── app ├── Actions │ ├── CheckForScheduledSpeedtests.php │ ├── CheckInternetConnection.php │ ├── GetExternalIpAddress.php │ ├── GetOoklaSpeedtestServers.php │ ├── Influxdb │ │ └── v2 │ │ │ ├── BuildPointData.php │ │ │ └── CreateClient.php │ ├── Notifications │ │ ├── SendDatabaseTestNotification.php │ │ ├── SendDiscordTestNotification.php │ │ ├── SendGotifyTestNotification.php │ │ ├── SendHealthCheckTestNotification.php │ │ ├── SendMailTestNotification.php │ │ ├── SendNtfyTestNotification.php │ │ ├── SendPushoverTestNotification.php │ │ ├── SendSlackTestNotification.php │ │ ├── SendTelegramTestNotification.php │ │ └── SendWebhookTestNotification.php │ └── Ookla │ │ └── RunSpeedtest.php ├── Console │ └── Commands │ │ ├── OoklaListServers.php │ │ ├── ResultFixStatuses.php │ │ ├── UserChangeRole.php │ │ └── UserResetPassword.php ├── Enums │ ├── ResultService.php │ ├── ResultStatus.php │ └── UserRole.php ├── Events │ ├── SpeedtestBenchmarkFailed.php │ ├── SpeedtestBenchmarkPassed.php │ ├── SpeedtestBenchmarking.php │ ├── SpeedtestChecking.php │ ├── SpeedtestCompleted.php │ ├── SpeedtestFailed.php │ ├── SpeedtestRunning.php │ ├── SpeedtestSkipped.php │ ├── SpeedtestStarted.php │ └── SpeedtestWaiting.php ├── Filament │ ├── Exports │ │ └── ResultExporter.php │ ├── Pages │ │ ├── Dashboard.php │ │ └── Settings │ │ │ ├── DataIntegrationPage.php │ │ │ ├── NotificationPage.php │ │ │ └── ThresholdsPage.php │ ├── Resources │ │ ├── ApiTokenResource.php │ │ ├── ApiTokenResource │ │ │ └── Pages │ │ │ │ └── ListApiTokens.php │ │ ├── ResultResource.php │ │ ├── ResultResource │ │ │ └── Pages │ │ │ │ └── ListResults.php │ │ ├── UserResource.php │ │ └── UserResource │ │ │ └── Pages │ │ │ └── ListUsers.php │ ├── VersionProviders │ │ └── SpeedtestTrackerVersionProvider.php │ └── Widgets │ │ ├── RecentDownloadChartWidget.php │ │ ├── RecentDownloadLatencyChartWidget.php │ │ ├── RecentJitterChartWidget.php │ │ ├── RecentPingChartWidget.php │ │ ├── RecentUploadChartWidget.php │ │ ├── RecentUploadLatencyChartWidget.php │ │ └── StatsOverviewWidget.php ├── Helpers │ ├── Average.php │ ├── Benchmark.php │ ├── Bitrate.php │ ├── Network.php │ ├── Number.php │ └── Ookla.php ├── Http │ ├── Controllers │ │ ├── Api │ │ │ ├── V0 │ │ │ │ └── GetLatestController.php │ │ │ └── V1 │ │ │ │ ├── ApiController.php │ │ │ │ ├── LatestResult.php │ │ │ │ ├── ListResults.php │ │ │ │ ├── ListSpeedtestServers.php │ │ │ │ ├── RunSpeedtest.php │ │ │ │ ├── ShowResult.php │ │ │ │ └── Stats.php │ │ ├── Controller.php │ │ └── PagesController.php │ ├── Middleware │ │ ├── AllowedIpAddressesMiddleware.php │ │ ├── GettingStarted.php │ │ └── PublicDashboard.php │ └── Resources │ │ └── V1 │ │ ├── ResultResource.php │ │ └── StatResource.php ├── Jobs │ ├── CheckForInternetConnectionJob.php │ ├── Influxdb │ │ └── v2 │ │ │ ├── BulkWriteResults.php │ │ │ ├── TestConnectionJob.php │ │ │ └── WriteResult.php │ ├── Ookla │ │ ├── BenchmarkSpeedtestJob.php │ │ ├── CompleteSpeedtestJob.php │ │ ├── RunSpeedtestJob.php │ │ ├── SelectSpeedtestServerJob.php │ │ ├── SkipSpeedtestJob.php │ │ └── StartSpeedtestJob.php │ └── TruncateResults.php ├── Listeners │ ├── Database │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── Discord │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── Gotify │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── HealthCheck │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── Mail │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── Ntfy │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── Pushover │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── Slack │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ ├── SpeedtestEventSubscriber.php │ ├── Telegram │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php │ └── Webhook │ │ ├── SendSpeedtestCompletedNotification.php │ │ └── SendSpeedtestThresholdNotification.php ├── Livewire │ └── Topbar │ │ └── RunSpeedtestAction.php ├── Mail │ ├── SpeedtestCompletedMail.php │ ├── SpeedtestThresholdMail.php │ └── Test.php ├── Models │ ├── Result.php │ ├── Traits │ │ └── ResultDataAttributes.php │ └── User.php ├── Notifications │ └── Telegram │ │ ├── SpeedtestNotification.php │ │ └── TestNotification.php ├── OpenApi │ ├── OpenApiDefinition.php │ └── Schemas │ │ ├── ForbiddenErrorSchema.php │ │ ├── NotFoundErrorSchema.php │ │ ├── ResultResponseSchema.php │ │ ├── ResultSchema.php │ │ ├── ResultsCollectionSchema.php │ │ ├── ServersCollectionSchema.php │ │ ├── SpeedtestRunSchema.php │ │ ├── StatsSchema.php │ │ ├── UnauthenticatedErrorSchema.php │ │ └── ValidationErrorSchema.php ├── Policies │ ├── ResultPolicy.php │ └── UserPolicy.php ├── Providers │ ├── AppServiceProvider.php │ ├── Filament │ │ └── AdminPanelProvider.php │ ├── FilamentServiceProvider.php │ └── TelescopeServiceProvider.php ├── Rules │ └── Cron.php ├── Services │ └── SpeedtestFakeResultGenerator.php ├── Settings │ ├── DataIntegrationSettings.php │ ├── NotificationSettings.php │ └── ThresholdSettings.php ├── View │ └── Components │ │ ├── AppLayout.php │ │ ├── DebugLayout.php │ │ └── GuestLayout.php └── helpers.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── .gitkeep ├── api.php ├── app.php ├── database.php ├── logging.php ├── mail.php ├── sanctum.php ├── services.php ├── settings.php ├── speedtest.php ├── telescope.php └── webhook-server.php ├── database ├── .gitignore ├── factories │ ├── ResultFactory.php │ └── UserFactory.php ├── migrations │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2018_08_08_100000_create_telescope_entries_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2022_08_18_015337_create_jobs_table.php │ ├── 2022_08_31_202106_create_results_table.php │ ├── 2022_10_20_211143_create_sessions_table.php │ ├── 2022_10_21_104903_create_settings_table.php │ ├── 2022_10_24_152031_create_notifications_table.php │ ├── 2023_01_05_205157_create_cache_table.php │ ├── 2023_05_07_000000_rename_password_resets_table.php │ ├── 2024_02_18_000050_update_locked_default_on_settings_table.php │ ├── 2024_02_19_134641_create_job_batches_table.php │ ├── 2024_02_19_134706_create_imports_table.php │ ├── 2024_02_19_134707_create_exports_table.php │ ├── 2024_02_19_134708_create_failed_import_rows_table.php │ ├── 2024_11_22_235011_add_benchmarks_to_results_table.php │ ├── 2024_11_23_021744_add_healthy_to_results_table.php │ └── 2025_05_19_160458_reset_existing_api_token_abilities.php ├── seeders │ └── DatabaseSeeder.php └── settings │ ├── 2022_10_21_130121_create_influxdb_settings.php │ ├── 2022_10_24_153150_create_database_notifications_settings.php │ ├── 2022_10_24_153411_create_thresholds_settings.php │ ├── 2022_11_11_134355_create_mail_notification_settings.php │ ├── 2022_12_22_125055_create_telegram_notification_settings.php │ ├── 2023_03_06_002044_add_verify_ssl_to_influx_db_settings.php │ ├── 2023_09_11_144858_create_webhook_notification_settings.php │ ├── 2024_02_07_173217_add_telegram_disable_notification_to_notification_settings.php │ ├── 2024_02_22_144620_create_healthcheck_notification_settings.php │ ├── 2024_02_22_144650_create_discord_notification_settings.php │ ├── 2024_02_22_144650_create_ntfy_notification_settings.php │ ├── 2024_02_22_144650_create_slack_notification_settings.php │ ├── 2024_02_22_144654_create_gotify_notification_settings.php │ ├── 2024_02_22_144680_create_pushover_notification_settings.php │ └── 2024_09_11_094357_rename_influxdb_settings.php.php ├── docker-compose.yml ├── docker ├── 8.3 │ ├── Dockerfile │ ├── php.ini │ ├── start-container │ └── supervisord.conf ├── mariadb │ └── create-testing-database.sh ├── mysql │ └── create-testing-database.sh └── pgsql │ └── create-testing-database.sql ├── lang ├── de_DE │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── en │ ├── passwords.php │ └── validation.php ├── es_ES │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── fr_FR │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── it_IT │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── pt_BR │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── tr_TR │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php └── zh_TW │ ├── auth.php │ ├── pagination.php │ ├── passwords.php │ └── validation.php ├── openapi.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── pint.json ├── postcss.config.js ├── public ├── .htaccess ├── css │ ├── app │ │ └── panel.css │ └── filament │ │ ├── filament │ │ └── app.css │ │ ├── forms │ │ └── forms.css │ │ └── support │ │ └── support.css ├── favicon.ico ├── fonts │ └── inter │ │ ├── Inter-Black.woff │ │ ├── Inter-Black.woff2 │ │ ├── Inter-BlackItalic.woff │ │ ├── Inter-BlackItalic.woff2 │ │ ├── Inter-Bold.woff │ │ ├── Inter-Bold.woff2 │ │ ├── Inter-BoldItalic.woff │ │ ├── Inter-BoldItalic.woff2 │ │ ├── Inter-ExtraBold.woff │ │ ├── Inter-ExtraBold.woff2 │ │ ├── Inter-ExtraBoldItalic.woff │ │ ├── Inter-ExtraBoldItalic.woff2 │ │ ├── Inter-ExtraLight.woff │ │ ├── Inter-ExtraLight.woff2 │ │ ├── Inter-ExtraLightItalic.woff │ │ ├── Inter-ExtraLightItalic.woff2 │ │ ├── Inter-Italic.woff │ │ ├── Inter-Italic.woff2 │ │ ├── Inter-Light.woff │ │ ├── Inter-Light.woff2 │ │ ├── Inter-LightItalic.woff │ │ ├── Inter-LightItalic.woff2 │ │ ├── Inter-Medium.woff │ │ ├── Inter-Medium.woff2 │ │ ├── Inter-MediumItalic.woff │ │ ├── Inter-MediumItalic.woff2 │ │ ├── Inter-Regular.woff │ │ ├── Inter-Regular.woff2 │ │ ├── Inter-SemiBold.woff │ │ ├── Inter-SemiBold.woff2 │ │ ├── Inter-SemiBoldItalic.woff │ │ ├── Inter-SemiBoldItalic.woff2 │ │ ├── Inter-Thin.woff │ │ ├── Inter-Thin.woff2 │ │ ├── Inter-ThinItalic.woff │ │ ├── Inter-ThinItalic.woff2 │ │ ├── Inter-italic.var.woff2 │ │ ├── Inter-roman.var.woff2 │ │ ├── Inter.var.woff2 │ │ └── inter.css ├── img │ └── speedtest-tracker-icon.png ├── index.php ├── js │ └── filament │ │ ├── filament │ │ ├── app.js │ │ └── echo.js │ │ ├── forms │ │ ├── components │ │ │ ├── color-picker.js │ │ │ ├── date-time-picker.js │ │ │ ├── file-upload.js │ │ │ ├── key-value.js │ │ │ ├── markdown-editor.js │ │ │ ├── rich-editor.js │ │ │ ├── select.js │ │ │ ├── tags-input.js │ │ │ └── textarea.js │ │ └── forms.js │ │ ├── notifications │ │ └── notifications.js │ │ ├── support │ │ ├── async-alpine.js │ │ └── support.js │ │ ├── tables │ │ ├── components │ │ │ └── table.js │ │ └── tables.js │ │ └── widgets │ │ └── components │ │ ├── chart.js │ │ └── stats-overview │ │ └── stat │ │ └── chart.js ├── robots.txt └── vendor │ └── telescope │ ├── app-dark.css │ ├── app.css │ ├── app.js │ ├── favicon.ico │ └── mix-manifest.json ├── resources ├── css │ ├── app.css │ └── panel.css └── views │ ├── dashboard.blade.php │ ├── discord │ ├── speedtest-completed.blade.php │ └── speedtest-threshold.blade.php │ ├── emails │ ├── speedtest-completed.blade.php │ ├── speedtest-threshold.blade.php │ └── test.blade.php │ ├── errors │ └── 500.blade.php │ ├── filament │ ├── forms │ │ ├── notifications-helptext.blade.php │ │ └── thresholds-helptext.blade.php │ └── pages │ │ └── dashboard.blade.php │ ├── getting-started.blade.php │ ├── gotify │ ├── speedtest-completed.blade.php │ └── speedtest-threshold.blade.php │ ├── layouts │ ├── app.blade.php │ ├── debug.blade.php │ └── guest.blade.php │ ├── livewire │ └── topbar │ │ └── run-speedtest-action.blade.php │ ├── ntfy │ ├── speedtest-completed.blade.php │ └── speedtest-threshold.blade.php │ ├── pushover │ ├── speedtest-completed.blade.php │ └── speedtest-threshold.blade.php │ ├── slack │ ├── speedtest-completed.blade.php │ └── speedtest-threshold.blade.php │ └── telegram │ ├── speedtest-completed.blade.php │ └── speedtest-threshold.blade.php ├── routes ├── api.php ├── api │ └── v1 │ │ └── routes.php ├── console.php ├── test.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── FeatureTestCase.php │ └── RouteTest.php ├── Pest.php ├── TestCase.php └── Unit │ ├── ResultTest.php │ └── UnitTestCase.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Speedtest Tracker" 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=false 5 | APP_URL=http://localhost 6 | 7 | APP_LOCALE=en 8 | APP_FALLBACK_LOCALE=en 9 | APP_FAKER_LOCALE=en_US 10 | 11 | APP_MAINTENANCE_DRIVER=file 12 | APP_MAINTENANCE_STORE=database 13 | 14 | PHP_CLI_SERVER_WORKERS=4 15 | 16 | BCRYPT_ROUNDS=12 17 | 18 | LOG_CHANNEL=stack 19 | LOG_STACK=single 20 | LOG_DEPRECATIONS_CHANNEL=null 21 | LOG_LEVEL=debug 22 | 23 | DB_CONNECTION=sqlite 24 | 25 | BROADCAST_CONNECTION=log 26 | CACHE_STORE=database 27 | FILESYSTEM_DISK=local 28 | QUEUE_CONNECTION=database 29 | 30 | SESSION_DRIVER=database 31 | SESSION_LIFETIME=120 32 | SESSION_ENCRYPT=false 33 | SESSION_PATH=/ 34 | SESSION_DOMAIN=null 35 | 36 | MAIL_MAILER=smtp 37 | MAIL_HOST=mailhog 38 | MAIL_PORT=1025 39 | MAIL_USERNAME=null 40 | MAIL_PASSWORD=null 41 | MAIL_SCHEME=null 42 | MAIL_FROM_ADDRESS="hello@example.com" 43 | MAIL_FROM_NAME="Speedtest Tracker" 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | _ide_helper.php 10 | .env 11 | .env.backup 12 | .phpstorm.meta.php 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | auth.json 17 | npm-debug.log 18 | yarn-error.log 19 | /.fleet 20 | /.idea 21 | /.nova 22 | /.phpunit.cache 23 | /.vscode 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | See the [docs](https://docs.speedtest-tracker.dev/) for contribution guidelines and how to setup your local environment. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Justesen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐇 Speedtest Tracker 2 | 3 | Speedtest Tracker is a self-hosted application that monitors the performance and uptime of your internet connection. 4 | 5 | ![Dashboard](.github/screenshots/dashboard.jpeg) 6 | 7 | ## Features 8 | 9 | - **Automated Tests**: Schedule regular speed tests to monitor your internet connection's performance over time. 10 | - **Detailed Metrics**: Capture download and upload speeds, ping, packet loss and more. 11 | - **Historical Data**: View historical data and trends to identify patterns and issues with your internet connection. 12 | - **Notifications**: Receive notifications when your internet performance drops below a certain threshold. 13 | 14 | ## Getting Started 15 | 16 | Speedtest Tracker is containerized so you can run it anywhere you run your containers. The image is built by LinuxServer.io, build information can be found [here](https://fleet.linuxserver.io/image?name=linuxserver/speedtest-tracker). 17 | 18 | - [Installation](https://docs.speedtest-tracker.dev/getting-started/installation) guide will get you up and running and includes steps for deploying the Docker image or to NAS platforms like Synology and Unraid. 19 | - [Configurations](https://docs.speedtest-tracker.dev/getting-started/environment-variables) are used to tailor Speedtest Tracker to your needs. 20 | - [Notifications](https://docs.speedtest-tracker.dev/settings/notifications) channels alert you when issues happen. 21 | - [Frequently Asked Questions](https://docs.speedtest-tracker.dev/help/faqs) are common questions that can help you resolve issues. 22 | 23 | [![Star History Chart](https://api.star-history.com/svg?repos=alexjustesen/speedtest-tracker&type=Date)](https://star-history.com/#alexjustesen/speedtest-tracker&Date) 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Found a security vulnerability? 6 | 7 | **DON'T open an issue**, report it to [sec@alexjustesen.com](mailto:sec@alexjustesen.com) so I can address it promptly. 8 | -------------------------------------------------------------------------------- /app/Actions/CheckForScheduledSpeedtests.php: -------------------------------------------------------------------------------- 1 | isSpeedtestDue(schedule: $schedule), 23 | scheduled: true, 24 | ); 25 | } 26 | 27 | /** 28 | * Assess if a speedtest is due to run based on the schedule. 29 | */ 30 | private function isSpeedtestDue(string $schedule): bool 31 | { 32 | $cron = new CronExpression($schedule); 33 | 34 | return $cron->isDue( 35 | currentTime: now(), 36 | timeZone: config('app.display_timezone') 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Actions/CheckInternetConnection.php: -------------------------------------------------------------------------------- 1 | timeout(5) 20 | ->get(config('speedtest.checkinternet_url')); 21 | 22 | if (! $response->ok()) { 23 | return false; 24 | } 25 | 26 | return Str::trim($response->body()); 27 | } catch (Throwable $e) { 28 | Log::error('Failed to connect to the internet.', [$e->getMessage()]); 29 | 30 | return false; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Actions/GetExternalIpAddress.php: -------------------------------------------------------------------------------- 1 | timeout(5) 20 | ->get(url: 'https://icanhazip.com/'); 21 | } catch (Throwable $e) { 22 | Log::error('Failed to fetch external IP address.', [$e->getMessage()]); 23 | 24 | return false; 25 | } 26 | 27 | return Str::trim($response->body()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Actions/GetOoklaSpeedtestServers.php: -------------------------------------------------------------------------------- 1 | 'js', 18 | 'https_functional' => true, 19 | 'limit' => 20, 20 | ]; 21 | 22 | try { 23 | $response = Http::retry(3, 250) 24 | ->timeout(5) 25 | ->get(url: 'https://www.speedtest.net/api/js/servers', query: $query); 26 | } catch (Throwable $e) { 27 | Log::error('Unable to retrieve Ookla servers.', [$e->getMessage()]); 28 | 29 | return [ 30 | '⚠️ Unable to retrieve Ookla servers, check internet connection and see logs.', 31 | ]; 32 | } 33 | 34 | return $response->collect()->mapWithKeys(function (array $item, int $key) { 35 | return [ 36 | $item['id'] => $item['sponsor'].' ('.$item['name'].', '.$item['id'].')', 37 | ]; 38 | })->toArray(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Actions/Influxdb/v2/CreateClient.php: -------------------------------------------------------------------------------- 1 | $settings->influxdb_v2_url, 20 | 'token' => $settings->influxdb_v2_token, 21 | 'bucket' => $settings->influxdb_v2_bucket, 22 | 'org' => $settings->influxdb_v2_org, 23 | 'verifySSL' => $settings->influxdb_v2_verify_ssl, 24 | 'precision' => WritePrecision::S, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendDatabaseTestNotification.php: -------------------------------------------------------------------------------- 1 | notify( 16 | Notification::make() 17 | ->title('Test database notification received!') 18 | ->body('You say pong') 19 | ->success() 20 | ->toDatabase(), 21 | ); 22 | 23 | Notification::make() 24 | ->title('Test database notification sent.') 25 | ->body('I say ping') 26 | ->success() 27 | ->send(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendDiscordTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add Discord urls!') 18 | ->warning() 19 | ->send(); 20 | 21 | return; 22 | } 23 | 24 | foreach ($webhooks as $webhook) { 25 | WebhookCall::create() 26 | ->url($webhook['url']) 27 | ->payload(['content' => '👋 Testing the Discord notification channel.']) 28 | ->doNotSign() 29 | ->dispatch(); 30 | } 31 | 32 | Notification::make() 33 | ->title('Test Discord notification sent.') 34 | ->success() 35 | ->send(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendGotifyTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add Gotify urls!') 18 | ->warning() 19 | ->send(); 20 | 21 | return; 22 | } 23 | 24 | foreach ($webhooks as $webhook) { 25 | WebhookCall::create() 26 | ->url($webhook['url']) 27 | ->payload(['message' => '👋 Testing the Gotify notification channel.']) 28 | ->doNotSign() 29 | ->dispatch(); 30 | } 31 | 32 | Notification::make() 33 | ->title('Test Gotify notification sent.') 34 | ->success() 35 | ->send(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendHealthCheckTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add HealthCheck.io urls!') 18 | ->warning() 19 | ->send(); 20 | 21 | return; 22 | } 23 | 24 | foreach ($webhooks as $webhook) { 25 | WebhookCall::create() 26 | ->url($webhook['url']) 27 | ->payload(['message' => '👋 Testing the HealthCheck.io notification channel.']) 28 | ->doNotSign() 29 | ->dispatch(); 30 | } 31 | 32 | Notification::make() 33 | ->title('Test HealthCheck.io notification sent.') 34 | ->success() 35 | ->send(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendMailTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add mail recipients!') 19 | ->warning() 20 | ->send(); 21 | 22 | return; 23 | } 24 | 25 | foreach ($recipients as $recipient) { 26 | Mail::to($recipient) 27 | ->send(new TestMail); 28 | } 29 | 30 | Notification::make() 31 | ->title('Test mail notification sent.') 32 | ->success() 33 | ->send(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendNtfyTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add ntfy urls!') 18 | ->warning() 19 | ->send(); 20 | 21 | return; 22 | } 23 | 24 | foreach ($webhooks as $webhook) { 25 | $webhookCall = WebhookCall::create() 26 | ->url($webhook['url']) 27 | ->payload([ 28 | 'topic' => $webhook['topic'], 29 | 'message' => '👋 Testing the ntfy notification channel.', 30 | ]) 31 | ->doNotSign(); 32 | 33 | // Only add authentication if username and password are provided 34 | if (! empty($webhook['username']) && ! empty($webhook['password'])) { 35 | $authHeader = 'Basic '.base64_encode($webhook['username'].':'.$webhook['password']); 36 | $webhookCall->withHeaders([ 37 | 'Authorization' => $authHeader, 38 | ]); 39 | } 40 | 41 | $webhookCall->dispatch(); 42 | } 43 | 44 | Notification::make() 45 | ->title('Test ntfy notification sent.') 46 | ->success() 47 | ->send(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendPushoverTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add Pushover URLs!') 18 | ->warning() 19 | ->send(); 20 | 21 | return; 22 | } 23 | 24 | foreach ($webhooks as $webhook) { 25 | WebhookCall::create() 26 | ->url($webhook['url']) 27 | ->payload([ 28 | 'token' => $webhook['api_token'], 29 | 'user' => $webhook['user_key'], 30 | 'message' => '👋 Testing the Pushover notification channel.', 31 | ]) 32 | ->doNotSign() 33 | ->dispatch(); 34 | } 35 | 36 | Notification::make() 37 | ->title('Test Pushover notification sent.') 38 | ->success() 39 | ->send(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendSlackTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add Slack URLs!') 18 | ->warning() 19 | ->send(); 20 | 21 | return; 22 | } 23 | 24 | foreach ($webhooks as $webhook) { 25 | WebhookCall::create() 26 | ->url($webhook['url']) 27 | ->payload(['text' => '👋 Testing the Slack notification channel.']) 28 | ->doNotSign() 29 | ->dispatch(); 30 | } 31 | 32 | Notification::make() 33 | ->title('Test Slack notification sent.') 34 | ->success() 35 | ->send(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendTelegramTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add Telegram recipients!') 19 | ->warning() 20 | ->send(); 21 | 22 | return; 23 | } 24 | 25 | foreach ($recipients as $recipient) { 26 | FacadesNotification::route('telegram_chat_id', $recipient['telegram_chat_id']) 27 | ->notify(new TestNotification); 28 | } 29 | 30 | Notification::make() 31 | ->title('Test Telegram notification sent.') 32 | ->success() 33 | ->send(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Actions/Notifications/SendWebhookTestNotification.php: -------------------------------------------------------------------------------- 1 | title('You need to add webhook URLs!') 20 | ->warning() 21 | ->send(); 22 | 23 | return; 24 | } 25 | 26 | // Generate a fake Result (NOT saved to database) 27 | $fakeResult = SpeedtestFakeResultGenerator::completed(); 28 | 29 | foreach ($webhooks as $webhook) { 30 | WebhookCall::create() 31 | ->url($webhook['url']) 32 | ->payload([ 33 | 'result_id' => fake()->uuid(), 34 | 'site_name' => 'Webhook Notification Testing', 35 | 'isp' => $fakeResult->data['isp'], 36 | 'ping' => $fakeResult->ping, 37 | 'download' => $fakeResult->download, 38 | 'upload' => $fakeResult->upload, 39 | 'packetLoss' => $fakeResult->data['packetLoss'], 40 | 'speedtest_url' => $fakeResult->data['result']['url'], 41 | 'url' => url('/admin/results'), 42 | ]) 43 | ->doNotSign() 44 | ->dispatch(); 45 | } 46 | 47 | Notification::make() 48 | ->title('Test webhook notification sent.') 49 | ->success() 50 | ->send(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Actions/Ookla/RunSpeedtest.php: -------------------------------------------------------------------------------- 1 | server->id' => $serverId, 30 | 'service' => ResultService::Ookla, 31 | 'status' => ResultStatus::Waiting, 32 | 'scheduled' => $scheduled, 33 | ]); 34 | 35 | SpeedtestWaiting::dispatch($result); 36 | 37 | Bus::batch([ 38 | [ 39 | new StartSpeedtestJob($result), 40 | new CheckForInternetConnectionJob($result), 41 | new SkipSpeedtestJob($result), 42 | new SelectSpeedtestServerJob($result), 43 | new RunSpeedtestJob($result), 44 | new BenchmarkSpeedtestJob($result), 45 | new CompleteSpeedtestJob($result), 46 | ], 47 | ])->catch(function (Batch $batch, ?Throwable $e) { 48 | Log::error(sprintf('Speedtest batch "%s" failed for an unknown reason.', $batch->id)); 49 | })->name('Ookla Speedtest')->dispatch(); 50 | 51 | return $result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Console/Commands/OoklaListServers.php: -------------------------------------------------------------------------------- 1 | get( 33 | url: 'https://www.speedtest.net/api/js/servers', 34 | query: [ 35 | 'engine' => 'js', 36 | 'https_functional' => true, 37 | 'search' => $this->argument('search'), 38 | 'limit' => 20, // 20 is the max returned by the api 39 | ], 40 | ); 41 | 42 | if ($response->failed()) { 43 | $this->fail('There was an issue retrieving a list of speedtest servers, check the logs.'); 44 | } 45 | 46 | $fields = ['id', 'sponsor', 'name', 'country', 'distance']; 47 | 48 | table( 49 | headers: $fields, 50 | rows: $response->collect()->select($fields), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Console/Commands/ResultFixStatuses.php: -------------------------------------------------------------------------------- 1 | newLine(); 32 | 33 | $this->info('This will check each result and correct the status to "completed" or "failed" based on the data column.'); 34 | $this->info('📖 Read the docs: https://docs.speedtest-tracker.dev/other/commands'); 35 | 36 | if (! $this->confirm('Do you want to continue?')) { 37 | $this->fail('Command cancelled.'); 38 | } 39 | 40 | /** 41 | * Update completed status 42 | */ 43 | DB::table('results') 44 | ->where(function (Builder $query) { 45 | $query->where('service', '=', 'ookla') 46 | ->whereNull('data->level') 47 | ->whereNull('data->message'); 48 | }) 49 | ->update([ 50 | 'status' => ResultStatus::Completed, 51 | ]); 52 | 53 | /** 54 | * Update failed status. 55 | */ 56 | DB::table('results') 57 | ->where(function (Builder $query) { 58 | $query->where('service', '=', 'ookla') 59 | ->where('data->level', '=', 'error') 60 | ->whereNotNull('data->message'); 61 | }) 62 | ->update([ 63 | 'status' => ResultStatus::Failed, 64 | ]); 65 | 66 | $this->line('✅ finished!'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/Console/Commands/UserChangeRole.php: -------------------------------------------------------------------------------- 1 | match (true) { 37 | ! User::firstWhere('email', $value) => 'User not found.', 38 | default => null 39 | } 40 | ); 41 | 42 | $role = select( 43 | label: 'What role should the user have?', 44 | options: [ 45 | 'admin' => 'Admin', 46 | 'user' => 'User', 47 | ], 48 | default: 'user' 49 | ); 50 | 51 | User::where('email', '=', $email) 52 | ->update([ 53 | 'role' => $role, 54 | ]); 55 | 56 | info('User role updated.'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Console/Commands/UserResetPassword.php: -------------------------------------------------------------------------------- 1 | match (true) { 38 | ! User::firstWhere('email', $value) => 'User not found.', 39 | default => null 40 | } 41 | ); 42 | 43 | $password = password( 44 | label: 'What is the new password?', 45 | required: true, 46 | ); 47 | 48 | User::where('email', '=', $email) 49 | ->update([ 50 | 'password' => Hash::make($password), 51 | ]); 52 | 53 | info('The password for "'.$email.'" has been updated.'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Enums/ResultService.php: -------------------------------------------------------------------------------- 1 | name); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Enums/ResultStatus.php: -------------------------------------------------------------------------------- 1 | 'info', 24 | self::Checking => 'info', 25 | self::Completed => 'success', 26 | self::Failed => 'danger', 27 | self::Running => 'info', 28 | self::Started => 'info', 29 | self::Skipped => 'gray', 30 | self::Waiting => 'info', 31 | }; 32 | } 33 | 34 | public function getLabel(): ?string 35 | { 36 | return Str::title($this->name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Enums/UserRole.php: -------------------------------------------------------------------------------- 1 | 'success', 18 | self::User => 'gray', 19 | }; 20 | } 21 | 22 | public function getLabel(): ?string 23 | { 24 | return Str::title($this->name); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Events/SpeedtestBenchmarkFailed.php: -------------------------------------------------------------------------------- 1 | getNextRunDate(timeZone: config('app.display_timezone')))->format(config('app.datetime_format')); 33 | 34 | return 'Next speedtest at: '.$nextRunDate; 35 | } 36 | 37 | protected function getHeaderWidgets(): array 38 | { 39 | return [ 40 | StatsOverviewWidget::make(), 41 | RecentDownloadChartWidget::make(), 42 | RecentUploadChartWidget::make(), 43 | RecentPingChartWidget::make(), 44 | RecentJitterChartWidget::make(), 45 | RecentDownloadLatencyChartWidget::make(), 46 | RecentUploadLatencyChartWidget::make(), 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Filament/Resources/ApiTokenResource/Pages/ListApiTokens.php: -------------------------------------------------------------------------------- 1 | label('Create API Token') 20 | ->form(ApiTokenResource::getTokenFormSchema()) 21 | ->action(function (array $data): void { 22 | $token = auth()->user()->createToken( 23 | $data['name'], 24 | $data['abilities'], 25 | $data['expires_at'] ? Carbon::parse($data['expires_at']) : null 26 | ); 27 | 28 | Notification::make() 29 | ->title('Token Created') 30 | ->body('Your token: `'.explode('|', $token->plainTextToken)[1].'`') 31 | ->success() 32 | ->persistent() 33 | ->send(); 34 | }) 35 | ->modalWidth('xl'), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Filament/Resources/ResultResource/Pages/ListResults.php: -------------------------------------------------------------------------------- 1 | isProduction() 17 | ? (config('speedtest.build_version')) 18 | : config('app.env'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Helpers/Average.php: -------------------------------------------------------------------------------- 1 | map(function ($item) use ($magnitude, $precision) { 16 | return ! blank($item->download) 17 | ? Number::bitsToMagnitude(bits: $item->download_bits, precision: $precision, magnitude: $magnitude) 18 | : 0; 19 | })->avg(), 20 | $precision 21 | ); 22 | } 23 | 24 | public static function averageUpload(Collection $results, int $precision = 2, string $magnitude = 'mbit'): float 25 | { 26 | return round( 27 | $results->map(function ($item) use ($magnitude, $precision) { 28 | return ! blank($item->upload) 29 | ? Number::bitsToMagnitude(bits: $item->upload_bits, precision: $precision, magnitude: $magnitude) 30 | : 0; 31 | })->avg(), 32 | $precision 33 | ); 34 | } 35 | 36 | public static function averagePing(Collection $results, int $precision = 2): float 37 | { 38 | $avgPing = $results->filter(function ($item) { 39 | return ! blank($item->ping); 40 | })->avg('ping'); 41 | 42 | return round($avgPing, $precision); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Helpers/Benchmark.php: -------------------------------------------------------------------------------- 1 | = Bitrate::normalizeToBits($value.$unit); 24 | } 25 | 26 | /** 27 | * Validate if the ping passes the benchmark. 28 | */ 29 | public static function ping(float|int $ping, array $benchmark): bool 30 | { 31 | $value = Arr::get($benchmark, 'value'); 32 | 33 | // Pass the benchmark if the value is empty. 34 | if (blank($value)) { 35 | return true; 36 | } 37 | 38 | return $ping < $value; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Helpers/Network.php: -------------------------------------------------------------------------------- 1 | '32']; 13 | 14 | $rangeDecimal = ip2long($range); 15 | 16 | $ipDecimal = ip2long($ip); 17 | 18 | $maskDecimal = ~((1 << (32 - (int) $mask)) - 1); 19 | 20 | return ($rangeDecimal & $maskDecimal) === ($ipDecimal & $maskDecimal); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Helpers/Ookla.php: -------------------------------------------------------------------------------- 1 | getMessage()); 15 | $errorMessages = []; 16 | 17 | foreach ($messages as $message) { 18 | $decoded = json_decode($message, true); 19 | if (json_last_error() === JSON_ERROR_NONE && isset($decoded['message'])) { 20 | $errorMessages[] = $decoded['message']; 21 | } 22 | } 23 | 24 | // If no valid messages, use the placeholder 25 | if (empty($errorMessages)) { 26 | $errorMessages[] = 'An unexpected error occurred while running the Ookla CLI.'; 27 | } 28 | 29 | // Remove duplicates and concatenate 30 | return implode(' | ', array_unique($errorMessages)); 31 | } 32 | 33 | public static function getConfigServers(): ?array 34 | { 35 | $list = []; 36 | 37 | if (blank(config('speedtest.servers'))) { 38 | return null; 39 | } 40 | 41 | $servers = collect(array_map( 42 | 'trim', 43 | explode(',', config('speedtest.servers')) 44 | )); 45 | 46 | if (! count($servers)) { 47 | return null; 48 | } 49 | 50 | $list = $servers->mapWithKeys(function ($serverId) { 51 | return [$serverId => $serverId.' (Config server)']; 52 | })->sort()->toArray(); 53 | 54 | return $list; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V0/GetLatestController.php: -------------------------------------------------------------------------------- 1 | whereIn('status', [ResultStatus::Completed, ResultStatus::Failed]) 20 | ->latest() 21 | ->first(); 22 | 23 | if (! $latest) { 24 | return response()->json([ 25 | 'message' => 'No results found.', 26 | ], 404); 27 | } 28 | 29 | return response()->json([ 30 | 'message' => 'ok', 31 | 'data' => [ 32 | 'id' => $latest->id, 33 | 'ping' => $latest->ping, 34 | 'download' => ! blank($latest->download) ? Number::bitsToMagnitude(bits: $latest->download_bits, precision: 2, magnitude: 'mbit') : null, 35 | 'upload' => ! blank($latest->upload) ? Number::bitsToMagnitude(bits: $latest->upload_bits, precision: 2, magnitude: 'mbit') : null, 36 | 'server_id' => $latest->server_id, 37 | 'server_host' => $latest->server_host, 38 | 'server_name' => $latest->server_name, 39 | 'url' => $latest->result_url, 40 | 'scheduled' => $latest->scheduled, 41 | 'failed' => $latest->status === ResultStatus::Failed, 42 | 'created_at' => $latest->created_at->timezone(config('app.display_timezone'))->toISOString(true), 43 | 'updated_at' => $latest->updated_at->timezone(config('app.display_timezone'))->toISOString(true), 44 | ], 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/ApiController.php: -------------------------------------------------------------------------------- 1 | $data, 23 | 'filters' => $filters, 24 | 'message' => $message, 25 | ]); 26 | 27 | if (! empty($message)) { 28 | $response['message'] = $message; 29 | } 30 | 31 | return response()->json( 32 | data: $response, 33 | status: $code, 34 | ); 35 | } 36 | 37 | /** 38 | * Throw an exception. 39 | * 40 | * @param \Exception $e 41 | * @param int $code 42 | * 43 | * @throws \Illuminate\Http\Exceptions\HttpResponseException 44 | */ 45 | public static function throw($e, $code = 500) 46 | { 47 | Log::info($e); 48 | 49 | $response = [ 50 | 'message' => $e->getMessage(), 51 | ]; 52 | 53 | throw new HttpResponseException( 54 | response: response()->json( 55 | data: $response, 56 | status: $code, 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/LatestResult.php: -------------------------------------------------------------------------------- 1 | latest() 41 | ->firstOr(function () { 42 | self::throw( 43 | e: new NotFoundException('No result found.'), 44 | code: 404, 45 | ); 46 | }); 47 | 48 | return self::sendResponse( 49 | data: new ResultResource($result), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/ListSpeedtestServers.php: -------------------------------------------------------------------------------- 1 | 'You do not have permission to view speedtest servers.'] 36 | ) 37 | ), 38 | ] 39 | )] 40 | public function __invoke(Request $request) 41 | { 42 | if ($request->user()->tokenCant('ookla:list-servers')) { 43 | return self::sendResponse( 44 | data: null, 45 | message: 'You do not have permission to view speedtest servers.', 46 | code: Response::HTTP_FORBIDDEN, 47 | ); 48 | } 49 | 50 | $servers = GetOoklaSpeedtestServers::run(); 51 | 52 | return self::sendResponse( 53 | data: $servers, 54 | message: 'Speedtest servers fetched successfully.' 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/ShowResult.php: -------------------------------------------------------------------------------- 1 | select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) 20 | ->where('status', '=', ResultStatus::Completed) 21 | ->latest() 22 | ->first(); 23 | 24 | return view('dashboard', [ 25 | 'latestResult' => $latestResult, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/AllowedIpAddressesMiddleware.php: -------------------------------------------------------------------------------- 1 | ip(), $allowedIps) 25 | ? $next($request) 26 | : abort(403); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/GettingStarted.php: -------------------------------------------------------------------------------- 1 | doesntExist()) { 21 | return redirect()->route('getting-started'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Middleware/PublicDashboard.php: -------------------------------------------------------------------------------- 1 | route('filament.admin.auth.login'); 20 | } 21 | 22 | return $next($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Resources/V1/ResultResource.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function toArray(Request $request): array 17 | { 18 | return [ 19 | 'id' => $this->id, 20 | 'service' => $this->service, 21 | 'ping' => $this->ping, 22 | 'download' => $this->download, 23 | 'upload' => $this->upload, 24 | 'download_bits' => $this->when($this->download, fn (): int|float => Bitrate::bytesToBits($this->download)), 25 | 'upload_bits' => $this->when($this->upload, fn (): int|float => Bitrate::bytesToBits($this->upload)), 26 | 'download_bits_human' => $this->when($this->download, fn (): string => Bitrate::formatBits(Bitrate::bytesToBits($this->download)).'ps'), 27 | 'upload_bits_human' => $this->when($this->upload, fn (): string => Bitrate::formatBits(Bitrate::bytesToBits($this->upload)).'ps'), 28 | 'benchmarks' => $this->benchmarks, 29 | 'healthy' => $this->healthy, 30 | 'status' => $this->status, 31 | 'scheduled' => $this->scheduled, 32 | 'comments' => $this->comments, 33 | 'data' => $this->data, 34 | 'created_at' => $this->created_at->toDateTimestring(), 35 | 'updated_at' => $this->updated_at->toDateTimestring(), 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Jobs/CheckForInternetConnectionJob.php: -------------------------------------------------------------------------------- 1 | result->update([ 42 | 'status' => ResultStatus::Checking, 43 | ]); 44 | 45 | SpeedtestChecking::dispatch($this->result); 46 | 47 | if (CheckInternetConnection::run() !== false) { 48 | return; 49 | } 50 | 51 | $this->result->update([ 52 | 'data->type' => 'log', 53 | 'data->level' => 'error', 54 | 'data->message' => 'Failed to connect to the internet.', 55 | 'status' => ResultStatus::Failed, 56 | ]); 57 | 58 | SpeedtestFailed::dispatch($this->result); 59 | 60 | $this->batch()->cancel(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Jobs/Influxdb/v2/TestConnectionJob.php: -------------------------------------------------------------------------------- 1 | createWriteApi(); 33 | 34 | $point = Point::measurement('speedtest') 35 | ->addTag('service', 'faker') 36 | ->addField('download', (int) 420) 37 | ->addField('upload', (int) 69) 38 | ->addField('ping', (float) 4.321) 39 | ->time(time()); 40 | 41 | try { 42 | $writeApi->write($point); 43 | } catch (ApiException $e) { 44 | Log::error('Failed to write test data to Influxdb.', [ 45 | 'error' => $e->getMessage(), 46 | ]); 47 | 48 | Notification::make() 49 | ->title('Influxdb test failed') 50 | ->body('Check the logs for more details.') 51 | ->danger() 52 | ->sendToDatabase($this->user); 53 | 54 | $writeApi->close(); 55 | 56 | return; 57 | } 58 | 59 | $writeApi->close(); 60 | 61 | Notification::make() 62 | ->title('Successfully sent test data to Influxdb') 63 | ->body('Test data has been sent to InfluxDB, check if the data was received.') 64 | ->success() 65 | ->sendToDatabase($this->user); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Jobs/Influxdb/v2/WriteResult.php: -------------------------------------------------------------------------------- 1 | createWriteApi(); 31 | 32 | $point = BuildPointData::run($this->result); 33 | 34 | try { 35 | $writeApi->write($point); 36 | } catch (\Exception $e) { 37 | Log::error('Failed to write to InfluxDB.', [ 38 | 'error' => $e->getMessage(), 39 | 'result_id' => $this->result->id, 40 | ]); 41 | 42 | $this->fail($e); 43 | } 44 | 45 | $writeApi->close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Jobs/Ookla/CompleteSpeedtestJob.php: -------------------------------------------------------------------------------- 1 | result->update([ 40 | 'status' => ResultStatus::Completed, 41 | ]); 42 | 43 | SpeedtestCompleted::dispatch($this->result); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Jobs/Ookla/StartSpeedtestJob.php: -------------------------------------------------------------------------------- 1 | result->update([ 40 | 'status' => ResultStatus::Started, 41 | ]); 42 | 43 | SpeedtestStarted::dispatch($this->result); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Jobs/TruncateResults.php: -------------------------------------------------------------------------------- 1 | truncate(); 36 | } catch (\Throwable $th) { 37 | $this->fail($th); 38 | 39 | return; 40 | } 41 | 42 | Notification::make() 43 | ->title('Results table truncated!') 44 | ->success() 45 | ->sendToDatabase($this->user); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Listeners/Database/SendSpeedtestCompletedNotification.php: -------------------------------------------------------------------------------- 1 | database_enabled) { 20 | return; 21 | } 22 | 23 | if (! $notificationSettings->database_on_speedtest_run) { 24 | return; 25 | } 26 | 27 | foreach (User::all() as $user) { 28 | Notification::make() 29 | ->title('Speedtest completed') 30 | ->success() 31 | ->sendToDatabase($user); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Listeners/HealthCheck/SendSpeedtestCompletedNotification.php: -------------------------------------------------------------------------------- 1 | healthcheck_enabled) { 20 | return; 21 | } 22 | 23 | if (! $notificationSettings->healthcheck_on_speedtest_run) { 24 | return; 25 | } 26 | 27 | if (! count($notificationSettings->healthcheck_webhooks)) { 28 | Log::warning('healthcheck urls not found, check healthcheck notification channel settings.'); 29 | 30 | return; 31 | } 32 | 33 | foreach ($notificationSettings->healthcheck_webhooks as $url) { 34 | WebhookCall::create() 35 | ->url($url['url']) 36 | ->payload([ 37 | 'result_id' => $event->result->id, 38 | 'site_name' => config('app.name'), 39 | 'isp' => $event->result->isp, 40 | 'ping' => $event->result->ping, 41 | 'download' => $event->result->downloadBits, 42 | 'upload' => $event->result->uploadBits, 43 | 'packetLoss' => $event->result->packet_loss, 44 | 'speedtest_url' => $event->result->result_url, 45 | 'url' => url('/admin/results'), 46 | ]) 47 | ->doNotSign() 48 | ->dispatch(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Listeners/Mail/SendSpeedtestCompletedNotification.php: -------------------------------------------------------------------------------- 1 | mail_enabled) { 21 | return; 22 | } 23 | 24 | if (! $notificationSettings->mail_on_speedtest_run) { 25 | return; 26 | } 27 | 28 | if (! count($notificationSettings->mail_recipients)) { 29 | Log::warning('Mail recipients not found, check mail notification channel settings.'); 30 | 31 | return; 32 | } 33 | 34 | foreach ($notificationSettings->mail_recipients as $recipient) { 35 | Mail::to($recipient) 36 | ->send(new SpeedtestCompletedMail($event->result)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Listeners/SpeedtestEventSubscriber.php: -------------------------------------------------------------------------------- 1 | influxdb_v2_enabled) { 21 | WriteResult::dispatch($event->result); 22 | } 23 | } 24 | 25 | /** 26 | * Handle speedtest completed events. 27 | */ 28 | public function handleSpeedtestCompleted(SpeedtestCompleted $event): void 29 | { 30 | $settings = app(DataIntegrationSettings::class); 31 | 32 | if ($settings->influxdb_v2_enabled) { 33 | WriteResult::dispatch($event->result); 34 | } 35 | } 36 | 37 | /** 38 | * Register the listeners for the subscriber. 39 | */ 40 | public function subscribe(Dispatcher $events): void 41 | { 42 | $events->listen( 43 | SpeedtestFailed::class, 44 | [SpeedtestEventSubscriber::class, 'handleSpeedtestFailed'] 45 | ); 46 | 47 | $events->listen( 48 | SpeedtestCompleted::class, 49 | [SpeedtestEventSubscriber::class, 'handleSpeedtestCompleted'] 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Listeners/Webhook/SendSpeedtestCompletedNotification.php: -------------------------------------------------------------------------------- 1 | webhook_enabled) { 20 | return; 21 | } 22 | 23 | if (! $notificationSettings->webhook_on_speedtest_run) { 24 | return; 25 | } 26 | 27 | if (! count($notificationSettings->webhook_urls)) { 28 | Log::warning('Webhook urls not found, check webhook notification channel settings.'); 29 | 30 | return; 31 | } 32 | 33 | foreach ($notificationSettings->webhook_urls as $url) { 34 | WebhookCall::create() 35 | ->url($url['url']) 36 | ->payload([ 37 | 'result_id' => $event->result->id, 38 | 'site_name' => config('app.name'), 39 | 'isp' => $event->result->isp, 40 | 'ping' => $event->result->ping, 41 | 'download' => $event->result->downloadBits, 42 | 'upload' => $event->result->uploadBits, 43 | 'packetLoss' => $event->result->packet_loss, 44 | 'speedtest_url' => $event->result->result_url, 45 | 'url' => url('/admin/results'), 46 | ]) 47 | ->doNotSign() 48 | ->dispatch(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Mail/SpeedtestCompletedMail.php: -------------------------------------------------------------------------------- 1 | result->id, 35 | ); 36 | } 37 | 38 | /** 39 | * Get the message content definition. 40 | */ 41 | public function content(): Content 42 | { 43 | return new Content( 44 | markdown: 'emails.speedtest-completed', 45 | with: [ 46 | 'id' => $this->result->id, 47 | 'service' => Str::title($this->result->service->getLabel()), 48 | 'serverName' => $this->result->server_name, 49 | 'serverId' => $this->result->server_id, 50 | 'isp' => $this->result->isp, 51 | 'ping' => round($this->result->ping, 2).' ms', 52 | 'download' => Number::toBitRate(bits: $this->result->download_bits, precision: 2), 53 | 'upload' => Number::toBitRate(bits: $this->result->upload_bits, precision: 2), 54 | 'packetLoss' => is_numeric($this->result->packet_loss) ? $this->result->packet_loss : 'n/a', 55 | 'speedtest_url' => $this->result->result_url, 56 | 'url' => url('/admin/results'), 57 | ], 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Mail/SpeedtestThresholdMail.php: -------------------------------------------------------------------------------- 1 | result->id, 35 | ); 36 | } 37 | 38 | /** 39 | * Get the message content definition. 40 | */ 41 | public function content(): Content 42 | { 43 | return new Content( 44 | markdown: 'emails.speedtest-threshold', 45 | with: [ 46 | 'id' => $this->result->id, 47 | 'service' => Str::title($this->result->service->getLabel()), 48 | 'serverName' => $this->result->server_name, 49 | 'serverId' => $this->result->server_id, 50 | 'isp' => $this->result->isp, 51 | 'speedtest_url' => $this->result->result_url, 52 | 'url' => url('/admin/results'), 53 | 'metrics' => $this->metrics, 54 | ], 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Mail/Test.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | protected function casts(): array 30 | { 31 | return [ 32 | 'benchmarks' => 'array', 33 | 'data' => 'array', 34 | 'healthy' => 'boolean', 35 | 'scheduled' => 'boolean', 36 | 'service' => ResultService::class, 37 | 'status' => ResultStatus::class, 38 | ]; 39 | } 40 | 41 | /** 42 | * Get the prunable model query. 43 | */ 44 | public function prunable(): Builder 45 | { 46 | return static::where('created_at', '<=', now()->subDays(config('speedtest.prune_results_older_than'))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Notifications/Telegram/SpeedtestNotification.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function via(object $notifiable): array 28 | { 29 | return ['telegram']; 30 | } 31 | 32 | /** 33 | * Get the Telegram message representation of the notification. 34 | * 35 | * @param mixed $notifiable 36 | */ 37 | public function toTelegram($notifiable): TelegramMessage 38 | { 39 | return TelegramMessage::create() 40 | ->to($notifiable->routes['telegram_chat_id']) 41 | ->content($this->content) 42 | ->disableNotification($this->disableNotification); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Notifications/Telegram/TestNotification.php: -------------------------------------------------------------------------------- 1 | settings = new NotificationSettings; 25 | } 26 | 27 | /** 28 | * Get the notification's delivery channels. 29 | * 30 | * @return array 31 | */ 32 | public function via(object $notifiable): array 33 | { 34 | return ['telegram']; 35 | } 36 | 37 | /** 38 | * Get the Telegram representation of the notification. 39 | * 40 | * @return array 41 | */ 42 | public function toTelegram(object $notifiable): TelegramMessage 43 | { 44 | return TelegramMessage::create() 45 | ->to($notifiable->routes['telegram_chat_id']) 46 | ->disableNotification($this->settings->telegram_disable_notification) 47 | ->content('👋 Testing the Telegram notification channel.'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/OpenApi/OpenApiDefinition.php: -------------------------------------------------------------------------------- 1 | [ 18 | '12345' => 'Fibernet (New York, 12345)', 19 | ], 20 | ], 21 | ), 22 | new OA\Property( 23 | property: 'message', 24 | type: 'string', 25 | description: 'Response status message' 26 | ), 27 | ], 28 | additionalProperties: false 29 | )] 30 | class ServersCollectionSchema {} 31 | -------------------------------------------------------------------------------- /app/OpenApi/Schemas/StatsSchema.php: -------------------------------------------------------------------------------- 1 | Blade::render("@livewire('topbar.run-speedtest-action')"), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Providers/TelescopeServiceProvider.php: -------------------------------------------------------------------------------- 1 | hideSensitiveRequestDetails(); 20 | 21 | Telescope::filter(function (IncomingEntry $entry) { 22 | if ($this->app->environment('local')) { 23 | return true; 24 | } 25 | 26 | return $entry->isReportableException() || 27 | $entry->isFailedRequest() || 28 | $entry->isFailedJob() || 29 | $entry->isScheduledTask() || 30 | $entry->hasMonitoredTag(); 31 | }); 32 | } 33 | 34 | /** 35 | * Prevent sensitive request details from being logged by Telescope. 36 | */ 37 | protected function hideSensitiveRequestDetails(): void 38 | { 39 | if ($this->app->environment('local')) { 40 | return; 41 | } 42 | 43 | Telescope::hideRequestParameters(['_token']); 44 | 45 | Telescope::hideRequestHeaders([ 46 | 'cookie', 47 | 'x-csrf-token', 48 | 'x-xsrf-token', 49 | ]); 50 | } 51 | 52 | /** 53 | * Register the Telescope gate. 54 | * 55 | * This gate determines who can access Telescope in non-local environments. 56 | */ 57 | protected function gate(): void 58 | { 59 | Gate::define('viewTelescope', function ($user) { 60 | return in_array($user->email, [ 61 | // 62 | ]); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Rules/Cron.php: -------------------------------------------------------------------------------- 1 | title = $title; 16 | } 17 | 18 | /** 19 | * Get the view / contents that represent the component. 20 | */ 21 | public function render(): View|Closure|string 22 | { 23 | return view('layouts.app'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/View/Components/DebugLayout.php: -------------------------------------------------------------------------------- 1 | title = $title; 16 | } 17 | 18 | /** 19 | * Get the view / contents that represent the component. 20 | */ 21 | public function render(): View|Closure|string 22 | { 23 | return view('layouts.guest'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 17 | 18 | exit($status); 19 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withProviders() 11 | ->withRouting( 12 | web: __DIR__.'/../routes/web.php', 13 | api: __DIR__.'/../routes/api.php', 14 | commands: __DIR__.'/../routes/console.php', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware) { 17 | $middleware->alias([ 18 | 'getting-started' => App\Http\Middleware\GettingStarted::class, 19 | 'public-dashboard' => App\Http\Middleware\PublicDashboard::class, 20 | ]); 21 | 22 | $middleware->prependToGroup('api', [ 23 | App\Http\Middleware\AllowedIpAddressesMiddleware::class, 24 | ]); 25 | 26 | $middleware->prependToGroup('web', [ 27 | App\Http\Middleware\AllowedIpAddressesMiddleware::class, 28 | ]); 29 | 30 | $middleware->redirectGuestsTo(fn () => route('filament.admin.auth.login')); 31 | $middleware->redirectUsersTo(AppServiceProvider::HOME); 32 | 33 | $middleware->trustProxies(at: '*'); 34 | 35 | $middleware->trustProxies(headers: Request::HEADER_X_FORWARDED_FOR | 36 | Request::HEADER_X_FORWARDED_HOST | 37 | Request::HEADER_X_FORWARDED_PORT | 38 | Request::HEADER_X_FORWARDED_PROTO | 39 | Request::HEADER_X_FORWARDED_AWS_ELB 40 | ); 41 | 42 | $middleware->throttleApi(); 43 | }) 44 | ->withExceptions(function (Exceptions $exceptions) { 45 | // 46 | })->create(); 47 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('API_RATE_LIMIT', 60), 6 | 7 | ]; 8 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Speedtest Tracker'), 6 | 7 | 'env' => env('APP_ENV', 'production'), 8 | 9 | 'chart_begin_at_zero' => env('CHART_BEGIN_AT_ZERO', true), 10 | 11 | 'chart_datetime_format' => env('CHART_DATETIME_FORMAT', 'M. j - G:i'), 12 | 13 | 'datetime_format' => env('DATETIME_FORMAT', 'M. jS, Y g:ia'), 14 | 15 | 'display_timezone' => env('DISPLAY_TIMEZONE', 'UTC'), 16 | 17 | 'force_https' => env('FORCE_HTTPS', false), 18 | 19 | 'admin_name' => env('ADMIN_NAME', 'Admin'), 20 | 21 | 'admin_email' => env('ADMIN_EMAIL', 'admin@example.com'), 22 | 23 | 'admin_password' => env('ADMIN_PASSWORD', 'password'), 24 | 25 | ]; 26 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'smtp' => [ 7 | 'transport' => 'smtp', 8 | 'url' => env('MAIL_URL'), 9 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 10 | 'port' => env('MAIL_PORT', 587), 11 | 'encryption' => env('MAIL_SCHEME', 'tls'), 12 | 'username' => env('MAIL_USERNAME'), 13 | 'password' => env('MAIL_PASSWORD'), 14 | 'timeout' => null, 15 | 'local_domain' => env('MAIL_EHLO_DOMAIN'), 16 | 'verify_peer' => env('MAIL_VERIFY_SSL', true), 17 | 'scheme' => env('MAIL_SCHEME'), 18 | ], 19 | 20 | 'mailgun' => [ 21 | 'transport' => 'mailgun', 22 | // 'client' => [ 23 | // 'timeout' => 5, 24 | // ], 25 | ], 26 | ], 27 | 28 | ]; 29 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'token' => env('TELEGRAM_BOT_TOKEN'), 7 | ], 8 | 9 | ]; 10 | -------------------------------------------------------------------------------- /config/speedtest.php: -------------------------------------------------------------------------------- 1 | Carbon::parse('2025-05-19'), 8 | 9 | 'build_version' => 'v1.6.0', 10 | 11 | /** 12 | * General settings. 13 | */ 14 | 'allowed_ips' => env('ALLOWED_IPS'), 15 | 16 | 'content_width' => env('CONTENT_WIDTH', '7xl'), 17 | 18 | 'prune_results_older_than' => (int) env('PRUNE_RESULTS_OLDER_THAN', 0), 19 | 20 | 'public_dashboard' => env('PUBLIC_DASHBOARD', false), 21 | 22 | /** 23 | * Speedtest settings. 24 | */ 25 | 'schedule' => env('SPEEDTEST_SCHEDULE', false), 26 | 27 | 'servers' => env('SPEEDTEST_SERVERS'), 28 | 29 | 'blocked_servers' => env('SPEEDTEST_BLOCKED_SERVERS'), 30 | 31 | 'interface' => env('SPEEDTEST_INTERFACE'), 32 | 33 | 'checkinternet_url' => env('SPEEDTEST_CHECKINTERNET_URL', 'https://icanhazip.com'), 34 | 35 | /** 36 | * IP filtering settings. 37 | */ 38 | 'skip_ips' => env('SPEEDTEST_SKIP_IPS', ''), 39 | 40 | /** 41 | * Threshold settings. 42 | */ 43 | 44 | 'threshold_enabled' => env('THRESHOLD_ENABLED', false), 45 | 46 | 'threshold_download' => env('THRESHOLD_DOWNLOAD', 0), 47 | 48 | 'threshold_upload' => env('THRESHOLD_UPLOAD', 0), 49 | 50 | 'threshold_ping' => env('THRESHOLD_PING', 0) , 51 | ]; 52 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'name' => fake()->name(), 22 | 'email' => fake()->unique()->safeEmail(), 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's email address should be unverified. 31 | */ 32 | public function unverified(): static 33 | { 34 | return $this->state(fn (array $attributes) => [ 35 | 'email_verified_at' => null, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('name'); 20 | $table->string('email')->unique(); 21 | $table->timestamp('email_verified_at')->nullable(); 22 | $table->string('password'); 23 | $table->rememberToken(); 24 | $table->string('role')->default('user'); 25 | $table->timestamps(); 26 | }); 27 | 28 | User::create([ 29 | 'name' => config('app.admin_name'), 30 | 'email' => config('app.admin_email'), 31 | 'email_verified_at' => now(), 32 | 'password' => Hash::make(config('app.admin_password')), 33 | 'role' => UserRole::Admin, 34 | ]); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | */ 40 | public function down(): void 41 | { 42 | Schema::dropIfExists('users'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 16 | $table->string('token'); 17 | $table->timestamp('created_at')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('password_resets'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('uuid')->unique(); 17 | $table->text('connection'); 18 | $table->text('queue'); 19 | $table->longText('payload'); 20 | $table->longText('exception'); 21 | $table->timestamp('failed_at')->useCurrent(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('failed_jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2022_08_18_015337_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('jobs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2022_08_31_202106_create_results_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('service')->default('ookla'); 17 | $table->float('ping', 8, 3)->nullable(); 18 | $table->unsignedBigInteger('download')->nullable(); 19 | $table->unsignedBigInteger('upload')->nullable(); 20 | $table->text('comments')->nullable(); 21 | $table->json('data')->nullable(); 22 | $table->string('status'); 23 | $table->boolean('scheduled')->default(false); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('results'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2022_10_20_211143_create_sessions_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 16 | $table->foreignId('user_id')->nullable()->index(); 17 | $table->string('ip_address', 45)->nullable(); 18 | $table->text('user_agent')->nullable(); 19 | $table->longText('payload'); 20 | $table->integer('last_activity')->index(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('sessions'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2022_10_21_104903_create_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('group')->index(); 15 | $table->string('name'); 16 | $table->boolean('locked'); 17 | $table->json('payload'); 18 | 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('settings'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2022_10_24_152031_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->string('type'); 17 | $table->morphs('notifiable'); 18 | 19 | /** 20 | * PostgreSQL doesn't support "text" column type so we need to use the 'json' type instead. 21 | * 22 | * Docs: https://filamentphp.com/docs/2.x/notifications/database-notifications 23 | */ 24 | if (config('database.default') == 'pgsql') { 25 | $table->json('data'); 26 | } else { 27 | $table->text('data'); 28 | } 29 | 30 | $table->timestamp('read_at')->nullable(); 31 | $table->timestamps(); 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | */ 38 | public function down(): void 39 | { 40 | Schema::dropIfExists('notifications'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /database/migrations/2023_01_05_205157_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2023_05_07_000000_rename_password_resets_table.php: -------------------------------------------------------------------------------- 1 | boolean('locked')->default(false)->change(); 16 | 17 | $table->unique(['group', 'name']); 18 | 19 | $table->dropIndex(['group']); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::table('settings', function (Blueprint $table): void { 29 | $table->boolean('locked')->default(null)->change(); 30 | 31 | $table->dropUnique(['group', 'name']); 32 | 33 | $table->index('group'); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2024_02_19_134641_create_job_batches_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 16 | $table->string('name'); 17 | $table->integer('total_jobs'); 18 | $table->integer('pending_jobs'); 19 | $table->integer('failed_jobs'); 20 | $table->longText('failed_job_ids'); 21 | $table->mediumText('options')->nullable(); 22 | $table->integer('cancelled_at')->nullable(); 23 | $table->integer('created_at'); 24 | $table->integer('finished_at')->nullable(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('job_batches'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2024_02_19_134706_create_imports_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamp('completed_at')->nullable(); 17 | $table->string('file_name'); 18 | $table->string('file_path'); 19 | $table->string('importer'); 20 | $table->unsignedInteger('processed_rows')->default(0); 21 | $table->unsignedInteger('total_rows'); 22 | $table->unsignedInteger('successful_rows')->default(0); 23 | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('imports'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2024_02_19_134707_create_exports_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamp('completed_at')->nullable(); 17 | $table->string('file_disk'); 18 | $table->string('file_name')->nullable(); 19 | $table->string('exporter'); 20 | $table->unsignedInteger('processed_rows')->default(0); 21 | $table->unsignedInteger('total_rows'); 22 | $table->unsignedInteger('successful_rows')->default(0); 23 | $table->foreignId('user_id')->constrained()->cascadeOnDelete(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('exports'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2024_02_19_134708_create_failed_import_rows_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->json('data'); 17 | $table->foreignId('import_id')->constrained()->cascadeOnDelete(); 18 | $table->text('validation_error')->nullable(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('failed_import_rows'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_11_22_235011_add_benchmarks_to_results_table.php: -------------------------------------------------------------------------------- 1 | json('benchmarks') 16 | ->nullable() 17 | ->after('data'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | // 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_11_23_021744_add_healthy_to_results_table.php: -------------------------------------------------------------------------------- 1 | boolean('healthy') 16 | ->nullable() 17 | ->after('benchmarks'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | // 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_05_19_160458_reset_existing_api_token_abilities.php: -------------------------------------------------------------------------------- 1 | each(function (PersonalAccessToken $token) { 14 | $token->abilities = [ 15 | 'results:read', 16 | ]; 17 | 18 | $token->save(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | // 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | // \App\Models\User::factory()->create([ 18 | // 'name' => 'Test User', 19 | // 'email' => 'test@example.com', 20 | // ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/settings/2022_10_21_130121_create_influxdb_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('influxdb.v2_enabled', false); 10 | $this->migrator->add('influxdb.v2_url', null); 11 | $this->migrator->add('influxdb.v2_org', null); 12 | $this->migrator->add('influxdb.v2_bucket', 'speedtest-tracker'); 13 | $this->migrator->add('influxdb.v2_token', null); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /database/settings/2022_10_24_153150_create_database_notifications_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.database_enabled', false); 10 | $this->migrator->add('notification.database_on_speedtest_run', false); 11 | $this->migrator->add('notification.database_on_threshold_failure', false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /database/settings/2022_10_24_153411_create_thresholds_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('threshold.absolute_enabled', config('speedtest.threshold_enabled')); 10 | $this->migrator->add('threshold.absolute_download', config('speedtest.threshold_download')); 11 | $this->migrator->add('threshold.absolute_upload', config('speedtest.threshold_upload')); 12 | $this->migrator->add('threshold.absolute_ping', config('speedtest.threshold_ping')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/settings/2022_11_11_134355_create_mail_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.mail_enabled', false); 10 | $this->migrator->add('notification.mail_on_speedtest_run', false); 11 | $this->migrator->add('notification.mail_on_threshold_failure', false); 12 | $this->migrator->add('notification.mail_recipients', null); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/settings/2022_12_22_125055_create_telegram_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.telegram_enabled', false); 10 | $this->migrator->add('notification.telegram_on_speedtest_run', false); 11 | $this->migrator->add('notification.telegram_on_threshold_failure', false); 12 | $this->migrator->add('notification.telegram_recipients', null); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /database/settings/2023_03_06_002044_add_verify_ssl_to_influx_db_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('influxdb.v2_verify_ssl', true); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /database/settings/2023_09_11_144858_create_webhook_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.webhook_enabled', false); 13 | $this->migrator->add('notification.webhook_on_speedtest_run', false); 14 | $this->migrator->add('notification.webhook_on_threshold_failure', false); 15 | $this->migrator->add('notification.webhook_urls', null); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /database/settings/2024_02_07_173217_add_telegram_disable_notification_to_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.telegram_disable_notification', false); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /database/settings/2024_02_22_144620_create_healthcheck_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.healthcheck_enabled', false); 10 | $this->migrator->add('notification.healthcheck_on_speedtest_run', false); 11 | $this->migrator->add('notification.healthcheck_on_threshold_failure', false); 12 | $this->migrator->add('notification.healthcheck_webhooks', null); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /database/settings/2024_02_22_144650_create_discord_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.discord_enabled', false); 10 | $this->migrator->add('notification.discord_on_speedtest_run', false); 11 | $this->migrator->add('notification.discord_on_threshold_failure', false); 12 | $this->migrator->add('notification.discord_webhooks', null); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /database/settings/2024_02_22_144650_create_ntfy_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.ntfy_enabled', false); 10 | $this->migrator->add('notification.ntfy_on_speedtest_run', false); 11 | $this->migrator->add('notification.ntfy_on_threshold_failure', false); 12 | $this->migrator->add('notification.ntfy_webhooks', null); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /database/settings/2024_02_22_144650_create_slack_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.slack_enabled', false); 10 | $this->migrator->add('notification.slack_on_speedtest_run', false); 11 | $this->migrator->add('notification.slack_on_threshold_failure', false); 12 | $this->migrator->add('notification.slack_webhooks', null); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /database/settings/2024_02_22_144654_create_gotify_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.gotify_enabled', false); 10 | $this->migrator->add('notification.gotify_on_speedtest_run', false); 11 | $this->migrator->add('notification.gotify_on_threshold_failure', false); 12 | $this->migrator->add('notification.gotify_webhooks', null); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /database/settings/2024_02_22_144680_create_pushover_notification_settings.php: -------------------------------------------------------------------------------- 1 | migrator->add('notification.pushover_enabled', false); 10 | $this->migrator->add('notification.pushover_on_speedtest_run', false); 11 | $this->migrator->add('notification.pushover_on_threshold_failure', false); 12 | $this->migrator->add('notification.pushover_webhooks', null); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /database/settings/2024_09_11_094357_rename_influxdb_settings.php.php: -------------------------------------------------------------------------------- 1 | migrator->rename('influxdb.v2_enabled', 'dataintegration.influxdb_v2_enabled'); 10 | $this->migrator->rename('influxdb.v2_url', 'dataintegration.influxdb_v2_url'); 11 | $this->migrator->rename('influxdb.v2_org', 'dataintegration.influxdb_v2_org'); 12 | $this->migrator->rename('influxdb.v2_bucket', 'dataintegration.influxdb_v2_bucket'); 13 | $this->migrator->rename('influxdb.v2_token', 'dataintegration.influxdb_v2_token'); 14 | $this->migrator->rename('influxdb.v2_verify_ssl', 'dataintegration.influxdb_v2_verify_ssl'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker/8.3/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | pcov.directory = . 6 | -------------------------------------------------------------------------------- /docker/8.3/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then 4 | echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'." 5 | exit 1 6 | fi 7 | 8 | if [ ! -z "$WWWUSER" ]; then 9 | usermod -u $WWWUSER sail 10 | fi 11 | 12 | if [ ! -d /.composer ]; then 13 | mkdir /.composer 14 | fi 15 | 16 | chmod -R ugo+rw /.composer 17 | 18 | if [ $# -gt 0 ]; then 19 | if [ "$SUPERVISOR_PHP_USER" = "root" ]; then 20 | exec "$@" 21 | else 22 | exec gosu $WWWUSER "$@" 23 | fi 24 | else 25 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 26 | fi 27 | -------------------------------------------------------------------------------- /docker/8.3/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:php] 8 | command=%(ENV_SUPERVISOR_PHP_COMMAND)s 9 | user=%(ENV_SUPERVISOR_PHP_USER)s 10 | environment=LARAVEL_SAIL="1" 11 | stdout_logfile=/dev/stdout 12 | stdout_logfile_maxbytes=0 13 | stderr_logfile=/dev/stderr 14 | stderr_logfile_maxbytes=0 15 | -------------------------------------------------------------------------------- /docker/mariadb/create-testing-database.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | /usr/bin/mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL 4 | CREATE DATABASE IF NOT EXISTS testing; 5 | GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%'; 6 | EOSQL 7 | -------------------------------------------------------------------------------- /docker/mysql/create-testing-database.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL 4 | CREATE DATABASE IF NOT EXISTS testing; 5 | GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%'; 6 | EOSQL 7 | -------------------------------------------------------------------------------- /docker/pgsql/create-testing-database.sql: -------------------------------------------------------------------------------- 1 | SELECT 'CREATE DATABASE testing' 2 | WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec 3 | -------------------------------------------------------------------------------- /lang/de_DE/auth.php: -------------------------------------------------------------------------------- 1 | 'Die eingegebenen Zugangsdaten stimmen nicht überein.', 17 | 'password' => 'Das eingegebene Passwort ist falsch.', 18 | 'throttle' => 'Zu viele Anmeldeversuche. Bitte warte :seconds Sekunden und versuche es erneut.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/de_DE/pagination.php: -------------------------------------------------------------------------------- 1 | '« Zurück', 17 | 'next' => 'Weiter »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/de_DE/passwords.php: -------------------------------------------------------------------------------- 1 | 'Dein Passwort wurde erfolgreich zurückgesetzt!', 17 | 'sent' => 'Wir haben dir einen Link zum Zurücksetzen des Passworts per E-Mail geschickt!', 18 | 'password' => 'Das Passwort muss mindestens 6 Zeichen lang sein und mit der Bestätigung übereinstimmen.', 19 | 'throttled' => 'Bitte warte einen Moment, bevor du es erneut versuchst.', 20 | 'token' => 'Der Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen.', 21 | 'user' => 'Zu dieser E-Mail-Adresse existiert kein Benutzerkonto.', 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'password' => 'The password and confirmation must match and contain at least six characters.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/es_ES/auth.php: -------------------------------------------------------------------------------- 1 | 'Estas credenciales no coinciden con nuestros registros.', 17 | 'password' => 'La contraseña proporcionada es incorrecta.', 18 | 'throttle' => 'Demasiados intentos de acceso. Por favor, inténtelo de nuevo en :seconds segundos.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/es_ES/pagination.php: -------------------------------------------------------------------------------- 1 | '« Anterior', 17 | 'next' => 'Siguiente »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/es_ES/passwords.php: -------------------------------------------------------------------------------- 1 | '¡Su contraseña ha sido restablecida!', 17 | 'sent' => '¡Hemos enviado por correo electrónico el enlace para restablecer su contraseña!', 18 | 'password' => 'La contraseña y la confirmación deben coincidir y contener al menos seis caracteres.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/fr_FR/auth.php: -------------------------------------------------------------------------------- 1 | 'Ces crédentials ne correspondent pas à nos archives.', 17 | 'password' => 'Le mot de passe fourni est incorrect.', 18 | 'throttle' => 'Trop de tentatives de connexion échouées. Veuillez réessayer dans :seconds secondes.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/fr_FR/pagination.php: -------------------------------------------------------------------------------- 1 | '« Précédent', 17 | 'next' => 'Suivant »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/fr_FR/passwords.php: -------------------------------------------------------------------------------- 1 | 'Votre mot de passe a été réinitialisé!', 17 | 'sent' => 'Nous vous avons envoyé un email contenant votre lien de réinitialisation!', 18 | 'password' => 'Le mot de passe et le mot de passe de confirmation doivent contenir au moins 6 caractères.', 19 | 'throttled' => 'Veuillez attendre avant de réessayer.', 20 | 'token' => 'Le jeton de réinitialisation du mot de passe n\'est pas valide.', 21 | 'user' => 'Aucun email trouvé pour cette adresse.', 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /lang/it_IT/auth.php: -------------------------------------------------------------------------------- 1 | 'Queste credenziali non corrispondono ai nostri archivi.', 17 | 'password' => 'La password fornita non è corretta.', 18 | 'throttle' => 'Troppi tentativi di accesso. Riprova tra :seconds secondi.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/it_IT/pagination.php: -------------------------------------------------------------------------------- 1 | '« Precedente', 17 | 'next' => 'Successivo »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/it_IT/passwords.php: -------------------------------------------------------------------------------- 1 | 'La tua password è stata reimpostata!', 17 | 'sent' => 'Ti abbiamo inviato via email il link per reimpostare la password!', 18 | 'password' => 'La password e la conferma devono corrispondere e contenere almeno sei caratteri.', 19 | 'throttled' => 'Per favore attendi prima di riprovare.', 20 | 'token' => 'Questo token di reimpostazione della password non è valido.', 21 | 'user' => "Non riusciamo a trovare un utente con quell'indirizzo email.", 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /lang/pt_BR/auth.php: -------------------------------------------------------------------------------- 1 | 'Essas credenciais não foram encontradas em nossos registros.', 17 | 'password' => 'A senha informada está incorreta.', 18 | 'throttle' => 'Muitas tentativas de login. Tente novamente em :seconds segundos.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/pt_BR/pagination.php: -------------------------------------------------------------------------------- 1 | '« Anterior', 17 | 'next' => 'Próximo »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/pt_BR/passwords.php: -------------------------------------------------------------------------------- 1 | 'Sua senha foi redefinida!', 17 | 'sent' => 'Enviamos seu link de redefinição de senha por e-mail!', 18 | 'password' => 'A senha e a confirmação devem combinar e possuir pelo menos seis caracteres.', 19 | 'throttled' => 'Aguarde antes de tentar novamente.', 20 | 'token' => 'Este token de redefinição de senha é inválido.', 21 | 'user' => 'Não encontramos um usuário com esse endereço de e-mail.', 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /lang/tr_TR/auth.php: -------------------------------------------------------------------------------- 1 | 'Bu bilgiler kayıtlarımızla uyuşmuyor.', 17 | 'password' => 'Sağlanan şifre yanlış.', 18 | 'throttle' => 'Çok fazla giriş denemesi. Lütfen :seconds saniye içinde tekrar deneyin.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/tr_TR/pagination.php: -------------------------------------------------------------------------------- 1 | '« Önceki', 17 | 'next' => 'Sonraki »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/tr_TR/passwords.php: -------------------------------------------------------------------------------- 1 | 'Parolanız sıfırlandı!', 17 | 'sent' => 'Parola sıfırlama bağlantınızı e-postayla gönderdik!', 18 | 'password' => 'Şifre ve onay aynı olmalı ve en az altı karakter uzunluğunda olmalıdır.', 19 | 'throttled' => 'Tekrar denemeden önce lütfen bekleyin.', 20 | 'token' => 'Bu parola sıfırlama anahtarı geçersiz.', 21 | 'user' => 'Bu e-posta adresine sahip bir kullanıcı bulamadık.', 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /lang/zh_TW/auth.php: -------------------------------------------------------------------------------- 1 | '您輸入的帳號密碼與系統記錄不符。', 17 | 'password' => '您輸入的密碼不正確。', 18 | 'throttle' => '登入嘗試次數太多,請於 :seconds 秒後再試。', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /lang/zh_TW/pagination.php: -------------------------------------------------------------------------------- 1 | '« 上一頁', 17 | 'next' => '下一頁 »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/zh_TW/passwords.php: -------------------------------------------------------------------------------- 1 | '您的密碼已成功重設!', 17 | 'sent' => '我們已將密碼重設連結寄送至您的電子郵件信箱!', 18 | 'password' => '密碼必須至少包含六個字元,且與確認密碼相符。', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.7", 10 | "@tailwindcss/typography": "^0.5.10", 11 | "autoprefixer": "^10.4.15", 12 | "laravel-vite-plugin": "^1.0.0", 13 | "postcss": "^8.5.4", 14 | "tailwindcss": "^3.4.0", 15 | "vite": "^6.2.6", 16 | "concurrently": "^9.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "exclude": [ 4 | "config" 5 | ], 6 | "rules": { 7 | "fully_qualified_strict_types": false, 8 | "single_line_empty_body": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /public/css/app/panel.css: -------------------------------------------------------------------------------- 1 | .fi-topbar #dashboardAction .fi-btn-label, 2 | .fi-topbar #speedtestAction .fi-btn-label { 3 | display: none; 4 | } 5 | 6 | .fi-fo-field-wrp-helper-text a, 7 | .fi-fo-field-wrp-hint a { 8 | @apply font-bold underline; 9 | } 10 | 11 | @media (min-width: 768px) { 12 | .fi-topbar #dashboardAction .fi-btn-label, 13 | .fi-topbar #speedtestAction .fi-btn-label { 14 | display: block; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Black.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Black.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-BlackItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-BlackItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Bold.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraLight.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraLightItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ExtraLightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ExtraLightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Italic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Italic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Light.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Light.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-LightItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-LightItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Medium.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-MediumItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-MediumItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Regular.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-SemiBoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-SemiBoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Thin.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-Thin.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ThinItalic.woff -------------------------------------------------------------------------------- /public/fonts/inter/Inter-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-ThinItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /public/fonts/inter/Inter.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/fonts/inter/Inter.var.woff2 -------------------------------------------------------------------------------- /public/img/speedtest-tracker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/img/speedtest-tracker-icon.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 21 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/key-value.js: -------------------------------------------------------------------------------- 1 | function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/tags-input.js: -------------------------------------------------------------------------------- 1 | function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/textarea.js: -------------------------------------------------------------------------------- 1 | function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init:function(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight:function(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /public/js/filament/forms/forms.js: -------------------------------------------------------------------------------- 1 | (()=>{function b(n){n.directive("mask",(e,{value:t,expression:u},{effect:f,evaluateLater:c})=>{let r=()=>u,l="";queueMicrotask(()=>{if(["function","dynamic"].includes(t)){let o=c(u);f(()=>{r=a=>{let s;return n.dontAutoEvaluateFunctions(()=>{o(d=>{s=typeof d=="function"?d(a):d},{scope:{$input:a,$money:I.bind({el:e})}})}),s},i(e,!1)})}else i(e,!1);e._x_model&&e._x_model.set(e.value)}),e.addEventListener("input",()=>i(e)),e.addEventListener("blur",()=>i(e,!1));function i(o,a=!0){let s=o.value,d=r(s);if(!d||d==="false")return!1;if(l.length-o.value.length===1)return l=o.value;let g=()=>{l=o.value=p(s,d)};a?k(o,d,()=>{g()}):g()}function p(o,a){if(o==="")return"";let s=h(a,o);return m(a,s)}}).before("model")}function k(n,e,t){let u=n.selectionStart,f=n.value;t();let c=f.slice(0,u),r=m(e,h(e,c)).length;n.setSelectionRange(r,r)}function h(n,e){let t=e,u="",f={9:/[0-9]/,a:/[a-zA-Z]/,"*":/[a-zA-Z0-9]/},c="";for(let r=0;r{let o="",a=0;for(let s=i.length-1;s>=0;s--)i[s]!==p&&(a===3?(o=i[s]+p+o,a=0):o=i[s]+o,a++);return o},c=n.startsWith("-")?"-":"",r=n.replaceAll(new RegExp(`[^0-9\\${e}]`,"g"),""),l=Array.from({length:r.split(e)[0].length}).fill("9").join("");return l=`${c}${f(l,t)}`,u>0&&n.includes(e)&&(l+=`${e}`+"9".repeat(u)),queueMicrotask(()=>{this.el.value.endsWith(e)||this.el.value[this.el.selectionStart-1]===e&&this.el.setSelectionRange(this.el.selectionStart-1,this.el.selectionStart-1)}),l}var v=b;document.addEventListener("alpine:init",()=>{window.Alpine.plugin(v)});})(); 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/vendor/telescope/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexjustesen/speedtest-tracker/c419969e79fd8d8c4ada58a723d2bd06f5245d9f/public/vendor/telescope/favicon.ico -------------------------------------------------------------------------------- /public/vendor/telescope/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=99e99836705c54c9dc04352a9907bc7f", 3 | "/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9", 4 | "/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf" 5 | } 6 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | [x-cloak] { 7 | display: none !important; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /resources/css/panel.css: -------------------------------------------------------------------------------- 1 | .fi-topbar #dashboardAction .fi-btn-label, 2 | .fi-topbar #speedtestAction .fi-btn-label { 3 | display: none; 4 | } 5 | 6 | .fi-fo-field-wrp-helper-text a, 7 | .fi-fo-field-wrp-hint a { 8 | @apply font-bold underline; 9 | } 10 | 11 | @media (min-width: 768px) { 12 | .fi-topbar #dashboardAction .fi-btn-label, 13 | .fi-topbar #speedtestAction .fi-btn-label { 14 | display: block; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/views/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | @livewire(\App\Filament\Widgets\StatsOverviewWidget::class) 5 |
6 | 7 | @isset($latestResult) 8 |
9 | Latest result: 10 |
11 | @endisset 12 | 13 |
14 | @livewire(\App\Filament\Widgets\RecentDownloadChartWidget::class) 15 |
16 | 17 |
18 | @livewire(\App\Filament\Widgets\RecentUploadChartWidget::class) 19 |
20 | 21 |
22 | @livewire(\App\Filament\Widgets\RecentPingChartWidget::class) 23 |
24 | 25 |
26 | @livewire(\App\Filament\Widgets\RecentJitterChartWidget::class) 27 |
28 | 29 |
30 | @livewire(\App\Filament\Widgets\RecentDownloadLatencyChartWidget::class) 31 |
32 | 33 |
34 | @livewire(\App\Filament\Widgets\RecentUploadLatencyChartWidget::class) 35 |
36 | 37 |
38 | 39 |
40 | -------------------------------------------------------------------------------- /resources/views/discord/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | **Speedtest Completed - #{{ $id }}** 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}**. 4 | 5 | - **Server name:** {{ $serverName }} 6 | - **Server ID:** {{ $serverId }} 7 | - **ISP:** {{ $isp }} 8 | - **Ping:** {{ $ping }} 9 | - **Download:** {{ $download }} 10 | - **Upload:** {{ $upload }} 11 | - **Packet Loss:** {{ $packetLoss }} **%** 12 | - **Ookla Speedtest:** {{ $speedtest_url }} 13 | - **URL:** {{ $url }} 14 | -------------------------------------------------------------------------------- /resources/views/discord/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | **Speedtest Threshold Breached - #{{ $id }}** 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 4 | 5 | @foreach ($metrics as $item) 6 | - **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }} 7 | @endforeach 8 | - **Ookla Speedtest:** {{ $speedtest_url }} 9 | - **URL:** {{ $url }} 10 | -------------------------------------------------------------------------------- /resources/views/emails/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | 2 | # Speedtest Completed - #{{ $id }} 3 | 4 | A new speedtest was completed using **{{ $service }}**. 5 | 6 | 7 | | **Metric** | **Value** | 8 | |:------------|---------------------------:| 9 | | Server name | {{ $serverName }} | 10 | | Server ID | {{ $serverId }} | 11 | | ISP | {{ $isp }} | 12 | | Ping | {{ $ping }} | 13 | | Download | {{ $download }} | 14 | | Upload | {{ $upload }} | 15 | | Packet Loss | {{ $packetLoss }} **%** | 16 | 17 | 18 | 19 | 20 | 21 | View Results 22 | 23 | 24 | 25 | View Results on Ookla 26 | 27 | 28 | Thanks,
29 | {{ config('app.name') }} 30 |
31 | -------------------------------------------------------------------------------- /resources/views/emails/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | 2 | # Speedtest Threshold Breached - #{{ $id }} 3 | 4 | A new speedtest was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 5 | 6 | 7 | | **Metric** | **Threshold** | **Value** | 8 | |:-----------|:--------------|----------:| 9 | @foreach ($metrics as $item) 10 | | {{ $item['name'] }} | {{ $item['threshold'] }} | {{ $item['value'] }} | 11 | @endforeach 12 | 13 | 14 | 15 | View Results 16 | 17 | 18 | 19 | View Results on Ookla 20 | 21 | 22 | Thanks,
23 | {{ config('app.name') }} 24 |
25 | -------------------------------------------------------------------------------- /resources/views/emails/test.blade.php: -------------------------------------------------------------------------------- 1 | 2 | This is just a test of the email notification system. 3 | 4 | -------------------------------------------------------------------------------- /resources/views/errors/500.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

500

5 | 6 |

{{ __('Oops, server error!') }}

7 | 8 |

There was an issue, check the logs or view the docs for help.

9 | 10 | 15 | {{ __('Documentation') }} 16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /resources/views/filament/forms/notifications-helptext.blade.php: -------------------------------------------------------------------------------- 1 |
2 |

3 | ⚠️ Deprecated Channels 4 |

5 | 6 |

7 | Database, mail and webhook channels are considered core notification channels. 8 | 9 | All other channels should be considered deprecated and will be replaced by Apprise in a future release. 10 |

11 |
12 | -------------------------------------------------------------------------------- /resources/views/filament/forms/thresholds-helptext.blade.php: -------------------------------------------------------------------------------- 1 |
2 |

3 | 💡 Did you know, when a threshold is triggered it's sent to notification channels. 4 | Each channel can be configured separately to have the notification sent to a specific destination. 5 |

6 |
7 | -------------------------------------------------------------------------------- /resources/views/filament/pages/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Silence is golden --}} 3 | 4 | -------------------------------------------------------------------------------- /resources/views/gotify/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | **Speedtest Completed - #{{ $id }}** 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}**. 4 | 5 | - **Server name:** {{ $serverName }} 6 | - **Server ID:** {{ $serverId }} 7 | - **ISP:** {{ $isp }} 8 | - **Ping:** {{ $ping }} 9 | - **Download:** {{ $download }} 10 | - **Upload:** {{ $upload }} 11 | - **Packet Loss:** {{ $packetLoss }} **%** 12 | - **Ookla Speedtest:** [{{ $speedtest_url }}]({{ $speedtest_url }}) 13 | - **URL:** [{{ $url }}]({{ $url }}) 14 | -------------------------------------------------------------------------------- /resources/views/gotify/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | **Speedtest Threshold Breached - #{{ $id }}** 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 4 | 5 | @foreach ($metrics as $item) 6 | - {{ $item['name'] }} {{ $item['threshold'] }}: **{{ $item['value'] }}** 7 | @endforeach 8 | - **Ookla Speedtest:** [{{ $speedtest_url }}]({{ $speedtest_url }}) 9 | - **URL:** [{{ $url }}]({{ $url }}) 10 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $title }} - {{ config('app.name') }} 8 | 9 | 10 | {{-- Fonts --}} 11 | 12 | 13 | {{-- Styles --}} 14 | @filamentStyles 15 | @vite('resources/css/app.css') 16 | 17 | 29 | 30 | 31 |
32 |
33 |
34 |

{{ $title ?? 'Page Title' }} - {{ config('app.name') }}

35 |
36 | 37 |
38 | 42 | Admin Panel 43 | 44 |
45 |
46 | 47 | {{ $slot }} 48 |
49 | 50 | {{-- Scripts --}} 51 | @filamentScripts 52 | 53 | 54 | -------------------------------------------------------------------------------- /resources/views/layouts/debug.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Timezone - {{ config('app.name') }} 8 | 9 | 10 | {{-- Fonts --}} 11 | 12 | 13 | {{-- Styles --}} 14 | @filamentStyles 15 | @vite('resources/css/app.css') 16 | 17 | 18 |
19 | @if (isset($header)) 20 | {{ $header }} 21 | @endif 22 | 23 | {{ $slot }} 24 |
25 | 26 | {{-- Scripts --}} 27 | @filamentScripts 28 | 29 | 30 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ $title }} - {{ config('app.name') }} 8 | 9 | 10 | {{-- Fonts --}} 11 | 12 | 13 | {{-- Styles --}} 14 | @filamentStyles 15 | @vite('resources/css/app.css') 16 | 17 | 29 | 30 | 31 | {{ $slot }} 32 | 33 | {{-- Scripts --}} 34 | @filamentScripts 35 | 36 | 37 | -------------------------------------------------------------------------------- /resources/views/livewire/topbar/run-speedtest-action.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $this->dashboard }} 4 | 5 | {{ $this->speedtestAction }} 6 |
7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /resources/views/ntfy/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | Speedtest Completed - #{{ $id }} 2 | 3 | A new speedtest on {{ config('app.name') }} was completed using {{ $service }}. 4 | 5 | Server name: {{ $serverName }} 6 | Server ID: {{ $serverId }} 7 | ISP: {{ $isp }} 8 | Ping: {{ $ping }} 9 | Download: {{ $download }} 10 | Upload: {{ $upload }} 11 | Packet Loss: {{ $packetLoss }} % 12 | Ookla Speedtest: {{ $speedtest_url }} 13 | URL: {{ $url }} 14 | -------------------------------------------------------------------------------- /resources/views/ntfy/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | Speedtest Threshold Breached - #{{ $id }} 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 4 | 5 | @foreach ($metrics as $item) 6 | - {{ $item['name'] }} {{ $item['threshold'] }}: {{ $item['value'] }} 7 | @endforeach 8 | - Ookla Speedtest: {{ $speedtest_url }} 9 | - URL: {{ $url }} 10 | -------------------------------------------------------------------------------- /resources/views/pushover/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | Speedtest Completed - #{{ $id }} 2 | 3 | A new speedtest on {{ config('app.name') }} was completed using {{ $service }}. 4 | 5 | - Server name: {{ $serverName }} 6 | - Server ID: {{ $serverId }} 7 | - ISP: {{ $isp }} 8 | - Ping: {{ $ping }} 9 | - Download: {{ $download }} 10 | - Upload: {{ $upload }} 11 | - Packet Loss: {{ $packetLoss }} % 12 | - Ookla Speedtest: {{ $speedtest_url }} 13 | - URL: {{ $url }} 14 | -------------------------------------------------------------------------------- /resources/views/pushover/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | Speedtest Threshold Breached - #{{ $id }} 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 4 | 5 | @foreach ($metrics as $item) 6 | - {{ $item['name'] }} {{ $item['threshold'] }}: {{ $item['value'] }} 7 | @endforeach 8 | - Ookla Speedtest: {{ $speedtest_url }} 9 | - URL: {{ $url }} 10 | -------------------------------------------------------------------------------- /resources/views/slack/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | *Speedtest Completed - #{{ $id }}* 2 | 3 | A new speedtest on *{{ config('app.name') }}* was completed using *{{ $service }}*. 4 | 5 | - *Server name:* {{ $serverName }} 6 | - *Server ID:* {{ $serverId }} 7 | - *ISP:* {{ $isp }} 8 | - *Ping:* {{ $ping }} 9 | - *Download:* {{ $download }} 10 | - *Upload:* {{ $upload }} 11 | - *Packet Loss:* {{ $packetLoss }} *%* 12 | - *Ookla Speedtest:* {{ $speedtest_url }} 13 | - *URL:* {{ $url }} 14 | -------------------------------------------------------------------------------- /resources/views/slack/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | **Speedtest Threshold Breached - #{{ $id }}** 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 4 | 5 | @foreach ($metrics as $item) 6 | - *{{ $item['name'] }}* {{ $item['threshold'] }}: {{ $item['value'] }} 7 | @endforeach 8 | - *Ookla Speedtest:* {{ $speedtest_url }} 9 | - *URL:* {{ $url }} 10 | -------------------------------------------------------------------------------- /resources/views/telegram/speedtest-completed.blade.php: -------------------------------------------------------------------------------- 1 | *Speedtest Completed - #{{ $id }}* 2 | 3 | A new speedtest on *{{ config('app.name') }}* was completed using *{{ $service }}*. 4 | 5 | - *Server name:* {{ $serverName }} 6 | - *Server ID:* {{ $serverId }} 7 | - **ISP:** {{ $isp }} 8 | - *Ping:* {{ $ping }} 9 | - *Download:* {{ $download }} 10 | - *Upload:* {{ $upload }} 11 | - **Packet Loss:** {{ $packetLoss }}**%** 12 | - **Ookla Speedtest:** {{ $speedtest_url }} 13 | - **URL:** {{ $url }} 14 | -------------------------------------------------------------------------------- /resources/views/telegram/speedtest-threshold.blade.php: -------------------------------------------------------------------------------- 1 | **Speedtest Threshold Breached - #{{ $id }}** 2 | 3 | A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. 4 | 5 | @foreach ($metrics as $item) 6 | - **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }} 7 | @endforeach 8 | - **Ookla Speedtest:** {{ $speedtest_url }} 9 | - **URL:** {{ $url }} 10 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | json([ 13 | 'message' => 'Speedtest Tracker is running!', 14 | ]); 15 | })->name('healthcheck'); 16 | 17 | /** 18 | * This route provides backwards compatibility from https://github.com/henrywhitaker3/Speedtest-Tracker 19 | * for Homepage and Organizr dashboards which expects the returned 20 | * download and upload values in mbits. 21 | * 22 | * @deprecated 23 | */ 24 | Route::get('/speedtest/latest', GetLatestController::class) 25 | ->name('speedtest.latest'); 26 | 27 | Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () { 28 | require __DIR__.'/api/v1/routes.php'; 29 | }); 30 | -------------------------------------------------------------------------------- /routes/api/v1/routes.php: -------------------------------------------------------------------------------- 1 | name('api.v1.')->group(function () { 12 | Route::get('/results', ListResults::class) 13 | ->name('results.list'); 14 | 15 | Route::get('/results/latest', LatestResult::class) 16 | ->name('results.latest'); 17 | 18 | Route::get('/results/{result}', ShowResult::class) 19 | ->name('results.show'); 20 | 21 | Route::post('/speedtests/run', RunSpeedtest::class) 22 | ->name('speedtests.run'); 23 | 24 | Route::get('/ookla/list-servers', ListSpeedtestServers::class) 25 | ->name('ookla.list-servers'); 26 | 27 | Route::get('/stats', Stats::class) 28 | ->name('stats'); 29 | }); 30 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | daily() 11 | ->when(function () { 12 | return config('speedtest.prune_results_older_than') > 0; 13 | }); 14 | 15 | /** 16 | * Nightly maintenance 17 | */ 18 | Schedule::daily() 19 | ->group(function () { 20 | Schedule::command('queue:prune-batches --hours=48'); 21 | Schedule::command('queue:prune-failed --hours=48'); 22 | }); 23 | 24 | /** 25 | * Check for scheduled speedtests. 26 | */ 27 | Schedule::everyMinute() 28 | ->group(function () { 29 | Schedule::call(fn () => CheckForScheduledSpeedtests::run()); 30 | }); 31 | -------------------------------------------------------------------------------- /routes/test.php: -------------------------------------------------------------------------------- 1 | group(function () { 6 | // silence is golden 7 | }); 8 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | middleware(['getting-started', 'public-dashboard']) 19 | ->name('home'); 20 | 21 | Route::get('/getting-started', [PagesController::class, 'gettingStarted']) 22 | ->name('getting-started'); 23 | 24 | Route::redirect('/login', '/admin/login') 25 | ->name('login'); 26 | 27 | if (app()->isLocal()) { 28 | require __DIR__.'/test.php'; 29 | } 30 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | import presets from './vendor/filament/support/tailwind.config.preset'; 4 | import typography from '@tailwindcss/typography'; 5 | 6 | /** @type {import('tailwindcss').Config} */ 7 | export default { 8 | presets: [presets], 9 | 10 | content: [ 11 | // Core 12 | './resources/views/**/*.blade.php', 13 | 14 | // Filament 15 | './app/Filament/**/*.php', 16 | './vendor/filament/**/*.blade.php', 17 | ], 18 | 19 | theme: { 20 | extend: { 21 | fontFamily: { 22 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 23 | }, 24 | }, 25 | }, 26 | 27 | plugins: [ 28 | forms, 29 | typography, 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /tests/Feature/FeatureTestCase.php: -------------------------------------------------------------------------------- 1 | extend(Tests\Feature\FeatureTestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | pest()->extend(Tests\Unit\UnitTestCase::class) 19 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 20 | ->in('Unit'); 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Expectations 25 | |-------------------------------------------------------------------------- 26 | | 27 | | When you're writing tests, you often need to check that values meet certain conditions. The 28 | | "expect()" function gives you access to a set of "expectations" methods that you can use 29 | | to assert different things. Of course, you may extend the Expectation API at any time. 30 | | 31 | */ 32 | 33 | expect()->extend('toBeOne', function () { 34 | return $this->toBe(1); 35 | }); 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Functions 40 | |-------------------------------------------------------------------------- 41 | | 42 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 43 | | project that you don't want to repeat in every file. Here you can also expose helpers as 44 | | global functions to help you to reduce the number of lines of code in your test files. 45 | | 46 | */ 47 | 48 | function something() 49 | { 50 | // .. 51 | } 52 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | create(); 12 | 13 | expect(Result::count())->toBe(1); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/Unit/UnitTestCase.php: -------------------------------------------------------------------------------- 1 |