diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7ec51536b..04653336d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,8 @@ // https://aka.ms/devcontainer.json { - "name": "Existing Docker Compose (Extend)", + "name": "Speedtest Tracker Dev Environment", "dockerComposeFile": [ - "../docker-compose.yml" + "../compose.yaml" ], "service": "laravel.test", "workspaceFolder": "/var/www/html", @@ -20,7 +20,7 @@ } }, "remoteUser": "sail", - "postCreateCommand": "chown -R 1000:1000 /var/www/html 2>/dev/null || true" + "postCreateCommand": "composer install && npm install && npm run build && touch database/database.sqlite && php artisan migrate:fresh --force" // "forwardPorts": [], // "runServices": [], // "shutdownAction": "none", diff --git a/.env.example b/.env.example index 1a762a1ce..b09ac0014 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,11 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=sqlite +#DB_HOST= +#DB_PORT= +#DB_DATABASE= +#DB_USERNAME= +#DB_PASSWORD= SESSION_DRIVER=cookie SESSION_LIFETIME=10080 @@ -42,3 +47,7 @@ MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="Speedtest Tracker" VITE_APP_NAME="${APP_NAME}" + +# For the Dev Container +# WWWUSER=1000 +# WWWGROUP=1000 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6070ad3fd..ab4407dcc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,66 +2,100 @@ name: Bug Report description: Use this template to report a bug or issue. title: "[Question] " labels: ["question", "needs review"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to report this issue! We appreciate your help in improving the project. If this report is confirmed as a bug, we’ll update its type accordingly. - Please note: - - For **feature requests or changes**, use the [feature request form](https://github.com/alexjustesen/speedtest-tracker/issues/new?template=feature_request.yml). - - For **general questions**, **setup or configuration help**, or if you’re not sure this is a bug, please use **[GitHub Discussions](https://github.com/alexjustesen/speedtest-tracker/discussions)** instead. +body: - type: checkboxes + id: terms attributes: - label: Pre-work + label: Welcome! description: | - Before opening an issue make sure you've checked the resources below first, any issues that could have been solved by reading the docs or existing issues will be closed. + The issue tracker is for reporting bugs and feature requests only. For end-user related support questions, please use the **[GitHub Discussions](https://github.com/alexjustesen/speedtest-tracker/discussions)** instead + + Please note: + - For translation-related issues or requests, please use the [Crowdin project](https://crowdin.com/project/speedtest-tracker). + - Any issues that can be resolved by consulting the documentation or by reviewing existing open or closed issues will be closed. + - We only support installations that follow the methods described in the documentation. Installations using third-party or undocumented methods are not supported by the project. + options: - - label: I have read the [docs](https://docs.speedtest-tracker.dev). + - label: I have read the [documentation](https://docs.speedtest-tracker.dev) and my problem was not listed in the help section. + required: true + - label: I have searched open and closed issues and my problem was not mentioned before. required: true - - label: I have searched open and closed issues. + - label: I have verified I am using the latest version available. You can check the latest release [here](https://github.com/alexjustesen/speedtest-tracker/releases). required: true - label: I agree to follow this project's [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md). required: true + - type: textarea id: description attributes: - label: Description - description: Explain the issue you experienced, please be clear and concise. - placeholder: I went to the coffee pot and it was empty. + label: What did you do? + description: | + How to write a good bug report? + + - Respect the issue template as much as possible. + - The title should be short and descriptive. + - Explain the conditions which led you to report this issue: the context. + - The context should lead to something, a problem that you’re facing. + - Remain clear and concise. + - Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown) validations: required: true + - type: textarea id: expected-behavior attributes: label: Expected Behavior - description: In a perfect world, what should have happened? + description: | + In a perfect world, what should have happened? + **Important:** Be specific. Vague descriptions like "it should work" are not helpful. placeholder: When I got to the coffee pot, it should have been full. validations: required: true + - type: textarea id: steps-to-reproduce attributes: label: Steps to Reproduce - description: Describe how to reproduce the issue in repeatable steps. + description: | + Provide detailed, numbered steps that someone else can follow to reproduce the issue. + **Important:** Vague descriptions like "it doesn't work" or "it's broken" will result in the issue being closed. + Include specific actions, URLs, button clicks, and any relevant data or configuration. placeholder: | 1. Go to the coffee pot. 2. Make more coffee. 3. Pour it into a cup. + 4. Observe that the cup is empty instead of full. validations: required: true + - type: dropdown id: deployment-environment attributes: label: Deployment Environment - description: How did you deploy the application? + description: How did you deploy the application? Only supported deployment methods are listed. options: - Docker Compose - Docker Run - - Other default: 0 validations: required: true + + - type: textarea + id: environment-configuration + attributes: + label: What is your environment & configuration? + description: Please add your docker compose file or docker run command used to deploy the application. + placeholder: Add information here. + value: | + ```yaml + # (paste your configuration here) + ``` + + Add more configuration information here. + validations: + required: true + - type: textarea id: application-information attributes: @@ -70,6 +104,7 @@ body: render: json validations: required: true + - type: input id: browsers attributes: @@ -77,9 +112,12 @@ body: placeholder: Chrome, Firefox, Safari, etc. validations: required: true + - type: textarea id: logs attributes: label: Logs - description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. If you are unsure which logs to include, include all logs. You can get the logs by running `docker logs `. render: shell + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 678cea79e..25fd46494 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: Translations + url: https://crowdin.com/project/speedtest-tracker + about: Please report translation issues and request new translations here. - name: GitHub Community Support url: https://github.com/orgs/community/discussions about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 439014ec8..bdd144689 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,21 +2,28 @@ name: Feature Request description: Use this template for requesting a new feature or change. title: "[Feature] " labels: ["feature", "needs review"] + body: - - type: markdown - attributes: - value: | - You should only use this form to request a change or new feature, to report a bug or issue use the [bug report form](https://github.com/alexjustesen/speedtest-tracker). - type: checkboxes attributes: - label: Pre-work + label: Welcome! + description: | + The issue tracker is for reporting bugs and feature requests only. For end-user related support questions, please use the **[GitHub Discussions](https://github.com/alexjustesen/speedtest-tracker/discussions)** instead + + Please note: + - For **Bug reports**, use the [Bug Form](https://github.com/alexjustesen/speedtest-tracker/issues/new?template=bug_report.yml). + - Any requests for new translations should be requested within the [crowdin project](https://crowdin.com/project/speedtest-tracker). + options: - - label: I have searched open and closed feature request to make sure this or similar feature request does not already exist. + - label: I have searched open and closed feature requests to make sure this or similar feature request does not already exist. + required: true + - label: I have reviewed the [Milestones](https://github.com/alexjustesen/speedtest-tracker/milestones) to ensure that this feature request, or a similar one, has not already been proposed. required: true - - label: I have reviewed the [milestones](https://github.com/alexjustesen/speedtest-tracker/milestones) to ensure that this feature request, or a similar one, has not already been proposed. + - label: This is a feature request, not a bug report or support question. required: true - - label: I agree to follow this project's [Code of Conduct](). + - label: I agree to follow this project's [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md). required: true + - type: dropdown id: idea-section attributes: @@ -27,14 +34,23 @@ body: - Notifications - Speedtest - Web UI/UX + - Other default: 0 validations: required: true + - type: textarea id: description attributes: label: Description - description: Describe the solution or feature you'd like, you should also mention if this solves a problem. - placeholder: Be sure to keep it clear and concise. + description: | + Describe the solution or feature you'd like. Explain what problem this solves or what value it adds. + **Important:** Be specific and detailed. Vague requests like "make it better" will be closed. + placeholder: | + Example: + - What is the feature? + - What problem does it solve? + - How should it work? + - Why would this be valuable? validations: required: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index c51b1a6c9..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,410 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.4.14 -- filament/filament (FILAMENT) - v4 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- laravel/sanctum (SANCTUM) - v4 -- livewire/livewire (LIVEWIRE) - v3 -- laravel/mcp (MCP) - v0 -- laravel/pint (PINT) - v1 -- laravel/sail (SAIL) - v1 -- laravel/telescope (TELESCOPE) - v5 -- pestphp/pest (PEST) - v3 -- phpunit/phpunit (PHPUNIT) - v11 -- tailwindcss (TAILWINDCSS) - v4 - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v12 rules === - -## Laravel 12 - -- Use the `search-docs` tool to get version specific documentation. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== livewire/core rules === - -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - - -=== livewire/v3 rules === - -## Livewire 3 - -### Key Changes From Livewire 2 -- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. - - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). - - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). - - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). - -### New Directives -- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. - -### Alpine -- Alpine is now included with Livewire, don't manually include Alpine.js. -- Plugins included with Alpine: persist, intersect, collapse, and focus. - -### Lifecycle Hooks -- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: - - -document.addEventListener('livewire:init', function () { - Livewire.hook('request', ({ fail }) => { - if (fail && fail.status === 419) { - alert('Your session expired'); - } - }); - - Livewire.hook('message.failed', (message, component) => { - console.error(message); - }); -}); - - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== pest/core rules === - -## Pest - -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. - -@theme { - --color-brand: oklch(0.72 0.11 178); -} - - -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - @tailwind base; - - @tailwind components; - - @tailwind utilities; - + @import "tailwindcss"; - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. - - -=== tightenco/duster rules === - -## Duster Code Formatter - -- You must run `vendor/bin/duster fix --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Duster wraps Laravel Pint and other formatters, so never run Pint directly. Always prefer Duster for formatting tasks. -
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..f6e40687f --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,27 @@ +name: Trigger Docker Image Build + +on: + release: + types: [published] + +jobs: + trigger-docker-build: + runs-on: ubuntu-24.04 + + steps: + - name: Generate GitHub App token + id: generate_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: alexjustesen + repositories: docker-speedtest-tracker + + - name: Trigger docker-speedtest-tracker build + uses: peter-evans/repository-dispatch@v4 + with: + token: ${{ steps.generate_token.outputs.token }} + repository: alexjustesen/docker-speedtest-tracker + event-type: speedtest-tracker-release + client-payload: '{"tag_name": "${{ github.event.release.tag_name }}"}' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcbf58101..1e6db763d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,26 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - name: "duster" - uses: tighten/duster-action@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - args: lint --using=pint -v + php-version: '8.4' + + - name: Cache Composer dependencies + uses: actions/cache@v5 + with: + path: vendor + key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + composer-${{ runner.os }}- + + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Run Pint + run: vendor/bin/pint --test test-mariadb-11: needs: lint-app @@ -35,7 +49,7 @@ jobs: options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -43,7 +57,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -51,7 +65,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -93,7 +107,7 @@ jobs: options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -101,7 +115,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -109,7 +123,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -151,7 +165,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -159,7 +173,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -167,7 +181,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -209,7 +223,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -217,7 +231,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -225,7 +239,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -267,7 +281,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -275,7 +289,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -283,7 +297,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -325,7 +339,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -333,7 +347,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -341,7 +355,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -383,7 +397,7 @@ jobs: options: --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -391,7 +405,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -399,7 +413,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} @@ -432,7 +446,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -440,7 +454,7 @@ jobs: php-version: '8.4' - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: vendor key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -448,7 +462,7 @@ jobs: composer-${{ runner.os }}- - name: Cache NPM dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} diff --git a/.github/workflows/update-openapi.yml b/.github/workflows/update-openapi.yml deleted file mode 100644 index 5aab78ef0..000000000 --- a/.github/workflows/update-openapi.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Update OpenAPI JSON - -on: - push: - paths: - - 'app/Http/Controllers/Api/**/*.php' - - 'app/OpenApi/**/*.php' - -jobs: - update-openapi: - runs-on: ubuntu-24.04 - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - - name: Install dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress - - - name: Generate OpenAPI JSON - run: ./vendor/bin/openapi ./app/Http/Controllers/Api ./app/OpenApi -o openapi_temp.json -f json - - - name: Commit OpenAPI JSON if changed - run: | - if ! diff -q openapi.json openapi_temp.json; then - mv openapi_temp.json openapi.json - git config user.name "GitHub Action" - git config user.email "actions@github.com" - git add openapi.json - git commit -m "Update OpenAPI JSON" - git push - fi diff --git a/.github/workflows/validate-openapi.yml b/.github/workflows/validate-openapi.yml new file mode 100644 index 000000000..45226a32c --- /dev/null +++ b/.github/workflows/validate-openapi.yml @@ -0,0 +1,49 @@ +name: Validate OpenAPI JSON + +on: + pull_request: + paths: + - 'app/Http/Controllers/Api/**/*.php' + - 'app/OpenApi/**/*.php' + +jobs: + update-openapi: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Install dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress + + # Generate OpenAPI file into a temp location + - name: Generate OpenAPI JSON + run: ./vendor/bin/openapi ./app/Http/Controllers/Api ./app/OpenApi -o openapi_temp.json -f json + + # Compare the generated file to the committed file + - name: Validate OpenAPI JSON is up to date + run: | + if ! diff -q openapi.json openapi_temp.json; then + echo "❌ OpenAPI documentation is out of sync!" + echo "" + echo "API code was modified but openapi.json was NOT updated." + echo "" + echo "Please regenerate the OpenAPI JSON and commit it:" + echo " ./vendor/bin/openapi ./app/Http/Controllers/Api ./app/OpenApi -o openapi.json -f json" + echo "" + exit 1 + fi + + - name: ✓ OpenAPI is up to date + if: success() + run: echo "✅ OpenAPI documentation matches the committed version!" diff --git a/.gitignore b/.gitignore index d4c8d324e..e86852207 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,11 @@ yarn-error.log /.nova /.phpunit.cache /.vscode + +# AI and Boost assets +/.claude +/.github/copilot-instructions.md +/.mcp.json +/boost.json +/AGENTS.md +/CLAUDE.md diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index fd4766a41..000000000 --- a/.mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mcpServers": { - "laravel-boost": { - "command": "./vendor/bin/sail", - "args": [ - "artisan", - "boost:mcp" - ] - } - } -} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index c51b1a6c9..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,410 +0,0 @@ - -=== foundation rules === - -# Laravel Boost Guidelines - -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. - -## Foundational Context -This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. - -- php - 8.4.14 -- filament/filament (FILAMENT) - v4 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- laravel/sanctum (SANCTUM) - v4 -- livewire/livewire (LIVEWIRE) - v3 -- laravel/mcp (MCP) - v0 -- laravel/pint (PINT) - v1 -- laravel/sail (SAIL) - v1 -- laravel/telescope (TELESCOPE) - v5 -- pestphp/pest (PEST) - v3 -- phpunit/phpunit (PHPUNIT) - v11 -- tailwindcss (TAILWINDCSS) - v4 - -## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. -- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. -- Check for existing components to reuse before writing a new one. - -## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. - -## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. -- Do not change the application's dependencies without approval. - -## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. - -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. - -## Documentation Files -- You must only create documentation files if explicitly requested by the user. - - -=== boost rules === - -## Laravel Boost -- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. - -## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. - -## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. - -## Tinker / Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. -- Use the `database-query` tool when you only need to read from the database. - -## Reading Browser Logs With the `browser-logs` Tool -- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. -- Only recent browser logs will be useful - ignore old logs. - -## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. -- Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. - -### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms - - -=== php rules === - -## PHP - -- Always use curly braces for control structures, even if it has one line. - -### Constructors -- Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. - -### Type Declarations -- Always use explicit return type declarations for methods and functions. -- Use appropriate PHP type hints for method parameters. - - -protected function isAccessible(User $user, ?string $path = null): bool -{ - ... -} - - -## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. - -## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. - -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. - - -=== laravel/core rules === - -## Do Things the Laravel Way - -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. -- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. - -### Database -- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries -- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. -- Generate code that prevents N+1 query problems by using eager loading. -- Use Laravel's query builder for very complex database operations. - -### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. - -### APIs & Eloquent Resources -- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. - -### Controllers & Validation -- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. -- Check sibling Form Requests to see if the application uses array or string based validation rules. - -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. - -### Authentication & Authorization -- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). - -### URL Generation -- When generating links to other pages, prefer named routes and the `route()` function. - -### Configuration -- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. - -### Testing -- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. -- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. - -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. - - -=== laravel/v12 rules === - -## Laravel 12 - -- Use the `search-docs` tool to get version specific documentation. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. - -### Database -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - - -=== livewire/core rules === - -## Livewire Core -- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests. -- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components -- State should live on the server, with the UI reflecting it. -- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions. - -## Livewire Best Practices -- Livewire components require a single root element. -- Use `wire:loading` and `wire:dirty` for delightful loading states. -- Add `wire:key` in loops: - - ```blade - @foreach ($items as $item) -
- {{ $item->name }} -
- @endforeach - ``` - -- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: - - - public function mount(User $user) { $this->user = $user; } - public function updatedSearch() { $this->resetPage(); } - - - -## Testing Livewire - - - Livewire::test(Counter::class) - ->assertSet('count', 0) - ->call('increment') - ->assertSet('count', 1) - ->assertSee(1) - ->assertStatus(200); - - - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - - -=== livewire/v3 rules === - -## Livewire 3 - -### Key Changes From Livewire 2 -- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions. - - Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default. - - Components now use the `App\Livewire` namespace (not `App\Http\Livewire`). - - Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`). - - Use the `components.layouts.app` view as the typical layout path (not `layouts.app`). - -### New Directives -- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples. - -### Alpine -- Alpine is now included with Livewire, don't manually include Alpine.js. -- Plugins included with Alpine: persist, intersect, collapse, and focus. - -### Lifecycle Hooks -- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring: - - -document.addEventListener('livewire:init', function () { - Livewire.hook('request', ({ fail }) => { - if (fail && fail.status === 419) { - alert('Your session expired'); - } - }); - - Livewire.hook('message.failed', (message, component) => { - console.error(message); - }); -}); - - - -=== pint/core rules === - -## Laravel Pint Code Formatter - -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - - -=== pest/core rules === - -## Pest - -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - - - -=== tailwindcss/core rules === - -## Tailwind Core - -- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own. -- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..) -- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically -- You can use the `search-docs` tool to get exact examples from the official documentation when needed. - -### Spacing -- When listing items, use gap utilities for spacing, don't use margins. - - -
-
Superior
-
Michigan
-
Erie
-
-
- - -### Dark Mode -- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. - - -=== tailwindcss/v4 rules === - -## Tailwind 4 - -- Always use Tailwind CSS v4 - do not use the deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. -- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. - -@theme { - --color-brand: oklch(0.72 0.11 178); -} - - -- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: - - - - @tailwind base; - - @tailwind components; - - @tailwind utilities; - + @import "tailwindcss"; - - - -### Replaced Utilities -- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement. -- Opacity values are still numeric. - -| Deprecated | Replacement | -|------------+--------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. - - -=== tightenco/duster rules === - -## Duster Code Formatter - -- You must run `vendor/bin/duster fix --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Duster wraps Laravel Pint and other formatters, so never run Pint directly. Always prefer Duster for formatting tasks. -
diff --git a/README.md b/README.md index af39aca34..b5c8275f7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Speedtest Tracker is a self-hosted application that monitors the performance and - **Detailed Metrics**: Capture download and upload speeds, ping, packet loss and more. - **Historical Data**: View historical data and trends to identify patterns and issues with your internet connection. - **Notifications**: Receive notifications when your internet performance drops below a certain threshold. +- **Multi-Language Support**: Available in multiple languages with community translations via [Crowdin](https://crowdin.com/project/speedtest-tracker). ## Getting Started @@ -20,4 +21,12 @@ Speedtest Tracker is containerized so you can run it anywhere you run your conta - [Notifications](https://docs.speedtest-tracker.dev/settings/notifications) channels alert you when issues happen. - [Frequently Asked Questions](https://docs.speedtest-tracker.dev/help/faqs) are common questions that can help you resolve issues. +## Translations + +Speedtest Tracker supports multiple languages thanks to our community translators! + +**Request a new language**: Visit our [Crowdin project](https://crowdin.com/project/speedtest-tracker) and request a new language directly in crowdin. + +**Help translate**: Join our [Crowdin project](https://crowdin.com/project/speedtest-tracker) to contribute translations in your language. All translation levels are welcome! + [![Star History Chart](https://api.star-history.com/svg?repos=alexjustesen/speedtest-tracker&type=Date)](https://star-history.com/#alexjustesen/speedtest-tracker&Date) diff --git a/app/Actions/CheckInternetConnection.php b/app/Actions/CheckInternetConnection.php deleted file mode 100644 index 22e26f78d..000000000 --- a/app/Actions/CheckInternetConnection.php +++ /dev/null @@ -1,33 +0,0 @@ -timeout(5) - ->get(config('speedtest.checkinternet_url')); - - if (! $response->ok()) { - return false; - } - - return Str::trim($response->body()); - } catch (Throwable $e) { - Log::error('Failed to connect to the internet.', [$e->getMessage()]); - - return false; - } - } -} diff --git a/app/Actions/GetExternalIpAddress.php b/app/Actions/GetExternalIpAddress.php index 6a4d0b114..d47cc0a6f 100644 --- a/app/Actions/GetExternalIpAddress.php +++ b/app/Actions/GetExternalIpAddress.php @@ -12,18 +12,28 @@ class GetExternalIpAddress { use AsAction; - public function handle(): bool|string + public function handle(?string $url = null): array { + $url = $url ?? config('speedtest.preflight.external_ip_url'); + try { $response = Http::retry(3, 100) ->timeout(5) - ->get(url: 'https://icanhazip.com/'); + ->get(url: $url); } catch (Throwable $e) { - Log::error('Failed to fetch external IP address.', [$e->getMessage()]); + $message = sprintf('Failed to fetch external IP address from "%s". See the logs for more details.', $url); + + Log::error($message, [$e->getMessage()]); - return false; + return [ + 'ok' => false, + 'body' => $message, + ]; } - return Str::trim($response->body()); + return [ + 'ok' => $response->ok(), + 'body' => Str::of($response->body())->trim()->toString(), + ]; } } diff --git a/app/Actions/Librespeed/RunSpeedtest.php b/app/Actions/Librespeed/RunSpeedtest.php new file mode 100644 index 000000000..4ce8cfb99 --- /dev/null +++ b/app/Actions/Librespeed/RunSpeedtest.php @@ -0,0 +1,46 @@ +server->url' => $server, + 'service' => ResultService::Librespeed, + 'status' => ResultStatus::Waiting, + 'scheduled' => $isScheduled, + 'dispatched_by' => $dispatchedBy, + ]); + + SpeedtestWaiting::dispatch($result); + + // TODO: Implement Librespeed speedtest job batching + + // Bus::batch([ + // [ + // new StartSpeedtestJob($result), + // new CheckForInternetConnectionJob($result), + // new SkipSpeedtestJob($result), + // new SelectSpeedtestServerJob($result), + // new RunSpeedtestJob($result), + // new BenchmarkSpeedtestJob($result), + // new CompleteSpeedtestJob($result), + // ], + // ])->catch(function (Batch $batch, ?Throwable $e) { + // Log::error(sprintf('Speedtest batch "%s" failed for an unknown reason.', $batch->id)); + // })->name('Ookla Speedtest')->dispatch(); + + return $result; + } +} diff --git a/app/Actions/Notifications/SendAppriseTestNotification.php b/app/Actions/Notifications/SendAppriseTestNotification.php new file mode 100644 index 000000000..64bba779b --- /dev/null +++ b/app/Actions/Notifications/SendAppriseTestNotification.php @@ -0,0 +1,100 @@ +title('You need to add Apprise channel URLs!') + ->warning() + ->send(); + + return; + } + + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + + if (empty($appriseUrl)) { + Notification::make() + ->title('Apprise Server URL is not configured') + ->body('Please configure the Apprise Server URL in the settings above.') + ->danger() + ->send(); + + return; + } + + try { + foreach ($channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + continue; + } + + // Use notifyNow() to send synchronously even though notification implements ShouldQueue + // This allows us to catch exceptions and show them in the UI immediately + FacadesNotification::route('apprise_urls', $channelUrl) + ->notifyNow(new TestNotification); + } + } catch (Throwable $e) { + $errorMessage = $this->cleanErrorMessage($e); + + Notification::make() + ->title('Failed to send Apprise test notification') + ->body($errorMessage) + ->danger() + ->send(); + + return; + } + + Notification::make() + ->title('Test Apprise notification sent.') + ->success() + ->send(); + } + + /** + * Clean up error message for display in UI. + */ + protected function cleanErrorMessage(Throwable $e): string + { + $message = $e->getMessage(); + + // Get the full Apprise server URL for error messages + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + + // Handle connection errors - extract just the important part + if (str_contains($message, 'cURL error')) { + if (str_contains($message, 'Could not resolve host')) { + return "Could not connect to Apprise server at {$appriseUrl}"; + } + + if (str_contains($message, 'Connection refused')) { + return "Connection refused by Apprise server at {$appriseUrl}"; + } + + if (str_contains($message, 'Operation timed out')) { + return "Connection to Apprise server at {$appriseUrl} timed out"; + } + + return "Failed to connect to Apprise server at {$appriseUrl}"; + } + + return $message; + } +} diff --git a/app/Actions/Notifications/SendDatabaseTestNotification.php b/app/Actions/Notifications/SendDatabaseTestNotification.php index 6611ad491..86e8e7374 100644 --- a/app/Actions/Notifications/SendDatabaseTestNotification.php +++ b/app/Actions/Notifications/SendDatabaseTestNotification.php @@ -12,17 +12,15 @@ class SendDatabaseTestNotification public function handle(User $user) { - $user->notify( - Notification::make() - ->title('Test database notification received!') - ->body('You say pong') - ->success() - ->toDatabase(), - ); + Notification::make() + ->title(__('settings/notifications.test_notifications.database.received')) + ->body(__('settings/notifications.test_notifications.database.pong')) + ->success() + ->sendToDatabase($user); Notification::make() - ->title('Test database notification sent.') - ->body('I say ping') + ->title(__('settings/notifications.test_notifications.database.sent')) + ->body(__('settings/notifications.test_notifications.database.ping')) ->success() ->send(); } diff --git a/app/Actions/Notifications/SendMailTestNotification.php b/app/Actions/Notifications/SendMailTestNotification.php index df293ef6b..f8220e20f 100644 --- a/app/Actions/Notifications/SendMailTestNotification.php +++ b/app/Actions/Notifications/SendMailTestNotification.php @@ -2,7 +2,7 @@ namespace App\Actions\Notifications; -use App\Mail\Test as TestMail; +use App\Mail\TestMail; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Mail; use Lorisleiva\Actions\Concerns\AsAction; @@ -15,7 +15,7 @@ public function handle(array $recipients) { if (! count($recipients)) { Notification::make() - ->title('You need to add mail recipients!') + ->title(__('settings/notifications.test_notifications.mail.add')) ->warning() ->send(); @@ -28,7 +28,7 @@ public function handle(array $recipients) } Notification::make() - ->title('Test mail notification sent.') + ->title(__('settings/notifications.test_notifications.mail.sent')) ->success() ->send(); } diff --git a/app/Actions/Notifications/SendWebhookTestNotification.php b/app/Actions/Notifications/SendWebhookTestNotification.php index 834a1d0cc..45aa2abc5 100644 --- a/app/Actions/Notifications/SendWebhookTestNotification.php +++ b/app/Actions/Notifications/SendWebhookTestNotification.php @@ -17,7 +17,7 @@ public function handle(array $webhooks) { if (! count($webhooks)) { Notification::make() - ->title('You need to add webhook URLs!') + ->title(__('settings/notifications.test_notifications.webhook.add')) ->warning() ->send(); @@ -32,12 +32,12 @@ public function handle(array $webhooks) ->url($webhook['url']) ->payload([ 'result_id' => Str::uuid(), - 'site_name' => 'Webhook Notification Testing', + 'site_name' => __('settings/notifications.test_notifications.webhook.payload'), 'isp' => $fakeResult->data['isp'], 'ping' => $fakeResult->ping, 'download' => $fakeResult->download, 'upload' => $fakeResult->upload, - 'packetLoss' => $fakeResult->data['packetLoss'], + 'packet_loss' => $fakeResult->data['packetLoss'], 'speedtest_url' => $fakeResult->data['result']['url'], 'url' => url('/admin/results'), ]) @@ -46,7 +46,7 @@ public function handle(array $webhooks) } Notification::make() - ->title('Test webhook notification sent.') + ->title(__('settings/notifications.test_notifications.webhook.sent')) ->success() ->send(); } diff --git a/app/Actions/Ookla/RunSpeedtest.php b/app/Actions/Ookla/RunSpeedtest.php index 0d6a87a87..c3814310e 100644 --- a/app/Actions/Ookla/RunSpeedtest.php +++ b/app/Actions/Ookla/RunSpeedtest.php @@ -23,13 +23,14 @@ class RunSpeedtest { use AsAction; - public function handle(bool $scheduled = false, ?int $serverId = null): mixed + public function handle(bool $scheduled = false, ?int $serverId = null, ?int $dispatchedBy = null): mixed { $result = Result::create([ 'data->server->id' => $serverId, 'service' => ResultService::Ookla, 'status' => ResultStatus::Waiting, 'scheduled' => $scheduled, + 'dispatched_by' => $dispatchedBy, ]); SpeedtestWaiting::dispatch($result); diff --git a/app/Actions/PingHostname.php b/app/Actions/PingHostname.php new file mode 100644 index 000000000..04884fe31 --- /dev/null +++ b/app/Actions/PingHostname.php @@ -0,0 +1,36 @@ +run(); + + $data = $ping->toArray(); + unset($data['raw_output'], $data['lines']); + + Log::debug('Pinged hostname', [ + 'host' => $hostname, + 'data' => $data, + ]); + + return $ping; + } +} diff --git a/app/Enums/ResultService.php b/app/Enums/ResultService.php index b324633a3..39a441d39 100644 --- a/app/Enums/ResultService.php +++ b/app/Enums/ResultService.php @@ -3,15 +3,19 @@ namespace App\Enums; use Filament\Support\Contracts\HasLabel; -use Illuminate\Support\Str; enum ResultService: string implements HasLabel { case Faker = 'faker'; + case Librespeed = 'librespeed'; case Ookla = 'ookla'; public function getLabel(): ?string { - return Str::title($this->name); + return match ($this) { + self::Faker => __('enums.service.faker'), + self::Librespeed => __('enums.service.librespeed'), + self::Ookla => __('enums.service.ookla'), + }; } } diff --git a/app/Enums/ResultStatus.php b/app/Enums/ResultStatus.php index 8b554f426..9ac28e370 100644 --- a/app/Enums/ResultStatus.php +++ b/app/Enums/ResultStatus.php @@ -4,7 +4,6 @@ use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasLabel; -use Illuminate\Support\Str; enum ResultStatus: string implements HasColor, HasLabel { @@ -33,6 +32,15 @@ public function getColor(): ?string public function getLabel(): ?string { - return Str::title($this->name); + return match ($this) { + self::Benchmarking => __('enums.status.benchmarking'), + self::Checking => __('enums.status.checking'), + self::Completed => __('enums.status.completed'), + self::Failed => __('enums.status.failed'), + self::Running => __('enums.status.running'), + self::Started => __('enums.status.started'), + self::Skipped => __('enums.status.skipped'), + self::Waiting => __('enums.status.waiting'), + }; } } diff --git a/app/Enums/UserRole.php b/app/Enums/UserRole.php index 69884c8e8..62e7d57e1 100644 --- a/app/Enums/UserRole.php +++ b/app/Enums/UserRole.php @@ -4,7 +4,6 @@ use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasLabel; -use Illuminate\Support\Str; enum UserRole: string implements HasColor, HasLabel { @@ -21,6 +20,9 @@ public function getColor(): ?string public function getLabel(): ?string { - return Str::title($this->name); + return match ($this) { + self::Admin => __('general.admin'), + self::User => __('general.user'), + }; } } diff --git a/app/Events/SpeedtestBenchmarkFailed.php b/app/Events/SpeedtestBenchmarkHealthy.php similarity index 90% rename from app/Events/SpeedtestBenchmarkFailed.php rename to app/Events/SpeedtestBenchmarkHealthy.php index b00175b13..2838d442b 100644 --- a/app/Events/SpeedtestBenchmarkFailed.php +++ b/app/Events/SpeedtestBenchmarkHealthy.php @@ -6,7 +6,7 @@ use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class SpeedtestBenchmarkFailed +class SpeedtestBenchmarkHealthy { use Dispatchable, SerializesModels; diff --git a/app/Events/SpeedtestBenchmarkPassed.php b/app/Events/SpeedtestBenchmarkUnhealthy.php similarity index 90% rename from app/Events/SpeedtestBenchmarkPassed.php rename to app/Events/SpeedtestBenchmarkUnhealthy.php index ab8e9ae82..92b706e35 100644 --- a/app/Events/SpeedtestBenchmarkPassed.php +++ b/app/Events/SpeedtestBenchmarkUnhealthy.php @@ -6,7 +6,7 @@ use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; -class SpeedtestBenchmarkPassed +class SpeedtestBenchmarkUnhealthy { use Dispatchable, SerializesModels; diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index 2a0bdd63a..bc6d7591d 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -2,48 +2,21 @@ namespace App\Filament\Pages; -use App\Filament\Widgets\RecentDownloadChartWidget; -use App\Filament\Widgets\RecentDownloadLatencyChartWidget; -use App\Filament\Widgets\RecentJitterChartWidget; -use App\Filament\Widgets\RecentPingChartWidget; -use App\Filament\Widgets\RecentUploadChartWidget; -use App\Filament\Widgets\RecentUploadLatencyChartWidget; -use App\Filament\Widgets\StatsOverviewWidget; -use Carbon\Carbon; -use Cron\CronExpression; use Filament\Pages\Dashboard as BasePage; class Dashboard extends BasePage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-layout-dashboard'; protected string $view = 'filament.pages.dashboard'; - public function getSubheading(): ?string + public function getTitle(): string { - $schedule = config('speedtest.schedule'); - - if (blank($schedule) || $schedule === false) { - return __('No speedtests scheduled.'); - } - - $cronExpression = new CronExpression($schedule); - - $nextRunDate = Carbon::parse($cronExpression->getNextRunDate(timeZone: config('app.display_timezone')))->format(config('app.datetime_format')); - - return 'Next speedtest at: '.$nextRunDate; + return __('dashboard.title'); } - protected function getHeaderWidgets(): array + public static function getNavigationLabel(): string { - return [ - StatsOverviewWidget::make(), - RecentDownloadChartWidget::make(), - RecentUploadChartWidget::make(), - RecentPingChartWidget::make(), - RecentJitterChartWidget::make(), - RecentDownloadLatencyChartWidget::make(), - RecentUploadLatencyChartWidget::make(), - ]; + return __('dashboard.title'); } } diff --git a/app/Filament/Pages/Settings/DataIntegration.php b/app/Filament/Pages/Settings/DataIntegration.php index 66c43173a..67a66bf52 100644 --- a/app/Filament/Pages/Settings/DataIntegration.php +++ b/app/Filament/Pages/Settings/DataIntegration.php @@ -7,28 +7,37 @@ use App\Settings\DataIntegrationSettings; use Filament\Actions\Action; use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Pages\SettingsPage; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Grid; -use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; +use Filament\Support\Icons\Heroicon; use Illuminate\Support\Facades\Auth; class DataIntegration extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-circle-stack'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-database'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; protected static ?int $navigationSort = 2; - protected static ?string $title = 'Data Integration'; + public function getTitle(): string + { + return __('settings/data_integration.title'); + } - protected static ?string $navigationLabel = 'Data Integration'; + public static function getNavigationLabel(): string + { + return __('settings/data_integration.label'); + } protected static string $settings = DataIntegrationSettings::class; @@ -46,54 +55,52 @@ public function form(Schema $schema): Schema { return $schema ->components([ - Grid::make([ - 'default' => 1, - 'md' => 3, - ]) + Tabs::make() ->schema([ - Section::make('InfluxDB v2') - ->description('When enabled, all new Speedtest results will also be sent to InfluxDB.') + Tab::make(__('settings/data_integration.influxdb_v2')) + ->icon(Heroicon::OutlinedCircleStack) ->schema([ Toggle::make('influxdb_v2_enabled') - ->label('Enable') + ->label(__('settings/data_integration.influxdb_v2_enabled')) + ->helperText(__('settings/data_integration.influxdb_v2_description')) ->reactive() ->columnSpanFull(), Grid::make(['default' => 1, 'md' => 3]) ->hidden(fn (Get $get) => $get('influxdb_v2_enabled') !== true) ->schema([ TextInput::make('influxdb_v2_url') - ->label('URL') - ->placeholder('http://your-influxdb-instance') + ->label(__('settings/data_integration.influxdb_v2_url')) + ->placeholder(__('settings/data_integration.influxdb_v2_url_placeholder')) ->maxLength(255) ->required(fn (Get $get) => $get('influxdb_v2_enabled') === true) ->columnSpan(['md' => 1]), TextInput::make('influxdb_v2_org') - ->label('Org') + ->label(__('settings/data_integration.influxdb_v2_org')) ->maxLength(255) ->required(fn (Get $get) => $get('influxdb_v2_enabled') === true) ->columnSpan(['md' => 1]), TextInput::make('influxdb_v2_bucket') - ->placeholder('speedtest-tracker') - ->label('Bucket') + ->placeholder(__('settings/data_integration.influxdb_v2_bucket_placeholder')) + ->label(__('settings/data_integration.influxdb_v2_bucket')) ->maxLength(255) ->required(fn (Get $get) => $get('influxdb_v2_enabled') === true) ->columnSpan(['md' => 2]), TextInput::make('influxdb_v2_token') - ->label('Token') + ->label(__('settings/data_integration.influxdb_v2_token')) ->maxLength(255) ->password() ->required(fn (Get $get) => $get('influxdb_v2_enabled') === true) ->columnSpan(['md' => 2]), Checkbox::make('influxdb_v2_verify_ssl') - ->label('Verify SSL') + ->label(__('settings/data_integration.influxdb_v2_verify_ssl')) ->columnSpanFull(), // Button to send old data to InfluxDB Actions::make([ Action::make('Export current results') - ->label('Export current results') + ->label(__('general.export_current_results')) ->action(function () { Notification::make() - ->title('Starting bulk data write to Influxdb') + ->title(__('settings/data_integration.starting_bulk_data_write_to_influxdb')) ->info() ->send(); @@ -106,10 +113,10 @@ public function form(Schema $schema): Schema // Button to test InfluxDB connection Actions::make([ Action::make('Test connection') - ->label('Test connection') + ->label(__('settings/data_integration.test_connection')) ->action(function () { Notification::make() - ->title('Sending test data to Influxdb') + ->title(__('settings/data_integration.sending_test_data_to_influxdb')) ->info() ->send(); @@ -121,7 +128,26 @@ public function form(Schema $schema): Schema ]), ]), ]) - ->compact() + ->columnSpanFull(), + Tab::make(__('settings/data_integration.prometheus')) + ->icon(Heroicon::OutlinedChartBar) + ->schema([ + Toggle::make('prometheus_enabled') + ->label(__('settings/data_integration.prometheus_enabled')) + ->helperText(__('settings/data_integration.prometheus_enabled_helper_text')) + ->reactive() + ->columnSpanFull(), + Grid::make(['default' => 1, 'md' => 3]) + ->hidden(fn (Get $get) => $get('prometheus_enabled') !== true) + ->schema([ + TagsInput::make('prometheus_allowed_ips') + ->label(__('settings/data_integration.prometheus_allowed_ips')) + ->helperText(__('settings/data_integration.prometheus_allowed_ips_helper')) + ->placeholder('192.168.1.100') + ->splitKeys(['Tab', ',', ' ']) + ->columnSpanFull(), + ]), + ]) ->columnSpanFull(), ]) ->columnSpanFull(), diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 94b717d59..c01ee4e1f 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -2,6 +2,7 @@ namespace App\Filament\Pages\Settings; +use App\Actions\Notifications\SendAppriseTestNotification; use App\Actions\Notifications\SendDatabaseTestNotification; use App\Actions\Notifications\SendDiscordTestNotification; use App\Actions\Notifications\SendGotifyTestNotification; @@ -12,8 +13,12 @@ use App\Actions\Notifications\SendSlackTestNotification; use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; +use App\Rules\AppriseScheme; +use App\Rules\ContainsString; use App\Settings\NotificationSettings; +use CodeWithDennis\SimpleAlert\Components\SimpleAlert; use Filament\Actions\Action; +use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -22,21 +27,30 @@ use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; +use Filament\Support\Icons\Heroicon; use Illuminate\Support\Facades\Auth; class Notification extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bell'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-bell-ringing'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; protected static ?int $navigationSort = 3; - protected static ?string $title = 'Notifications'; + public function getTitle(): string + { + return __('settings/notifications.title'); + } - protected static ?string $navigationLabel = 'Notifications'; + public static function getNavigationLabel(): string + { + return __('settings/notifications.label'); + } protected static string $settings = NotificationSettings::class; @@ -54,128 +68,247 @@ public function form(Schema $schema): Schema { return $schema ->components([ - Grid::make([ - 'default' => 1, - 'md' => 3, - ]) - ->columnSpan('full') + Tabs::make() ->schema([ - Grid::make([ - 'default' => 1, - ]) + Tab::make(__('settings/notifications.database')) + ->icon(Heroicon::OutlinedCircleStack) ->schema([ - Section::make('Database') - ->description('Notifications sent to this channel will show up under the 🔔 icon in the header.') + Toggle::make('database_enabled') + ->label(__('general.enable')) + ->live(), + + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('database_enabled') !== true) ->schema([ - Toggle::make('database_enabled') - ->label('Enable database notifications') - ->reactive() - ->columnSpanFull(), - Grid::make([ - 'default' => 1, - ]) - ->hidden(fn (Get $get) => $get('database_enabled') !== true) + Fieldset::make(__('settings.triggers')) + ->columns(1) ->schema([ - Fieldset::make('Triggers') - ->schema([ - Toggle::make('database_on_speedtest_run') - ->label('Notify on every speedtest run') - ->columnSpanFull(), - Toggle::make('database_on_threshold_failure') - ->label('Notify on threshold failures') - ->columnSpanFull(), - ]), - Actions::make([ - Action::make('test database') - ->label('Test database channel') - ->action(fn () => SendDatabaseTestNotification::run(user: Auth::user())), - ]), + Checkbox::make('database_on_speedtest_run') + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')), + Checkbox::make('database_on_threshold_failure') + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')), ]), + + Actions::make([ + Action::make('test database') + ->label(__('settings/notifications.test_database_channel')) + ->action(fn () => SendDatabaseTestNotification::run(user: Auth::user())), + ]), + ]), + + // ... + ]), + + Tab::make(__('settings/notifications.mail')) + ->icon(Heroicon::OutlinedEnvelope) + ->schema([ + Toggle::make('mail_enabled') + ->label(__('general.enable')) + ->live(), + + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('mail_enabled') !== true) + ->schema([ + Fieldset::make(__('settings.triggers')) + ->columns(1) + ->schema([ + Checkbox::make('mail_on_speedtest_run') + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')), + Checkbox::make('mail_on_threshold_failure') + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')), + ]), + + Repeater::make('mail_recipients') + ->label(__('settings/notifications.recipients')) + ->schema([ + TextInput::make('email_address') + ->placeholder('your@email.com') + ->email() + ->required(), + ]), + + Actions::make([ + Action::make('test mail') + ->label(__('settings/notifications.test_mail_channel')) + ->action(fn (Get $get) => SendMailTestNotification::run(recipients: $get('mail_recipients'))) + ->hidden(fn (Get $get) => ! count($get('mail_recipients'))), + ]), + ]), + + // ... + ]), + + Tab::make(__('settings/notifications.webhook')) + ->icon(Heroicon::OutlinedGlobeAlt) + ->schema([ + SimpleAlert::make('wehbook_info') + ->title(__('general.documentation')) + ->description(__('settings/notifications.webhook_hint_description')) + ->border() + ->info() + ->actions([ + Action::make('webhook_docs') + ->label(__('general.view_documentation')) + ->icon('heroicon-m-arrow-long-right') + ->color('info') + ->link() + ->url('https://docs.speedtest-tracker.dev/settings/notifications/webhook') + ->openUrlInNewTab(), ]) - ->compact() - ->columnSpan('full'), + ->columnSpanFull(), + + Toggle::make('webhook_enabled') + ->label(__('general.enable')) + ->live(), - Section::make('Mail') + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('webhook_enabled') !== true) ->schema([ - Toggle::make('mail_enabled') - ->label('Enable mail notifications') - ->reactive() - ->columnSpanFull(), - Grid::make([ - 'default' => 1, - ]) - ->hidden(fn (Get $get) => $get('mail_enabled') !== true) + Fieldset::make(__('settings.triggers')) + ->columns(1) ->schema([ - Fieldset::make('Triggers') - ->schema([ - Toggle::make('mail_on_speedtest_run') - ->label('Notify on every speedtest run') - ->columnSpanFull(), - Toggle::make('mail_on_threshold_failure') - ->label('Notify on threshold failures') - ->columnSpanFull(), - ]), - Repeater::make('mail_recipients') - ->label('Recipients') - ->schema([ - TextInput::make('email_address') - ->placeholder('your@email.com') - ->email() - ->required(), - ]) - ->columnSpanFull(), - Actions::make([ - Action::make('test mail') - ->label('Test mail channel') - ->action(fn (Get $get) => SendMailTestNotification::run(recipients: $get('mail_recipients'))) - ->hidden(fn (Get $get) => ! count($get('mail_recipients'))), - ]), + Checkbox::make('webhook_on_speedtest_run') + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')), + Checkbox::make('webhook_on_threshold_failure') + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')), ]), + + Repeater::make('webhook_urls') + ->label(__('settings/notifications.recipients')) + ->schema([ + TextInput::make('url') + ->placeholder('https://webhook.site/longstringofcharacters') + ->maxLength(2000) + ->required() + ->url(), + ]), + + Actions::make([ + Action::make('test webhook') + ->label(__('settings/notifications.test_webhook_channel')) + ->action(fn (Get $get) => SendWebhookTestNotification::run(webhooks: $get('webhook_urls'))) + ->hidden(fn (Get $get) => ! count($get('webhook_urls'))), + ]), + ]), + + // ... + ]), + Tab::make(__('settings/notifications.apprise')) + ->icon(Heroicon::CloudArrowUp) + ->schema([ + SimpleAlert::make('wehbook_info') + ->title(__('general.documentation')) + ->description(__('settings/notifications.apprise_hint_description')) + ->border() + ->info() + ->actions([ + Action::make('webhook_docs') + ->label(__('general.view_documentation')) + ->icon('heroicon-m-arrow-long-right') + ->color('info') + ->link() + ->url('https://docs.speedtest-tracker.dev/settings/notifications/apprise') + ->openUrlInNewTab(), ]) - ->compact() - ->columnSpan('full'), + ->columnSpanFull(), - Section::make('Webhook') + Toggle::make('apprise_enabled') + ->label(__('settings/notifications.enable_apprise_notifications')) + ->reactive() + ->columnSpanFull(), + Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Get $get) => $get('apprise_enabled') !== true) ->schema([ - Toggle::make('webhook_enabled') - ->label('Enable webhook notifications') - ->reactive() - ->columnSpanFull(), - Grid::make([ - 'default' => 1, - ]) - ->hidden(fn (Get $get) => $get('webhook_enabled') !== true) + Fieldset::make(__('settings/notifications.apprise_server')) ->schema([ - Fieldset::make('Triggers') - ->schema([ - Toggle::make('webhook_on_speedtest_run') - ->label('Notify on every speedtest run') - ->columnSpan(2), - Toggle::make('webhook_on_threshold_failure') - ->label('Notify on threshold failures') - ->columnSpan(2), - ]), - Repeater::make('webhook_urls') - ->label('Recipients') - ->schema([ - TextInput::make('url') - ->placeholder('https://webhook.site/longstringofcharacters') - ->maxLength(2000) - ->required() - ->url(), - ]) + TextInput::make('apprise_server_url') + ->label(__('settings/notifications.apprise_server_url')) + ->placeholder('http://localhost:8000/notify') + ->helperText(__('settings/notifications.apprise_server_url_helper')) + ->maxLength(2000) + ->required() + ->url() + ->rule(new ContainsString('/notify')) + ->columnSpanFull(), + Checkbox::make('apprise_verify_ssl') + ->label(__('settings/notifications.apprise_verify_ssl')) + ->default(true) ->columnSpanFull(), - Actions::make([ - Action::make('test webhook') - ->label('Test webhook channel') - ->action(fn (Get $get) => SendWebhookTestNotification::run(webhooks: $get('webhook_urls'))) - ->hidden(fn (Get $get) => ! count($get('webhook_urls'))), - ]), ]), - ]) - ->compact() - ->columnSpan('full'), + Fieldset::make(__('settings.triggers')) + ->schema([ + Checkbox::make('apprise_on_speedtest_run') + ->label(__('settings/notifications.notify_on_every_speedtest_run')) + ->helpertext(__('settings/notifications.notify_on_every_speedtest_run_helper')) + ->columnSpanFull(), + Checkbox::make('apprise_on_threshold_failure') + ->label(__('settings/notifications.notify_on_threshold_failures')) + ->helpertext(__('settings/notifications.notify_on_threshold_failures_helper')) + ->columnSpanFull(), + ]), + Repeater::make('apprise_channel_urls') + ->label(__('settings/notifications.apprise_channels')) + ->helperText(__('settings/notifications.apprise_save_to_test')) + ->schema([ + TextInput::make('channel_url') + ->label(__('settings/notifications.apprise_channel_url')) + ->placeholder('discord://WebhookID/WebhookToken') + ->helperText(__('settings/notifications.apprise_channel_url_helper')) + ->maxLength(2000) + ->distinct() + ->required() + ->rule(new AppriseScheme), + ]) + ->columnSpanFull(), + Actions::make([ + Action::make('test apprise') + ->label(__('settings/notifications.test_apprise_channel')) + ->action(fn (Get $get) => SendAppriseTestNotification::run( + channel_urls: $get('apprise_channel_urls'), + )) + ->hidden(function () { + $settings = app(NotificationSettings::class); + + return empty($settings->apprise_server_url) || ! count($settings->apprise_channel_urls ?? []); + }), + ]), + ]), + ]), + ]) + ->columnSpanFull(), + + // ! DEPRECATED CHANNELS + SimpleAlert::make('deprecation_warning') + ->title('Deprecated Notification Channels') + ->description('The following notification channels are deprecated and will be removed in a future release!') + ->border() + ->warning() + ->columnSpanFull(), + Grid::make([ + 'default' => 1, + 'md' => 3, + ]) + ->columnSpan('full') + ->schema([ + Grid::make([ + 'default' => 1, + ]) + ->schema([ Section::make('Pushover') ->description('⚠️ Pushover is deprecated and will be removed in a future release.') ->schema([ diff --git a/app/Filament/Pages/Settings/Thresholds.php b/app/Filament/Pages/Settings/Thresholds.php index 464f2cb60..1953ff52a 100644 --- a/app/Filament/Pages/Settings/Thresholds.php +++ b/app/Filament/Pages/Settings/Thresholds.php @@ -15,15 +15,21 @@ class Thresholds extends SettingsPage { - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-triangle'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-alert-triangle'; protected static string|\UnitEnum|null $navigationGroup = 'Settings'; protected static ?int $navigationSort = 4; - protected static ?string $title = 'Thresholds'; + public function getTitle(): string + { + return __('settings/thresholds.title'); + } - protected static ?string $navigationLabel = 'Thresholds'; + public static function getNavigationLabel(): string + { + return __('settings/thresholds.label'); + } protected static string $settings = ThresholdSettings::class; @@ -51,11 +57,11 @@ public function form(Schema $schema): Schema 'default' => 1, ]) ->schema([ - Section::make('Absolute') - ->description('Absolute thresholds do not take into account previous history and could be triggered on each test.') + Section::make(__('settings/thresholds.absolute')) + ->description(__('settings/thresholds.absolute_description')) ->schema([ Toggle::make('absolute_enabled') - ->label('Enable absolute thresholds') + ->label(__('settings/thresholds.absolute_enabled')) ->reactive() ->columnSpan(2), Grid::make([ @@ -63,28 +69,28 @@ public function form(Schema $schema): Schema ]) ->hidden(fn (Get $get) => $get('absolute_enabled') !== true) ->schema([ - Fieldset::make('Metrics') + Fieldset::make(__('settings/thresholds.metrics')) ->schema([ TextInput::make('absolute_download') - ->label('Download') - ->hint('Mbps') - ->helperText('Set to zero to disable this metric.') + ->label(__('general.download')) + ->hint(__('general.mbps')) + ->helperText(__('settings/thresholds.metrics_helper_text')) ->default(0) ->minValue(0) ->numeric() ->required(), TextInput::make('absolute_upload') - ->label('Upload') - ->hint('Mbps') - ->helperText('Set to zero to disable this metric.') + ->label(__('general.upload')) + ->hint(__('general.mbps')) + ->helperText(__('settings/thresholds.metrics_helper_text')) ->default(0) ->minValue(0) ->numeric() ->required(), TextInput::make('absolute_ping') - ->label('Ping') - ->hint('ms') - ->helperText('Set to zero to disable this metric.') + ->label(__('general.ping')) + ->hint(__('general.ms')) + ->helperText(__('settings/thresholds.metrics_helper_text')) ->default(0) ->minValue(0) ->numeric() diff --git a/app/Filament/Pages/Tools/ListOoklaServers.php b/app/Filament/Pages/Tools/ListOoklaServers.php index 7983c9d34..c82ad82b3 100644 --- a/app/Filament/Pages/Tools/ListOoklaServers.php +++ b/app/Filament/Pages/Tools/ListOoklaServers.php @@ -7,9 +7,9 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Contracts\HasForms; -use Filament\Forms\Form; use Filament\Notifications\Notification; use Filament\Pages\Page; +use Filament\Schemas\Schema; class ListOoklaServers extends Page implements HasForms { @@ -55,7 +55,7 @@ public function fetchServers(): void ]); } catch (\Exception $e) { Notification::make() - ->title('Error fetching servers') + ->title(__('errors.error_fetching_servers')) ->body($e->getMessage()) ->danger() ->send(); @@ -69,10 +69,10 @@ public function fetchServers(): void } } - public function form(Form $form): Form + public function form(Schema $schema): Schema { - return $form - ->schema([ + return $schema + ->components([ Textarea::make('servers') ->label(false) ->rows(20) @@ -92,7 +92,7 @@ protected function getHeaderActions(): array $this->fetchServers(); Notification::make() - ->title('Servers refreshed successfully') + ->title(__('errors.servers_refreshed_successfully')) ->success() ->send(); }), @@ -106,7 +106,7 @@ protected function getHeaderActions(): array $this->js('navigator.clipboard.writeText('.json_encode($this->servers).')'); Notification::make() - ->title('Copied to clipboard') + ->title(__('errors.copied_to_clipboard')) ->success() ->send(); }), diff --git a/app/Filament/Resources/ApiTokens/ApiTokenResource.php b/app/Filament/Resources/ApiTokens/ApiTokenResource.php index fa669b019..acca29a28 100644 --- a/app/Filament/Resources/ApiTokens/ApiTokenResource.php +++ b/app/Filament/Resources/ApiTokens/ApiTokenResource.php @@ -19,9 +19,15 @@ class ApiTokenResource extends Resource protected static string|\UnitEnum|null $navigationGroup = 'Settings'; - protected static ?string $label = 'API Token'; + public static function getLabel(): ?string + { + return __('api_tokens.api_token'); + } - protected static ?string $pluralLabel = 'API Tokens'; + public static function getPluralLabel(): ?string + { + return __('api_tokens.api_tokens'); + } public static function canAccess(): bool { diff --git a/app/Filament/Resources/ApiTokens/Pages/ListApiTokens.php b/app/Filament/Resources/ApiTokens/Pages/ListApiTokens.php index 9e176bb98..8c36d0fe9 100644 --- a/app/Filament/Resources/ApiTokens/Pages/ListApiTokens.php +++ b/app/Filament/Resources/ApiTokens/Pages/ListApiTokens.php @@ -18,7 +18,7 @@ protected function getHeaderActions(): array { return [ Action::make('createToken') - ->label('Create API Token') + ->label(__('api_tokens.create_api_token')) ->schema(ApiTokenForm::schema()) ->action(function (array $data): void { $token = Auth::user()->createToken( @@ -28,8 +28,8 @@ protected function getHeaderActions(): array ); Notification::make() - ->title('Token Created') - ->body('Your token: `'.explode('|', $token->plainTextToken)[1].'`') + ->title(__('general.token_created')) + ->body(__('api_tokens.your_token').': `'.explode('|', $token->plainTextToken)[1].'`') ->success() ->persistent() ->send(); diff --git a/app/Filament/Resources/ApiTokens/Schemas/ApiTokenForm.php b/app/Filament/Resources/ApiTokens/Schemas/ApiTokenForm.php index b6d2b1455..ed66fbe11 100644 --- a/app/Filament/Resources/ApiTokens/Schemas/ApiTokenForm.php +++ b/app/Filament/Resources/ApiTokens/Schemas/ApiTokenForm.php @@ -15,29 +15,29 @@ public static function schema(): array Grid::make() ->schema([ TextInput::make('name') - ->label('Name') + ->label(__('general.name')) ->unique(ignoreRecord: true) ->maxLength(100) ->required(), CheckboxList::make('abilities') - ->label('Abilities') + ->label(__('api_tokens.abilities')) ->options([ - 'results:read' => 'Read results', - 'speedtests:run' => 'Run speedtest', - 'ookla:list-servers' => 'List servers', + 'results:read' => __('api_tokens.read_results'), + 'speedtests:run' => __('general.run_speedtest'), + 'ookla:list-servers' => __('general.list_servers'), ]) ->required() ->bulkToggleable() ->descriptions([ - 'results:read' => 'Allow this token to read results.', - 'speedtests:run' => 'Allow this token to run speedtests.', - 'ookla:list-servers' => 'Allow this token to list servers.', + 'results:read' => __('api_tokens.read_results_description'), + 'speedtests:run' => __('api_tokens.run_speedtest_description'), + 'ookla:list-servers' => __('api_tokens.list_servers_description'), ]), DateTimePicker::make('expires_at') - ->label('Expires at') + ->label(__('api_tokens.expires_at')) ->nullable() ->native(false) - ->helperText('Leave empty for no expiration'), + ->helperText(__('api_tokens.expires_at_helper_text')), ]) ->columns([ 'lg' => 1, diff --git a/app/Filament/Resources/ApiTokens/Tables/ApiTokenTable.php b/app/Filament/Resources/ApiTokens/Tables/ApiTokenTable.php index 6f3bff19b..31aff987e 100644 --- a/app/Filament/Resources/ApiTokens/Tables/ApiTokenTable.php +++ b/app/Filament/Resources/ApiTokens/Tables/ApiTokenTable.php @@ -21,21 +21,28 @@ public static function table(Table $table): Table return $table ->query(PersonalAccessToken::query()->where('tokenable_id', Auth::id())) ->columns([ - TextColumn::make('name')->searchable(), - TextColumn::make('abilities')->badge(), + TextColumn::make('name') + ->label(__('general.name')) + ->searchable(), + TextColumn::make('abilities') + ->label(__('api_tokens.abilities')) + ->badge(), TextColumn::make('created_at') + ->label(__('general.created_at')) ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) ->toggleable(isToggledHiddenByDefault: false) ->sortable() ->alignEnd(), TextColumn::make('last_used_at') + ->label(__('api_tokens.last_used_at')) ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) ->toggleable(isToggledHiddenByDefault: true) ->sortable() ->alignEnd(), TextColumn::make('expires_at') + ->label(__('api_tokens.expires_at')) ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) ->toggleable(isToggledHiddenByDefault: false) @@ -44,10 +51,10 @@ public static function table(Table $table): Table ]) ->filters([ TernaryFilter::make('expired') - ->label('Token Status') - ->placeholder('All tokens') - ->falseLabel('Active tokens') - ->trueLabel('Expired tokens') + ->label(__('api_tokens.token_status')) + ->placeholder(__('api_tokens.all_tokens')) + ->falseLabel(__('api_tokens.active_tokens')) + ->trueLabel(__('api_tokens.expired_tokens')) ->native(false) ->queries( true: fn (Builder $query) => $query @@ -62,12 +69,12 @@ public static function table(Table $table): Table blank: fn (Builder $query) => $query, ), SelectFilter::make('abilities') - ->label('Abilities') + ->label(__('api_tokens.abilities')) ->multiple() ->options([ - 'results:read' => 'Read results', - 'speedtests:run' => 'Run speedtest', - 'ookla:list-servers' => 'List servers', + 'results:read' => __('api_tokens.read_results'), + 'speedtests:run' => __('general.run_speedtest'), + 'ookla:list-servers' => __('general.list_servers'), ]) ->query(function (Builder $query, array $data): Builder { foreach ($data['values'] ?? [] as $value) { diff --git a/app/Filament/Resources/Results/ResultResource.php b/app/Filament/Resources/Results/ResultResource.php index c05bc5f81..5ff0893d7 100644 --- a/app/Filament/Resources/Results/ResultResource.php +++ b/app/Filament/Resources/Results/ResultResource.php @@ -14,7 +14,22 @@ class ResultResource extends Resource { protected static ?string $model = Result::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-table-cells'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-table'; + + public static function getNavigationLabel(): string + { + return __('results.title'); + } + + public static function getModelLabel(): string + { + return __('results.title'); + } + + public static function getPluralModelLabel(): string + { + return __('results.title'); + } public static function form(Schema $schema): Schema { diff --git a/app/Filament/Resources/Results/Schemas/ResultForm.php b/app/Filament/Resources/Results/Schemas/ResultForm.php index 0981b5f3c..3e2e5b852 100644 --- a/app/Filament/Resources/Results/Schemas/ResultForm.php +++ b/app/Filament/Resources/Results/Schemas/ResultForm.php @@ -24,67 +24,67 @@ public static function schema(): array // Left column: stacked sections Grid::make(['default' => 1]) ->schema([ - Section::make('Result Overview')->schema([ + Section::make(__('results.result_overview'))->schema([ TextInput::make('id') - ->label('ID'), + ->label(__('general.id')), TextInput::make('created_at') - ->label('Created') + ->label(__('general.created_at')) ->afterStateHydrated(function (TextInput $component, $state) { $component->state(Carbon::parse($state) ->timezone(config('app.display_timezone')) ->format(config('app.datetime_format'))); }), TextInput::make('download') - ->label('Download') + ->label(__('general.download')) ->afterStateHydrated(fn ($component, Result $record) => $component->state(! blank($record->download) ? Number::toBitRate(bits: $record->download_bits, precision: 2) : '')), TextInput::make('upload') - ->label('Upload') + ->label(__('general.upload')) ->afterStateHydrated(fn ($component, Result $record) => $component->state(! blank($record->upload) ? Number::toBitRate(bits: $record->upload_bits, precision: 2) : '')), TextInput::make('ping') - ->label('Ping') + ->label(__('general.ping')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), TextInput::make('data.packetLoss') - ->label('Packet Loss') + ->label(__('results.packet_loss')) ->formatStateUsing(fn ($state) => number_format((float) $state, 2, '.', '').' %'), ])->columns(2)->columnSpan('full'), - Section::make('Download Latency') + Section::make(__('general.download_latency')) ->schema([ - TextInput::make('data.download.latency.jitter')->label('Jitter') + TextInput::make('data.download.latency.jitter')->label(__('general.jitter')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.download.latency.high')->label('High') + TextInput::make('data.download.latency.high')->label(__('general.high')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.download.latency.low')->label('Low') + TextInput::make('data.download.latency.low')->label(__('general.low')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.download.latency.iqm')->label('IQM') + TextInput::make('data.download.latency.iqm')->label(__('results.iqm')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), ]) ->columns(2) ->collapsed() ->columnSpan('full'), - Section::make('Upload Latency') + Section::make(__('general.upload_latency')) ->schema([ - TextInput::make('data.upload.latency.jitter')->label('Jitter') + TextInput::make('data.upload.latency.jitter')->label(__('general.jitter')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.upload.latency.high')->label('High') + TextInput::make('data.upload.latency.high')->label(__('general.high')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.upload.latency.low')->label('Low') + TextInput::make('data.upload.latency.low')->label(__('general.low')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.upload.latency.iqm')->label('IQM') + TextInput::make('data.upload.latency.iqm')->label(__('results.iqm')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), ]) ->columns(2) ->collapsed() ->columnSpan('full'), - Section::make('Ping Details') + Section::make(__('results.ping_details')) ->schema([ - TextInput::make('data.ping.jitter')->label('Jitter') + TextInput::make('data.ping.jitter')->label(__('general.jitter')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.ping.low')->label('Low') + TextInput::make('data.ping.low')->label(__('general.low')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), - TextInput::make('data.ping.high')->label('High') + TextInput::make('data.ping.high')->label(__('general.high')) ->formatStateUsing(fn ($state) => number_format((float) $state, 0, '.', '').' ms'), ]) ->columns(2) @@ -92,33 +92,39 @@ public static function schema(): array ->columnSpan('full'), Textarea::make('data.message') - ->label('Message') + ->label(__('general.message')) ->hint(new HtmlString('🔗Error Messages')) ->columnSpanFull(), ]) ->columnSpan(['md' => 3]), // Right column: Server & Metadata - Section::make('Server & Metadata')->schema([ + Section::make(__('results.server_&_metadata'))->schema([ TextEntry::make('service') + ->label(__('results.service')) ->state(fn (Result $result): string => $result->service->getLabel()), TextEntry::make('server_name') + ->label(__('results.server_name')) ->state(fn (Result $result): ?string => $result->server_name), TextEntry::make('server_id') - ->label('Server ID') + ->label(__('results.server_id')) ->state(fn (Result $result): ?string => $result->server_id), TextEntry::make('isp') - ->label('ISP') + ->label(__('results.isp')) ->state(fn (Result $result): ?string => $result->isp), TextEntry::make('server_location') - ->label('Server Location') + ->label(__('results.server_location')) ->state(fn (Result $result): ?string => $result->server_location), TextEntry::make('server_host') + ->label(__('results.server_host')) ->state(fn (Result $result): ?string => $result->server_host), TextEntry::make('comment') + ->label(__('general.comment')) ->state(fn (Result $result): ?string => $result->comments), - Checkbox::make('scheduled'), - Checkbox::make('healthy'), + Checkbox::make('scheduled') + ->label(__('results.scheduled')), + Checkbox::make('healthy') + ->label(__('general.healthy')), ])->columns(1)->columnSpan(['md' => 2]), ]), ]; diff --git a/app/Filament/Resources/Results/Tables/ResultTable.php b/app/Filament/Resources/Results/Tables/ResultTable.php index 42276d9c0..6808e9f7d 100644 --- a/app/Filament/Resources/Results/Tables/ResultTable.php +++ b/app/Filament/Resources/Results/Tables/ResultTable.php @@ -4,8 +4,8 @@ use App\Enums\ResultStatus; use App\Filament\Exports\ResultExporter; +use App\Filament\Tables\Columns\ResultServerColumn; use App\Helpers\Number; -use App\Jobs\TruncateResults; use App\Models\Result; use Filament\Actions\Action; use Filament\Actions\ActionGroup; @@ -16,6 +16,7 @@ use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Textarea; use Filament\Support\Enums\Alignment; +use Filament\Support\Icons\Heroicon; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\Filter; @@ -32,93 +33,67 @@ public static function table(Table $table): Table return $table ->columns([ TextColumn::make('id') - ->label('ID') + ->label(__('general.id')) ->toggleable(isToggledHiddenByDefault: false) ->sortable(), + + TextColumn::make('status') + ->label(__('general.status')) + ->badge() + ->toggleable(isToggledHiddenByDefault: false), + TextColumn::make('data.interface.externalIp') - ->label('IP address') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->interface->externalIp', $direction); - }), + ->label(__('results.ip_address')) + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('service') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(), - TextColumn::make('data.server.id') - ->label('Server ID') - ->toggleable(isToggledHiddenByDefault: false) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->server->id', $direction); - }), - TextColumn::make('data.isp') - ->label('ISP') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->isp', $direction); - }), - TextColumn::make('data.server.location') - ->label('Server Location') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->server->location', $direction); - }), - TextColumn::make('data.server.name') - ->toggleable(isToggledHiddenByDefault: false) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->server->name', $direction); - }), + ->label(__('results.service')) + ->toggleable(isToggledHiddenByDefault: true), + + ResultServerColumn::make('server') + ->label(__('general.server')) + ->toggleable(isToggledHiddenByDefault: false), + TextColumn::make('download') + ->label(__('results.download')) ->getStateUsing(fn (Result $record): ?string => ! blank($record->download) ? Number::toBitRate(bits: $record->download_bits, precision: 2) : null) ->toggleable(isToggledHiddenByDefault: false) ->sortable(), + TextColumn::make('upload') + ->label(__('results.upload')) ->getStateUsing(fn (Result $record): ?string => ! blank($record->upload) ? Number::toBitRate(bits: $record->upload_bits, precision: 2) : null) ->toggleable(isToggledHiddenByDefault: false) ->sortable(), + TextColumn::make('ping') + ->label(__('results.ping')) ->toggleable(isToggledHiddenByDefault: false) ->sortable() ->formatStateUsing(function ($state) { return number_format((float) $state, 0, '.', '').' ms'; }), - TextColumn::make('data.download.latency.jitter') - ->label('Download jitter') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->download->latency->jitter', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - TextColumn::make('data.download.latency.high') - ->label('Download latency high') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->download->latency->high', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - TextColumn::make('data.download.latency.low') - ->label('Download latency low') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->download->latency->low', $direction); - }) + + TextColumn::make('data.packetLoss') + ->label(__('results.packet_loss')) + ->toggleable(isToggledHiddenByDefault: false) + ->sortable() ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; + return number_format((float) $state, 2, '.', '').' %'; }), - TextColumn::make('data.download.latency.iqm') - ->label('Download latency iqm') + + TextColumn::make('data.download.latency.jitter') + ->label(__('results.download_latency_jitter')) ->toggleable(isToggledHiddenByDefault: true) ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->download->latency->iqm', $direction); + return $query->orderBy('data->download->latency->jitter', $direction); }) ->formatStateUsing(function ($state) { return number_format((float) $state, 0, '.', '').' ms'; }), + TextColumn::make('data.upload.latency.jitter') - ->label('Upload jitter') + ->label(__('results.upload_latency_jitter')) ->toggleable(isToggledHiddenByDefault: true) ->sortable(query: function (Builder $query, string $direction): Builder { return $query->orderBy('data->upload->latency->jitter', $direction); @@ -126,81 +101,40 @@ public static function table(Table $table): Table ->formatStateUsing(function ($state) { return number_format((float) $state, 0, '.', '').' ms'; }), - TextColumn::make('data.upload.latency.high') - ->label('Upload latency high') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->upload->latency->high', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - TextColumn::make('data.upload.latency.low') - ->label('Upload latency low') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->upload->latency->low', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - TextColumn::make('data.upload.latency.iqm') - ->label('Upload latency iqm') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(query: function (Builder $query, string $direction): Builder { - return $query->orderBy('data->upload->latency->iqm', $direction); - }) - ->formatStateUsing(function ($state) { - return number_format((float) $state, 0, '.', '').' ms'; - }), - TextColumn::make('data.packetLoss') - ->label('Packet Loss') - ->toggleable(isToggledHiddenByDefault: true) - ->sortable() - ->formatStateUsing(function ($state) { - return number_format((float) $state, 2, '.', '').' %'; - }), - TextColumn::make('status') - ->badge() - ->toggleable(isToggledHiddenByDefault: false) - ->sortable(), - IconColumn::make('scheduled') + + IconColumn::make('healthy') + ->label(__('general.healthy')) ->boolean() - ->toggleable(isToggledHiddenByDefault: true) + ->trueIcon(Heroicon::OutlinedCheckCircle) + ->falseIcon(Heroicon::OutlinedExclamationCircle) + ->trueColor('success') + ->falseColor('warning') + ->toggleable(isToggledHiddenByDefault: false) ->alignment(Alignment::Center), - IconColumn::make('healthy') + + IconColumn::make('scheduled') + ->label(__('results.scheduled')) ->boolean() ->toggleable(isToggledHiddenByDefault: true) - ->sortable() ->alignment(Alignment::Center), - TextColumn::make('data.message') - ->label('Error Message') - ->limit(15) - ->tooltip(fn ($state) => $state) - ->toggleable(isToggledHiddenByDefault: true) - ->sortable(), + TextColumn::make('created_at') + ->label(__('general.created_at')) ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) ->toggleable(isToggledHiddenByDefault: false) - ->sortable() - ->alignment(Alignment::End), - TextColumn::make('updated_at') - ->dateTime(config('app.datetime_format')) - ->timezone(config('app.display_timezone')) - ->toggleable(isToggledHiddenByDefault: true) - ->sortable() - ->alignment(Alignment::End), + ->sortable(), ]) - ->deferFilters(false) - ->deferColumnManager(false) ->filters([ Filter::make('created_at') + ->label(__('general.created_at')) ->schema([ DatePicker::make('created_from') + ->label(__('results.created_from')) ->closeOnDateSelection() ->native(false), DatePicker::make('created_until') + ->label(__('results.created_until')) ->closeOnDateSelection() ->native(false), ]) @@ -215,8 +149,9 @@ public static function table(Table $table): Table fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date), ); }), + SelectFilter::make('ip_address') - ->label('IP address') + ->label(__('results.ip_address')) ->multiple() ->options(function (): array { return Result::query() @@ -232,8 +167,9 @@ public static function table(Table $table): Table ->toArray(); }) ->attribute('data->interface->externalIp'), + SelectFilter::make('server_name') - ->label('Server name') + ->label(__('results.server_name')) ->multiple() ->options(function (): array { return Result::query() @@ -249,24 +185,48 @@ public static function table(Table $table): Table ->toArray(); }) ->attribute('data->server->name'), + + SelectFilter::make('server_id') + ->label(__('results.server_id')) + ->multiple() + ->options(function (): array { + return Result::query() + ->select('data->server->id AS data_server_id') + ->whereNotNull('data->server->id') + ->where('status', '=', ResultStatus::Completed) + ->distinct() + ->orderBy('data->server->id') + ->get() + ->mapWithKeys(function (Result $item, int $key) { + return [$item['data_server_id'] => $item['data_server_id']]; + }) + ->toArray(); + }) + ->attribute('data->server->id'), + TernaryFilter::make('scheduled') + ->label(__('results.scheduled')) ->nullable() ->native(false) - ->trueLabel('Only scheduled speedtests') - ->falseLabel('Only manual speedtests') + ->trueLabel(__('results.only_scheduled_speedtests')) + ->falseLabel(__('results.only_manual_speedtests')) ->queries( true: fn (Builder $query) => $query->where('scheduled', true), false: fn (Builder $query) => $query->where('scheduled', false), blank: fn (Builder $query) => $query, ), + SelectFilter::make('status') + ->label(__('general.status')) ->multiple() ->options(ResultStatus::class), + TernaryFilter::make('healthy') + ->label(__('general.healthy')) ->nullable() ->native(false) - ->trueLabel('Only healthy speedtests') - ->falseLabel('Only unhealthy speedtests') + ->trueLabel(__('results.only_healthy_speedtests')) + ->falseLabel(__('results.only_unhealthy_speedtests')) ->queries( true: fn (Builder $query) => $query->where('healthy', true), false: fn (Builder $query) => $query->where('healthy', false), @@ -278,12 +238,13 @@ public static function table(Table $table): Table ViewAction::make(), DeleteAction::make(), Action::make('view result') - ->label('View on Speedtest.net') + ->label(__('results.view_on_speedtest_net')) ->icon('heroicon-o-link') ->url(fn (Result $record): ?string => $record->result_url) ->hidden(fn (Result $record): bool => $record->status !== ResultStatus::Completed) ->openUrlInNewTab(), Action::make('updateComments') + ->label(__('results.update_comments')) ->icon('heroicon-o-chat-bubble-bottom-center-text') ->hidden(fn (): bool => ! (Auth::user()?->is_admin ?? false) && ! (Auth::user()?->is_user ?? false)) ->mountUsing(fn ($form, Result $record) => $form->fill([ @@ -295,6 +256,7 @@ public static function table(Table $table): Table }) ->schema([ Textarea::make('comments') + ->label(__('general.comments')) ->rows(6) ->maxLength(500), ]), @@ -302,29 +264,15 @@ public static function table(Table $table): Table ]) ->toolbarActions([ DeleteBulkAction::make(), - ]) - ->headerActions([ ExportAction::make() ->exporter(ResultExporter::class) ->columnMapping(false) - ->modalHeading('Export all Results') - ->modalDescription('This will export all columns for all results.') + ->modalHeading(__('results.export_all_results')) + ->modalDescription(__('results.export_all_results_description')) ->fileName(fn (): string => 'results-'.now()->timestamp), - ActionGroup::make([ - Action::make('truncate') - ->action(fn () => TruncateResults::dispatch(Auth::user())) - ->requiresConfirmation() - ->modalHeading('Truncate Results') - ->modalDescription('Are you sure you want to truncate all results data? This can\'t be undone.') - ->color('danger') - ->icon('heroicon-o-trash') - ->hidden(fn (): bool => ! Auth::user()->is_admin), - ]) - ->dropdownPlacement('left-start'), ]) ->defaultSort('id', 'desc') - ->paginationPageOptions([5, 10, 25, 50, 'all']) - ->deferLoading() + ->paginationPageOptions([10, 25, 50]) ->poll('60s'); } } diff --git a/app/Filament/Resources/Users/Schemas/UserForm.php b/app/Filament/Resources/Users/Schemas/UserForm.php index 4c72d0fc7..5f6f2eccf 100644 --- a/app/Filament/Resources/Users/Schemas/UserForm.php +++ b/app/Filament/Resources/Users/Schemas/UserForm.php @@ -22,24 +22,27 @@ public static function schema(): array ])->columnSpan([ 'lg' => 2, ])->schema([ - Section::make('Details') + Section::make(__('general.details')) ->columns([ 'default' => 1, 'lg' => 2, ]) ->schema([ TextInput::make('name') + ->label(__('general.name')) ->required() ->maxLength(255) ->columnSpanFull(), TextInput::make('email') + ->label(__('general.email')) ->email() ->required() ->maxLength(255) ->columnSpanFull(), TextInput::make('password') + ->label(__('general.password')) ->confirmed() ->password() ->revealable() @@ -48,6 +51,7 @@ public static function schema(): array ->dehydrated(fn ($state) => filled($state)), TextInput::make('password_confirmation') + ->label(__('general.password_confirmation')) ->password() ->revealable(), ]), @@ -56,10 +60,10 @@ public static function schema(): array Grid::make(1) ->columnSpan(1) ->schema([ - Section::make('Platform') + Section::make(__('general.platform')) ->schema([ Select::make('role') - ->label('Role') + ->label(__('general.role')) ->default(UserRole::User) ->options(UserRole::class) ->required() @@ -69,9 +73,11 @@ public static function schema(): array Section::make() ->schema([ Placeholder::make('created_at') + ->label(__('general.created_at')) ->content(fn (?User $record): string => $record ? $record->created_at->diffForHumans() : '-'), Placeholder::make('updated_at') + ->label(__('general.updated_at')) ->content(fn (?User $record): string => $record ? $record->updated_at->diffForHumans() : '-'), ]), ]), diff --git a/app/Filament/Resources/Users/Tables/UserTable.php b/app/Filament/Resources/Users/Tables/UserTable.php index 124109e4a..cb910493e 100644 --- a/app/Filament/Resources/Users/Tables/UserTable.php +++ b/app/Filament/Resources/Users/Tables/UserTable.php @@ -17,25 +17,30 @@ public static function table(Table $table): Table return $table ->columns([ TextColumn::make('id') - ->label('ID') + ->label(__('general.id')) ->sortable() ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('name') + ->label(__('general.name')) ->sortable() ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('email') + ->label(__('general.email')) ->searchable() ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('role') + ->label(__('general.role')) ->badge() ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('created_at') + ->label(__('general.created_at')) ->alignEnd() ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) ->sortable() ->toggleable(isToggledHiddenByDefault: false), TextColumn::make('updated_at') + ->label(__('general.updated_at')) ->alignEnd() ->dateTime(config('app.datetime_format')) ->timezone(config('app.display_timezone')) @@ -44,6 +49,7 @@ public static function table(Table $table): Table ]) ->filters([ SelectFilter::make('role') + ->label(__('general.role')) ->native(false) ->options(UserRole::class), ]) diff --git a/app/Filament/Resources/Users/UserResource.php b/app/Filament/Resources/Users/UserResource.php index b4d4fe121..c1b2d958d 100644 --- a/app/Filament/Resources/Users/UserResource.php +++ b/app/Filament/Resources/Users/UserResource.php @@ -14,12 +14,20 @@ class UserResource extends Resource { protected static ?string $model = User::class; - protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-users'; - - protected static string|\UnitEnum|null $navigationGroup = 'Settings'; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-users'; protected static ?int $navigationSort = 4; + public static function getLabel(): ?string + { + return __('general.user'); + } + + public static function getPluralLabel(): ?string + { + return __('general.users'); + } + public static function form(Schema $schema): Schema { return $schema->components(UserForm::schema()); diff --git a/app/Filament/Tables/Columns/ResultServerColumn.php b/app/Filament/Tables/Columns/ResultServerColumn.php new file mode 100644 index 000000000..357951e15 --- /dev/null +++ b/app/Filament/Tables/Columns/ResultServerColumn.php @@ -0,0 +1,28 @@ +serverName = $this->record->server_name; + + return $this->serverName; + } + + public function getServerId(): ?int + { + $this->serverId = $this->record->server_id; + + return $this->serverId; + } +} diff --git a/app/Filament/Widgets/RecentDownloadChartWidget.php b/app/Filament/Widgets/RecentDownloadChartWidget.php index 4d331b67a..098708648 100644 --- a/app/Filament/Widgets/RecentDownloadChartWidget.php +++ b/app/Filament/Widgets/RecentDownloadChartWidget.php @@ -13,7 +13,12 @@ class RecentDownloadChartWidget extends ChartWidget { use HasChartFilters; - protected ?string $heading = 'Download (Mbps)'; + protected ?string $heading = null; + + public function getHeading(): ?string + { + return __('general.download_mbps'); + } protected int|string|array $columnSpan = 'full'; @@ -33,13 +38,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'download', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') @@ -48,7 +53,7 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Download', + 'label' => __('general.download'), 'data' => $results->map(fn ($item) => ! blank($item->download) ? Number::bitsToMagnitude(bits: $item->download_bits, precision: 2, magnitude: 'mbit') : null), 'borderColor' => 'rgba(14, 165, 233)', 'backgroundColor' => 'rgba(14, 165, 233, 0.1)', @@ -59,7 +64,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Average', + 'label' => __('general.average'), 'data' => array_fill(0, count($results), Average::averageDownload($results)), 'borderColor' => 'rgb(243, 7, 6, 1)', 'pointBackgroundColor' => 'rgb(243, 7, 6, 1)', diff --git a/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php b/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php index d5beeab60..e06c86c87 100644 --- a/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php +++ b/app/Filament/Widgets/RecentDownloadLatencyChartWidget.php @@ -11,7 +11,12 @@ class RecentDownloadLatencyChartWidget extends ChartWidget { use HasChartFilters; - protected ?string $heading = 'Download Latency'; + protected ?string $heading = null; + + public function getHeading(): ?string + { + return __('general.download_latency'); + } protected int|string|array $columnSpan = 'full'; @@ -31,13 +36,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'data', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') @@ -46,7 +51,7 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Average (ms)', + 'label' => __('general.average_ms'), 'data' => $results->map(fn ($item) => $item->download_latency_iqm), 'borderColor' => 'rgba(16, 185, 129)', 'backgroundColor' => 'rgba(16, 185, 129, 0.1)', @@ -57,7 +62,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'High (ms)', + 'label' => __('general.high_ms'), 'data' => $results->map(fn ($item) => $item->download_latency_high), 'borderColor' => 'rgba(14, 165, 233)', 'backgroundColor' => 'rgba(14, 165, 233, 0.1)', @@ -68,7 +73,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Low (ms)', + 'label' => __('general.low_ms'), 'data' => $results->map(fn ($item) => $item->download_latency_low), 'borderColor' => 'rgba(139, 92, 246)', 'backgroundColor' => 'rgba(139, 92, 246, 0.1)', diff --git a/app/Filament/Widgets/RecentJitterChartWidget.php b/app/Filament/Widgets/RecentJitterChartWidget.php index 50a84c37c..03dd59b13 100644 --- a/app/Filament/Widgets/RecentJitterChartWidget.php +++ b/app/Filament/Widgets/RecentJitterChartWidget.php @@ -11,7 +11,12 @@ class RecentJitterChartWidget extends ChartWidget { use HasChartFilters; - protected ?string $heading = 'Jitter'; + protected ?string $heading = null; + + public function getHeading(): ?string + { + return __('general.jitter'); + } protected int|string|array $columnSpan = 'full'; @@ -31,13 +36,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'data', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') @@ -46,7 +51,7 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Download (ms)', + 'label' => __('general.download_ms'), 'data' => $results->map(fn ($item) => $item->download_jitter), 'borderColor' => 'rgba(14, 165, 233)', 'backgroundColor' => 'rgba(14, 165, 233, 0.1)', @@ -57,7 +62,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Upload (ms)', + 'label' => __('general.upload_ms'), 'data' => $results->map(fn ($item) => $item->upload_jitter), 'borderColor' => 'rgba(139, 92, 246)', 'backgroundColor' => 'rgba(139, 92, 246, 0.1)', @@ -68,7 +73,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Ping (ms)', + 'label' => __('general.ping_ms_label'), 'data' => $results->map(fn ($item) => $item->ping_jitter), 'borderColor' => 'rgba(16, 185, 129)', 'backgroundColor' => 'rgba(16, 185, 129, 0.1)', diff --git a/app/Filament/Widgets/RecentPingChartWidget.php b/app/Filament/Widgets/RecentPingChartWidget.php index 55ad7f046..096a190ec 100644 --- a/app/Filament/Widgets/RecentPingChartWidget.php +++ b/app/Filament/Widgets/RecentPingChartWidget.php @@ -12,7 +12,12 @@ class RecentPingChartWidget extends ChartWidget { use HasChartFilters; - protected ?string $heading = 'Ping (ms)'; + protected ?string $heading = null; + + public function getHeading(): ?string + { + return __('general.ping_ms'); + } protected int|string|array $columnSpan = 'full'; @@ -32,13 +37,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'ping', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') @@ -47,7 +52,7 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Ping', + 'label' => __('general.ping'), 'data' => $results->map(fn ($item) => $item->ping), 'borderColor' => 'rgba(16, 185, 129)', 'backgroundColor' => 'rgba(16, 185, 129, 0.1)', @@ -58,7 +63,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Average', + 'label' => __('general.average'), 'data' => array_fill(0, count($results), Average::averagePing($results)), 'borderColor' => 'rgb(243, 7, 6, 1)', 'pointBackgroundColor' => 'rgb(243, 7, 6, 1)', diff --git a/app/Filament/Widgets/RecentUploadChartWidget.php b/app/Filament/Widgets/RecentUploadChartWidget.php index f3f31e3d6..1bb96eb04 100644 --- a/app/Filament/Widgets/RecentUploadChartWidget.php +++ b/app/Filament/Widgets/RecentUploadChartWidget.php @@ -13,7 +13,12 @@ class RecentUploadChartWidget extends ChartWidget { use HasChartFilters; - protected ?string $heading = 'Upload (Mbps)'; + protected ?string $heading = null; + + public function getHeading(): ?string + { + return __('general.upload_mbps'); + } protected int|string|array $columnSpan = 'full'; @@ -33,13 +38,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'upload', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') @@ -48,7 +53,7 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Upload', + 'label' => __('general.upload'), 'data' => $results->map(fn ($item) => ! blank($item->upload) ? Number::bitsToMagnitude(bits: $item->upload_bits, precision: 2, magnitude: 'mbit') : null), 'borderColor' => 'rgba(139, 92, 246)', 'backgroundColor' => 'rgba(139, 92, 246, 0.1)', @@ -59,7 +64,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Average', + 'label' => __('general.average'), 'data' => array_fill(0, count($results), Average::averageUpload($results)), 'borderColor' => 'rgb(243, 7, 6, 1)', 'pointBackgroundColor' => 'rgb(243, 7, 6, 1)', diff --git a/app/Filament/Widgets/RecentUploadLatencyChartWidget.php b/app/Filament/Widgets/RecentUploadLatencyChartWidget.php index 6bb761cc9..90315ddd9 100644 --- a/app/Filament/Widgets/RecentUploadLatencyChartWidget.php +++ b/app/Filament/Widgets/RecentUploadLatencyChartWidget.php @@ -11,7 +11,12 @@ class RecentUploadLatencyChartWidget extends ChartWidget { use HasChartFilters; - protected ?string $heading = 'Upload Latency'; + protected ?string $heading = null; + + public function getHeading(): ?string + { + return __('general.upload_latency'); + } protected int|string|array $columnSpan = 'full'; @@ -31,13 +36,13 @@ protected function getData(): array $results = Result::query() ->select(['id', 'data', 'created_at']) ->where('status', '=', ResultStatus::Completed) - ->when($this->filter == '24h', function ($query) { + ->when($this->filter === '24h', function ($query) { $query->where('created_at', '>=', now()->subDay()); }) - ->when($this->filter == 'week', function ($query) { + ->when($this->filter === 'week', function ($query) { $query->where('created_at', '>=', now()->subWeek()); }) - ->when($this->filter == 'month', function ($query) { + ->when($this->filter === 'month', function ($query) { $query->where('created_at', '>=', now()->subMonth()); }) ->orderBy('created_at') @@ -46,7 +51,7 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Average (ms)', + 'label' => __('general.average_ms'), 'data' => $results->map(fn ($item) => $item->upload_latency_iqm), 'borderColor' => 'rgba(16, 185, 129)', 'backgroundColor' => 'rgba(16, 185, 129, 0.1)', @@ -57,7 +62,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'High (ms)', + 'label' => __('general.high_ms'), 'data' => $results->map(fn ($item) => $item->upload_latency_high), 'borderColor' => 'rgba(14, 165, 233)', 'backgroundColor' => 'rgba(14, 165, 233, 0.1)', @@ -68,7 +73,7 @@ protected function getData(): array 'pointRadius' => count($results) <= 24 ? 3 : 0, ], [ - 'label' => 'Low (ms)', + 'label' => __('general.low_ms'), 'data' => $results->map(fn ($item) => $item->upload_latency_low), 'borderColor' => 'rgba(139, 92, 246)', 'backgroundColor' => 'rgba(139, 92, 246, 0.1)', diff --git a/app/Filament/Widgets/StatsOverviewWidget.php b/app/Filament/Widgets/StatsOverviewWidget.php deleted file mode 100644 index 62f712766..000000000 --- a/app/Filament/Widgets/StatsOverviewWidget.php +++ /dev/null @@ -1,76 +0,0 @@ -result = Result::query() - ->select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) - ->where('status', '=', ResultStatus::Completed) - ->latest() - ->first(); - - if (blank($this->result)) { - return [ - Stat::make('Latest download', '-') - ->icon('heroicon-o-arrow-down-tray'), - Stat::make('Latest upload', '-') - ->icon('heroicon-o-arrow-up-tray'), - Stat::make('Latest ping', '-') - ->icon('heroicon-o-clock'), - ]; - } - - $previous = Result::query() - ->select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) - ->where('id', '<', $this->result->id) - ->where('status', '=', ResultStatus::Completed) - ->latest() - ->first(); - - if (! $previous) { - return [ - Stat::make('Latest download', fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->download_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-down-tray'), - Stat::make('Latest upload', fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->upload_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-up-tray'), - Stat::make('Latest ping', fn (): string => ! blank($this->result) ? number_format($this->result->ping, 2).' ms' : 'n/a') - ->icon('heroicon-o-clock'), - ]; - } - - $downloadChange = percentChange($this->result->download, $previous->download, 2); - $uploadChange = percentChange($this->result->upload, $previous->upload, 2); - $pingChange = percentChange($this->result->ping, $previous->ping, 2); - - return [ - Stat::make('Latest download', fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->download_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-down-tray') - ->description($downloadChange > 0 ? $downloadChange.'% faster' : abs($downloadChange).'% slower') - ->descriptionIcon($downloadChange > 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down') - ->color($downloadChange > 0 ? 'success' : 'danger'), - Stat::make('Latest upload', fn (): string => ! blank($this->result) ? Number::toBitRate(bits: $this->result->upload_bits, precision: 2) : 'n/a') - ->icon('heroicon-o-arrow-up-tray') - ->description($uploadChange > 0 ? $uploadChange.'% faster' : abs($uploadChange).'% slower') - ->descriptionIcon($uploadChange > 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down') - ->color($uploadChange > 0 ? 'success' : 'danger'), - Stat::make('Latest ping', fn (): string => ! blank($this->result) ? number_format($this->result->ping, 2).' ms' : 'n/a') - ->icon('heroicon-o-clock') - ->description($pingChange > 0 ? $pingChange.'% slower' : abs($pingChange).'% faster') - ->descriptionIcon($pingChange > 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down') - ->color($pingChange > 0 ? 'danger' : 'success'), - ]; - } -} diff --git a/app/Http/Controllers/Api/V1/ResultsController.php b/app/Http/Controllers/Api/V1/ResultsController.php index 9c802fe84..5f462ae3c 100644 --- a/app/Http/Controllers/Api/V1/ResultsController.php +++ b/app/Http/Controllers/Api/V1/ResultsController.php @@ -28,7 +28,7 @@ public function list(Request $request) ); } $validator = Validator::make($request->all(), [ - 'per_page' => 'integer|min:1|max:500', + 'page.size' => 'integer|min:1|max:'.config('json-api-paginate.max_results'), ]); if ($validator->fails()) { @@ -65,7 +65,7 @@ public function list(Request $request) 'created_at', 'updated_at', ]) - ->jsonPaginate($request->input('per_page', 25)); + ->jsonPaginate(); return ResultResource::collection($results); } diff --git a/app/Http/Controllers/Api/V1/SpeedtestController.php b/app/Http/Controllers/Api/V1/SpeedtestController.php index a0b00a37a..d605d945c 100644 --- a/app/Http/Controllers/Api/V1/SpeedtestController.php +++ b/app/Http/Controllers/Api/V1/SpeedtestController.php @@ -37,7 +37,9 @@ public function __invoke(Request $request) } $result = RunSpeedtestAction::run( + scheduled: true, serverId: $request->input('server_id'), + dispatchedBy: $request->user()->id, ); return $this->sendResponse( diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 0b6d0e906..543c692e4 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use App\Enums\ResultStatus; -use App\Models\Result; use Illuminate\Http\Request; class HomeController extends Controller @@ -13,14 +11,6 @@ class HomeController extends Controller */ public function __invoke(Request $request) { - $latestResult = Result::query() - ->select(['id', 'ping', 'download', 'upload', 'status', 'created_at']) - ->where('status', '=', ResultStatus::Completed) - ->latest() - ->first(); - - return view('dashboard', [ - 'latestResult' => $latestResult, - ]); + return view('dashboard'); } } diff --git a/app/Http/Controllers/MetricsController.php b/app/Http/Controllers/MetricsController.php new file mode 100644 index 000000000..984be20d8 --- /dev/null +++ b/app/Http/Controllers/MetricsController.php @@ -0,0 +1,28 @@ +settings->prometheus_enabled) { + abort(404); + } + + $metrics = $this->metricsService->generateMetrics(); + + return response($metrics, 200, [ + 'Content-Type' => 'text/plain; version=0.0.4; charset=utf-8', + ]); + } +} diff --git a/app/Http/Integrations/Unifi/Requests/GetApplicationInformationRequest.php b/app/Http/Integrations/Unifi/Requests/GetApplicationInformationRequest.php new file mode 100644 index 000000000..6303a26f7 --- /dev/null +++ b/app/Http/Integrations/Unifi/Requests/GetApplicationInformationRequest.php @@ -0,0 +1,22 @@ +siteId.'/wans'; + } +} diff --git a/app/Http/Integrations/Unifi/UnifiConnector.php b/app/Http/Integrations/Unifi/UnifiConnector.php new file mode 100644 index 000000000..8d97ba7f8 --- /dev/null +++ b/app/Http/Integrations/Unifi/UnifiConnector.php @@ -0,0 +1,39 @@ + false, + ]; + } + + /** + * Default headers for the connector + */ + protected function defaultHeaders(): array + { + return [ + 'X-API-KEY' => config('services.unifi-api.token'), + ]; + } +} diff --git a/app/Http/Middleware/PrometheusAllowedIpMiddleware.php b/app/Http/Middleware/PrometheusAllowedIpMiddleware.php new file mode 100644 index 000000000..c599b8a6f --- /dev/null +++ b/app/Http/Middleware/PrometheusAllowedIpMiddleware.php @@ -0,0 +1,43 @@ +settings->prometheus_allowed_ips)) { + return $next($request); + } + + $clientIp = $request->ip(); + $allowedIps = $this->settings->prometheus_allowed_ips; + + foreach ($allowedIps as $allowedIp) { + if (str_contains($allowedIp, '/')) { + if (Network::ipInRange($clientIp, $allowedIp)) { + return $next($request); + } + } elseif ($clientIp === $allowedIp) { + return $next($request); + } + } + + abort(403); + } +} diff --git a/app/Http/Resources/V1/ResultResource.php b/app/Http/Resources/V1/ResultResource.php index e00710297..069a90e71 100644 --- a/app/Http/Resources/V1/ResultResource.php +++ b/app/Http/Resources/V1/ResultResource.php @@ -34,6 +34,7 @@ public function toArray(Request $request): array 'healthy' => $this->healthy, 'status' => $this->status, 'scheduled' => $this->scheduled, + 'dispatched_by' => $this->dispatched_by, 'comments' => $this->comments, 'data' => $this->data, 'created_at' => $this->created_at->toDateTimestring(), diff --git a/app/Jobs/CheckForInternetConnectionJob.php b/app/Jobs/CheckForInternetConnectionJob.php index 37c5e6f04..c0f39a61f 100644 --- a/app/Jobs/CheckForInternetConnectionJob.php +++ b/app/Jobs/CheckForInternetConnectionJob.php @@ -2,7 +2,7 @@ namespace App\Jobs; -use App\Actions\CheckInternetConnection; +use App\Actions\PingHostname; use App\Enums\ResultStatus; use App\Events\SpeedtestChecking; use App\Events\SpeedtestFailed; @@ -44,14 +44,18 @@ public function handle(): void SpeedtestChecking::dispatch($this->result); - if (CheckInternetConnection::run() !== false) { + $ping = PingHostname::run(); + + if ($ping->isSuccess()) { return; } + $message = sprintf('Failed to connected to hostname "%s". Error received "%s".', $ping->getHost(), $ping->error()?->value); + $this->result->update([ 'data->type' => 'log', 'data->level' => 'error', - 'data->message' => 'Failed to connect to the internet.', + 'data->message' => $message, 'status' => ResultStatus::Failed, ]); diff --git a/app/Jobs/Influxdb/v2/BulkWriteResults.php b/app/Jobs/Influxdb/v2/BulkWriteResults.php index 7171089d7..2c3c866b3 100644 --- a/app/Jobs/Influxdb/v2/BulkWriteResults.php +++ b/app/Jobs/Influxdb/v2/BulkWriteResults.php @@ -58,8 +58,8 @@ public function handle(): void ]); Notification::make() - ->title('Failed to build write to Influxdb.') - ->body('Check the logs for more details.') + ->title(__('settings/data_integration.influxdb_bulk_write_failed')) + ->body(__('settings/data_integration.influxdb_bulk_write_failed_body')) ->danger() ->sendToDatabase($this->user); @@ -74,8 +74,8 @@ public function handle(): void $writeApi->close(); Notification::make() - ->title('Finished bulk data load to Influxdb.') - ->body('Data has been sent to InfluxDB, check if the data was received.') + ->title(__('settings/data_integration.influxdb_bulk_write_success')) + ->body(__('settings/data_integration.influxdb_bulk_write_success_body')) ->success() ->sendToDatabase($this->user); } diff --git a/app/Jobs/Influxdb/v2/TestConnectionJob.php b/app/Jobs/Influxdb/v2/TestConnectionJob.php index 9f564aea0..1d758513c 100644 --- a/app/Jobs/Influxdb/v2/TestConnectionJob.php +++ b/app/Jobs/Influxdb/v2/TestConnectionJob.php @@ -46,8 +46,8 @@ public function handle(): void ]); Notification::make() - ->title('Influxdb test failed') - ->body('Check the logs for more details.') + ->title(__('settings/data_integration.influxdb_test_failed')) + ->body(__('settings/data_integration.influxdb_test_failed_body')) ->danger() ->sendToDatabase($this->user); @@ -59,8 +59,8 @@ public function handle(): void $writeApi->close(); Notification::make() - ->title('Successfully sent test data to Influxdb') - ->body('Test data has been sent to InfluxDB, check if the data was received.') + ->title(__('settings/data_integration.influxdb_test_success')) + ->body(__('settings/data_integration.influxdb_test_success_body')) ->success() ->sendToDatabase($this->user); } diff --git a/app/Jobs/Ookla/BenchmarkSpeedtestJob.php b/app/Jobs/Ookla/BenchmarkSpeedtestJob.php index c1298a147..683fe395b 100644 --- a/app/Jobs/Ookla/BenchmarkSpeedtestJob.php +++ b/app/Jobs/Ookla/BenchmarkSpeedtestJob.php @@ -3,10 +3,11 @@ namespace App\Jobs\Ookla; use App\Enums\ResultStatus; -use App\Events\SpeedtestBenchmarkFailed; +use App\Events\SpeedtestBenchmarkHealthy; use App\Events\SpeedtestBenchmarking; -use App\Events\SpeedtestBenchmarkPassed; +use App\Events\SpeedtestBenchmarkUnhealthy; use App\Helpers\Benchmark; +use App\Helpers\Number; use App\Models\Result; use App\Settings\ThresholdSettings; use Illuminate\Bus\Batchable; @@ -70,8 +71,8 @@ public function handle(): void ]); $this->healthy - ? SpeedtestBenchmarkPassed::dispatch($this->result) - : SpeedtestBenchmarkFailed::dispatch($this->result); + ? SpeedtestBenchmarkHealthy::dispatch($this->result) + : SpeedtestBenchmarkUnhealthy::dispatch($this->result); } private function benchmark(Result $result, ThresholdSettings $settings): array @@ -83,7 +84,8 @@ private function benchmark(Result $result, ThresholdSettings $settings): array 'bar' => 'min', 'passed' => Benchmark::bitrate($result->download, ['value' => $settings->absolute_download, 'unit' => 'mbps']), 'type' => 'absolute', - 'value' => $settings->absolute_download, + 'test_value' => Number::bitsToMagnitude(bits: $result->download_bits, precision: 0, magnitude: 'mbit'), + 'benchmark_value' => $settings->absolute_download, 'unit' => 'mbps', ]); @@ -97,7 +99,8 @@ private function benchmark(Result $result, ThresholdSettings $settings): array 'bar' => 'min', 'passed' => filter_var(Benchmark::bitrate($result->upload, ['value' => $settings->absolute_upload, 'unit' => 'mbps']), FILTER_VALIDATE_BOOLEAN), 'type' => 'absolute', - 'value' => $settings->absolute_upload, + 'test_value' => Number::bitsToMagnitude(bits: $result->upload_bits, precision: 0, magnitude: 'mbit'), + 'benchmark_value' => $settings->absolute_upload, 'unit' => 'mbps', ]); @@ -111,7 +114,8 @@ private function benchmark(Result $result, ThresholdSettings $settings): array 'bar' => 'max', 'passed' => Benchmark::ping($result->ping, ['value' => $settings->absolute_ping]), 'type' => 'absolute', - 'value' => $settings->absolute_ping, + 'test_value' => round($result->ping), + 'benchmark_value' => $settings->absolute_ping, 'unit' => 'ms', ]); diff --git a/app/Jobs/Ookla/RunSpeedtestJob.php b/app/Jobs/Ookla/RunSpeedtestJob.php index 486faab6f..61b371939 100644 --- a/app/Jobs/Ookla/RunSpeedtestJob.php +++ b/app/Jobs/Ookla/RunSpeedtestJob.php @@ -60,6 +60,7 @@ public function handle(): void 'speedtest', '--accept-license', '--accept-gdpr', + '--selection-details', '--format=json', $this->result->server_id ? '--server-id='.$this->result->server_id : null, config('speedtest.interface') ? '--interface='.config('speedtest.interface') : null, diff --git a/app/Jobs/Ookla/SkipSpeedtestJob.php b/app/Jobs/Ookla/SkipSpeedtestJob.php index 13c444133..773d4a793 100644 --- a/app/Jobs/Ookla/SkipSpeedtestJob.php +++ b/app/Jobs/Ookla/SkipSpeedtestJob.php @@ -4,6 +4,7 @@ use App\Actions\GetExternalIpAddress; use App\Enums\ResultStatus; +use App\Events\SpeedtestFailed; use App\Events\SpeedtestSkipped; use App\Helpers\Network; use App\Models\Result; @@ -39,16 +40,31 @@ public function middleware(): array public function handle(): void { /** - * Only skip IPs for scheduled tests. + * Skip if test is not scheduled or no IPs are configured to skip. */ - if ($this->result->scheduled === false) { + if ($this->result->scheduled === false || empty(config('speedtest.preflight.skip_ips'))) { return; } $externalIp = GetExternalIpAddress::run(); + if ($externalIp['ok'] === false) { + $this->result->update([ + 'data->type' => 'log', + 'data->level' => 'error', + 'data->message' => $externalIp['body'], + 'status' => ResultStatus::Failed, + ]); + + SpeedtestFailed::dispatch($this->result); + + $this->batch()->cancel(); + + return; + } + $shouldSkip = $this->shouldSkip( - externalIp: $externalIp, + externalIp: $externalIp['body'], ); if ($shouldSkip === false) { @@ -76,11 +92,11 @@ private function shouldSkip(string $externalIp): bool|string $skipIPs = array_filter( array_map( 'trim', - explode(',', config('speedtest.skip_ips')), + explode(',', config('speedtest.preflight.skip_ips')), ), ); - if (count($skipIPs) < 1) { + if (empty($skipIPs)) { return false; } diff --git a/app/Jobs/TruncateResults.php b/app/Jobs/TruncateResults.php deleted file mode 100644 index 18f41f3e2..000000000 --- a/app/Jobs/TruncateResults.php +++ /dev/null @@ -1,48 +0,0 @@ -truncate(); - } catch (Throwable $th) { - $this->fail($th); - - return; - } - - Notification::make() - ->title('Results table truncated!') - ->success() - ->sendToDatabase($this->user); - } -} diff --git a/app/Listeners/Database/SendSpeedtestCompletedNotification.php b/app/Listeners/Database/SendSpeedtestCompletedNotification.php deleted file mode 100644 index 14ea66605..000000000 --- a/app/Listeners/Database/SendSpeedtestCompletedNotification.php +++ /dev/null @@ -1,34 +0,0 @@ -database_enabled) { - return; - } - - if (! $notificationSettings->database_on_speedtest_run) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title('Speedtest completed') - ->success() - ->sendToDatabase($user); - } - } -} diff --git a/app/Listeners/Database/SendSpeedtestThresholdNotification.php b/app/Listeners/Database/SendSpeedtestThresholdNotification.php deleted file mode 100644 index 7439b52f1..000000000 --- a/app/Listeners/Database/SendSpeedtestThresholdNotification.php +++ /dev/null @@ -1,101 +0,0 @@ -database_enabled) { - return; - } - - if (! $notificationSettings->database_on_threshold_failure) { - return; - } - - $thresholdSettings = new ThresholdSettings; - - if (! $thresholdSettings->absolute_enabled) { - return; - } - - if ($thresholdSettings->absolute_download > 0) { - $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings); - } - - if ($thresholdSettings->absolute_upload > 0) { - $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings); - } - - if ($thresholdSettings->absolute_ping > 0) { - $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings); - } - } - - /** - * Send database notification if absolute download threshold is breached. - */ - protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): void - { - if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title('Download threshold breached!') - ->body('Speedtest #'.$event->result->id.' breached the download threshold of '.$thresholdSettings->absolute_download.' Mbps at '.Number::toBitRate($event->result->download_bits).'.') - ->warning() - ->sendToDatabase($user); - } - } - - /** - * Send database notification if absolute upload threshold is breached. - */ - protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): void - { - if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title('Upload threshold breached!') - ->body('Speedtest #'.$event->result->id.' breached the upload threshold of '.$thresholdSettings->absolute_upload.' Mbps at '.Number::toBitRate($event->result->upload_bits).'.') - ->warning() - ->sendToDatabase($user); - } - } - - /** - * Send database notification if absolute upload threshold is breached. - */ - protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): void - { - if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { - return; - } - - foreach (User::all() as $user) { - Notification::make() - ->title('Ping threshold breached!') - ->body('Speedtest #'.$event->result->id.' breached the ping threshold of '.$thresholdSettings->absolute_ping.'ms at '.$event->result->ping.'ms.') - ->warning() - ->sendToDatabase($user); - } - } -} diff --git a/app/Listeners/Mail/SendSpeedtestCompletedNotification.php b/app/Listeners/Mail/SendSpeedtestCompletedNotification.php deleted file mode 100644 index 2e731cd99..000000000 --- a/app/Listeners/Mail/SendSpeedtestCompletedNotification.php +++ /dev/null @@ -1,39 +0,0 @@ -mail_enabled) { - return; - } - - if (! $notificationSettings->mail_on_speedtest_run) { - return; - } - - if (! count($notificationSettings->mail_recipients)) { - Log::warning('Mail recipients not found, check mail notification channel settings.'); - - return; - } - - foreach ($notificationSettings->mail_recipients as $recipient) { - Mail::to($recipient) - ->send(new SpeedtestCompletedMail($event->result)); - } - } -} diff --git a/app/Listeners/Mail/SendSpeedtestThresholdNotification.php b/app/Listeners/Mail/SendSpeedtestThresholdNotification.php deleted file mode 100644 index 774851df5..000000000 --- a/app/Listeners/Mail/SendSpeedtestThresholdNotification.php +++ /dev/null @@ -1,117 +0,0 @@ -mail_enabled) { - return; - } - - if (! $notificationSettings->mail_on_threshold_failure) { - return; - } - - if (! count($notificationSettings->mail_recipients) > 0) { - Log::warning('Mail recipients not found, check mail notification channel settings.'); - - return; - } - - $thresholdSettings = new ThresholdSettings; - - if (! $thresholdSettings->absolute_enabled) { - return; - } - - $failed = []; - - if ($thresholdSettings->absolute_download > 0) { - array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_upload > 0) { - array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_ping > 0) { - array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - $failed = array_filter($failed); - - if (! count($failed)) { - Log::warning('Failed mail thresholds not found, won\'t send notification.'); - - return; - } - - foreach ($notificationSettings->mail_recipients as $recipient) { - Mail::to($recipient) - ->send(new SpeedtestThresholdMail($event->result, $failed)); - } - } - - /** - * Build mail notification if absolute download threshold is breached. - */ - protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { - return false; - } - - return [ - 'name' => 'Download', - 'threshold' => $thresholdSettings->absolute_download.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2), - ]; - } - - /** - * Build mail notification if absolute upload threshold is breached. - */ - protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { - return false; - } - - return [ - 'name' => 'Upload', - 'threshold' => $thresholdSettings->absolute_upload.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2), - ]; - } - - /** - * Build mail notification if absolute ping threshold is breached. - */ - protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { - return false; - } - - return [ - 'name' => 'Ping', - 'threshold' => $thresholdSettings->absolute_ping.' ms', - 'value' => round($event->result->ping, 2).' ms', - ]; - } -} diff --git a/app/Listeners/ProcessCompletedSpeedtest.php b/app/Listeners/ProcessCompletedSpeedtest.php new file mode 100644 index 000000000..87b7b7794 --- /dev/null +++ b/app/Listeners/ProcessCompletedSpeedtest.php @@ -0,0 +1,178 @@ +result; + + if ($result->healthy === false) { + return; + } + + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } + + $this->notifyAppriseChannels($result); + $this->notifyDatabaseChannels($result); + $this->notifyMailChannels($result); + $this->notifyWebhookChannels($result); + } + + /** + * Notify Apprise channels. + */ + private function notifyAppriseChannels(Result $result): void + { + // Check if Apprise notifications are enabled. + if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_speedtest_run) { + return; + } + + if (! count($this->notificationSettings->apprise_channel_urls)) { + Log::warning('Apprise channel URLs not found, check Apprise notification channel settings.'); + + return; + } + + // Build the speedtest data + $body = view('apprise.speedtest-completed', [ + 'id' => $result->id, + 'service' => Str::title($result->service->getLabel()), + 'serverName' => $result->server_name, + 'serverId' => $result->server_id, + 'isp' => $result->isp, + 'ping' => round($result->ping).' ms', + 'download' => Number::toBitRate(bits: $result->download_bits, precision: 2), + 'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2), + 'packetLoss' => $result->packet_loss, + 'speedtest_url' => $result->result_url, + 'url' => url('/admin/results'), + ])->render(); + + $title = 'Speedtest Completed – #'.$result->id; + + // Send notification to each configured channel URL + foreach ($this->notificationSettings->apprise_channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + Log::warning('Skipping entry with missing channel_url.'); + + continue; + } + + Notification::route('apprise_urls', $channelUrl) + ->notify(new SpeedtestNotification($title, $body, 'info', 'markdown')); + } + } + + /** + * Notify database channels. + */ + private function notifyDatabaseChannels(Result $result): void + { + // Check if database notifications are enabled. + if (! $this->notificationSettings->database_enabled || ! $this->notificationSettings->database_on_speedtest_run) { + return; + } + + foreach (User::all() as $user) { + FilamentNotification::make() + ->title(__('results.speedtest_completed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->success() + ->sendToDatabase($user); + } + } + + /** + * Notify mail channels. + */ + private function notifyMailChannels(Result $result): void + { + if (! $this->notificationSettings->mail_enabled || ! $this->notificationSettings->mail_on_speedtest_run) { + return; + } + + if (! count($this->notificationSettings->mail_recipients)) { + Log::warning('Mail recipients not found, check mail notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->mail_recipients as $recipient) { + Mail::to($recipient) + ->send(new CompletedSpeedtestMail($result)); + } + } + + /** + * Notify webhook channels. + */ + private function notifyWebhookChannels(Result $result): void + { + // Check if webhook notifications are enabled. + if (! $this->notificationSettings->webhook_enabled || ! $this->notificationSettings->webhook_on_speedtest_run) { + return; + } + + // Check if webhook urls are configured. + if (! count($this->notificationSettings->webhook_urls)) { + Log::warning('Webhook urls not found, check webhook notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->webhook_urls as $url) { + WebhookCall::create() + ->url($url['url']) + ->payload([ + 'result_id' => $result->id, + 'site_name' => config('app.name'), + 'server_name' => Arr::get($result->data, 'server.name'), + 'server_id' => Arr::get($result->data, 'server.id'), + 'status' => $result->status, + 'isp' => Arr::get($result->data, 'isp'), + 'ping' => round($result->ping), + 'download' => Number::bitsToMagnitude(bits: $result->download_bits, precision: 0, magnitude: 'mbit'), + 'upload' => Number::bitsToMagnitude(bits: $result->upload_bits, precision: 0, magnitude: 'mbit'), + 'packet_loss' => Arr::get($result->data, 'packetLoss'), + 'speedtest_url' => Arr::get($result->data, 'result.url'), + 'url' => url('/admin/results'), + ]) + ->doNotSign() + ->dispatch(); + } + } +} diff --git a/app/Listeners/ProcessFailedSpeedtest.php b/app/Listeners/ProcessFailedSpeedtest.php new file mode 100644 index 000000000..3cc4d9cd8 --- /dev/null +++ b/app/Listeners/ProcessFailedSpeedtest.php @@ -0,0 +1,32 @@ +result; + + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } + + // $this->notifyAppriseChannels($result); + } + + /** + * Notify Apprise channels. + */ + private function notifyAppriseChannels(Result $result): void + { + // + } +} diff --git a/app/Listeners/ProcessSpeedtestDataIntegrations.php b/app/Listeners/ProcessSpeedtestDataIntegrations.php new file mode 100644 index 000000000..8b2360b9f --- /dev/null +++ b/app/Listeners/ProcessSpeedtestDataIntegrations.php @@ -0,0 +1,33 @@ +settings->influxdb_v2_enabled) { + WriteResult::dispatch($event->result); + } + + if ($this->settings->prometheus_enabled) { + Cache::forever('prometheus:latest_result', $event->result->id); + } + } +} diff --git a/app/Listeners/ProcessUnhealthySpeedtest.php b/app/Listeners/ProcessUnhealthySpeedtest.php new file mode 100644 index 000000000..288f60739 --- /dev/null +++ b/app/Listeners/ProcessUnhealthySpeedtest.php @@ -0,0 +1,205 @@ +result; + + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } + + $this->notifyAppriseChannels($result); + $this->notifyDatabaseChannels($result); + $this->notifyMailChannels($result); + $this->notifyWebhookChannels($result); + } + + /** + * Notify Apprise channels. + */ + private function notifyAppriseChannels(Result $result): void + { + if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_threshold_failure) { + return; + } + + if (! count($this->notificationSettings->apprise_channel_urls)) { + Log::warning('Apprise channel URLs not found, check Apprise notification channel settings.'); + + return; + } + + if (empty($result->benchmarks)) { + Log::warning('Benchmark data not found, won\'t send Apprise notification.'); + + return; + } + + // Build metrics array from failed benchmarks + $failed = []; + + foreach ($result->benchmarks as $metric => $benchmark) { + if ($benchmark['passed'] === false) { + $failed[] = [ + 'name' => ucfirst($metric), + 'threshold' => $benchmark['value'].' '.$benchmark['unit'], + 'value' => $this->formatMetricValue($metric, $result), + ]; + } + } + + if (! count($failed)) { + Log::warning('No failed thresholds found in benchmarks, won\'t send Apprise notification.'); + + return; + } + + $body = view('apprise.speedtest-threshold', [ + 'id' => $result->id, + 'service' => Str::title($result->service->getLabel()), + 'serverName' => $result->server_name, + 'serverId' => $result->server_id, + 'isp' => $result->isp, + 'metrics' => $failed, + 'speedtest_url' => $result->result_url, + 'url' => url('/admin/results'), + ])->render(); + + $title = 'Speedtest Threshold Breach – #'.$result->id; + + // Send notification to each configured channel URL + foreach ($this->notificationSettings->apprise_channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + Log::warning('Skipping entry with missing channel_url.'); + + continue; + } + + Notification::route('apprise_urls', $channelUrl) + ->notify(new SpeedtestNotification($title, $body, 'warning', 'markdown')); + } + } + + /** + * Format metric value for display in notification. + */ + private function formatMetricValue(string $metric, Result $result): string + { + return match ($metric) { + 'download' => Number::toBitRate(bits: $result->download_bits, precision: 2), + 'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2), + 'ping' => round($result->ping, 2).' ms', + default => '', + }; + } + + /** + * Notify database channels. + */ + private function notifyDatabaseChannels(Result $result): void + { + // Check if database notifications are enabled. + if (! $this->notificationSettings->database_enabled || ! $this->notificationSettings->database_on_threshold_failure) { + return; + } + + foreach (User::all() as $user) { + FilamentNotification::make() + ->title(__('results.speedtest_benchmark_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->success() + ->sendToDatabase($user); + } + } + + /** + * Notify mail channels. + */ + private function notifyMailChannels(Result $result): void + { + // Check if mail notifications are enabled. + if (! $this->notificationSettings->mail_enabled || ! $this->notificationSettings->mail_on_threshold_failure) { + return; + } + + // Check if mail recipients are configured. + if (! count($this->notificationSettings->mail_recipients)) { + Log::warning('Mail recipients not found, check mail notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->mail_recipients as $recipient) { + Mail::to($recipient) + ->send(new UnhealthySpeedtestMail($result)); + } + } + + /** + * Notify webhook channels. + */ + private function notifyWebhookChannels(Result $result): void + { + // Check if webhook notifications are enabled. + if (! $this->notificationSettings->webhook_enabled || ! $this->notificationSettings->webhook_on_threshold_failure) { + return; + } + + // Check if webhook urls are configured. + if (! count($this->notificationSettings->webhook_urls)) { + Log::warning('Webhook urls not found, check webhook notification channel settings.'); + + return; + } + + foreach ($this->notificationSettings->webhook_urls as $url) { + WebhookCall::create() + ->url($url['url']) + ->payload([ + 'result_id' => $result->id, + 'site_name' => config('app.name'), + 'isp' => $result->isp, + 'benchmarks' => $result->benchmarks, + 'speedtest_url' => $result->result_url, + 'url' => url('/admin/results'), + ]) + ->doNotSign() + ->dispatch(); + } + } +} diff --git a/app/Listeners/SpeedtestEventSubscriber.php b/app/Listeners/SpeedtestEventSubscriber.php deleted file mode 100644 index 7e81bc30e..000000000 --- a/app/Listeners/SpeedtestEventSubscriber.php +++ /dev/null @@ -1,52 +0,0 @@ -influxdb_v2_enabled) { - WriteResult::dispatch($event->result); - } - } - - /** - * Handle speedtest completed events. - */ - public function handleSpeedtestCompleted(SpeedtestCompleted $event): void - { - $settings = app(DataIntegrationSettings::class); - - if ($settings->influxdb_v2_enabled) { - WriteResult::dispatch($event->result); - } - } - - /** - * Register the listeners for the subscriber. - */ - public function subscribe(Dispatcher $events): void - { - $events->listen( - SpeedtestFailed::class, - [SpeedtestEventSubscriber::class, 'handleSpeedtestFailed'] - ); - - $events->listen( - SpeedtestCompleted::class, - [SpeedtestEventSubscriber::class, 'handleSpeedtestCompleted'] - ); - } -} diff --git a/app/Listeners/UserNotificationSubscriber.php b/app/Listeners/UserNotificationSubscriber.php new file mode 100644 index 000000000..aefa0eaae --- /dev/null +++ b/app/Listeners/UserNotificationSubscriber.php @@ -0,0 +1,104 @@ +result; + + if (empty($result->dispatched_by)) { + return; + } + + $result->loadMissing('dispatchedBy'); + + Notification::make() + ->title(__('results.speedtest_completed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->success() + ->sendToDatabase($result->dispatchedBy); + } + + /** + * Handle the event. + */ + public function handleBenchmarkFailed(SpeedtestBenchmarkUnhealthy $event): void + { + $result = $event->result; + + if (empty($result->dispatched_by)) { + return; + } + + // Don't send notifications for unscheduled speedtests. + if ($result->unscheduled) { + return; + } + + $result->loadMissing('dispatchedBy'); + + Notification::make() + ->title(__('results.speedtest_benchmark_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->warning() + ->sendToDatabase($result->dispatchedBy); + } + + /** + * Handle the event. + */ + public function handleFailed(SpeedtestFailed $event): void + { + $result = $event->result; + + if (empty($result->dispatched_by)) { + return; + } + + $result->loadMissing('dispatchedBy'); + + Notification::make() + ->title(__('results.speedtest_failed')) + ->actions([ + Action::make('view') + ->label(__('general.view')) + ->url(route('filament.admin.resources.results.index')), + ]) + ->warning() + ->sendToDatabase($result->dispatchedBy); + } + + /** + * Register the listeners for the subscriber. + * + * @return array + */ + public function subscribe(Dispatcher $events): array + { + return [ + SpeedtestCompleted::class => 'handleCompleted', + SpeedtestBenchmarkUnhealthy::class => 'handleBenchmarkFailed', + SpeedtestFailed::class => 'handleFailed', + ]; + } +} diff --git a/app/Listeners/Webhook/SendSpeedtestCompletedNotification.php b/app/Listeners/Webhook/SendSpeedtestCompletedNotification.php deleted file mode 100644 index bee0668d6..000000000 --- a/app/Listeners/Webhook/SendSpeedtestCompletedNotification.php +++ /dev/null @@ -1,54 +0,0 @@ -webhook_enabled) { - return; - } - - if (! $notificationSettings->webhook_on_speedtest_run) { - return; - } - - if (! count($notificationSettings->webhook_urls)) { - Log::warning('Webhook urls not found, check webhook notification channel settings.'); - - return; - } - - foreach ($notificationSettings->webhook_urls as $url) { - WebhookCall::create() - ->url($url['url']) - ->payload([ - 'result_id' => $event->result->id, - 'site_name' => config('app.name'), - 'server_name' => Arr::get($event->result->data, 'server.name'), - 'server_id' => Arr::get($event->result->data, 'server.id'), - 'isp' => Arr::get($event->result->data, 'isp'), - 'ping' => $event->result->ping, - 'download' => $event->result->downloadBits, - 'upload' => $event->result->uploadBits, - 'packet_loss' => Arr::get($event->result->data, 'packetLoss'), - 'speedtest_url' => Arr::get($event->result->data, 'result.url'), - 'url' => url('/admin/results'), - ]) - ->doNotSign() - ->dispatch(); - } - } -} diff --git a/app/Listeners/Webhook/SendSpeedtestThresholdNotification.php b/app/Listeners/Webhook/SendSpeedtestThresholdNotification.php deleted file mode 100644 index bb64866b4..000000000 --- a/app/Listeners/Webhook/SendSpeedtestThresholdNotification.php +++ /dev/null @@ -1,126 +0,0 @@ -webhook_enabled) { - return; - } - - if (! $notificationSettings->webhook_on_threshold_failure) { - return; - } - - if (! count($notificationSettings->webhook_urls)) { - Log::warning('Webhook urls not found, check webhook notification channel settings.'); - - return; - } - - $thresholdSettings = new ThresholdSettings; - - if (! $thresholdSettings->absolute_enabled) { - return; - } - - $failed = []; - - if ($thresholdSettings->absolute_download > 0) { - array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_upload > 0) { - array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - if ($thresholdSettings->absolute_ping > 0) { - array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings)); - } - - $failed = array_filter($failed); - - if (! count($failed)) { - Log::warning('Failed webhook thresholds not found, won\'t send notification.'); - - return; - } - - foreach ($notificationSettings->webhook_urls as $url) { - WebhookCall::create() - ->url($url['url']) - ->payload([ - 'result_id' => $event->result->id, - 'site_name' => config('app.name'), - 'isp' => $event->result->isp, - 'metrics' => $failed, - 'speedtest_url' => $event->result->result_url, - 'url' => url('/admin/results'), - ]) - ->doNotSign() - ->dispatch(); - } - } - - /** - * Build webhook notification if absolute download threshold is breached. - */ - protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { - return false; - } - - return [ - 'name' => 'Download', - 'threshold' => $thresholdSettings->absolute_download.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2), - ]; - } - - /** - * Build webhook notification if absolute upload threshold is breached. - */ - protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { - return false; - } - - return [ - 'name' => 'Upload', - 'threshold' => $thresholdSettings->absolute_upload.' Mbps', - 'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2), - ]; - } - - /** - * Build webhook notification if absolute ping threshold is breached. - */ - protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array - { - if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { - return false; - } - - return [ - 'name' => 'Ping', - 'threshold' => $thresholdSettings->absolute_ping.' ms', - 'value' => round($event->result->ping, 2).' ms', - ]; - } -} diff --git a/app/Livewire/DeprecatedNotificationChannelsBanner.php b/app/Livewire/DeprecatedNotificationChannelsBanner.php new file mode 100644 index 000000000..84533de19 --- /dev/null +++ b/app/Livewire/DeprecatedNotificationChannelsBanner.php @@ -0,0 +1,66 @@ +discord_enabled + || $settings->gotify_enabled + || $settings->healthcheck_enabled + || $settings->ntfy_enabled + || $settings->pushover_enabled + || $settings->slack_enabled + || $settings->telegram_enabled; + } + + #[Computed] + public function deprecatedChannelsList(): array + { + $settings = app(NotificationSettings::class); + $channels = []; + + if ($settings->discord_enabled) { + $channels[] = 'Discord'; + } + + if ($settings->gotify_enabled) { + $channels[] = 'Gotify'; + } + + if ($settings->healthcheck_enabled) { + $channels[] = 'Healthchecks'; + } + + if ($settings->ntfy_enabled) { + $channels[] = 'Ntfy'; + } + + if ($settings->pushover_enabled) { + $channels[] = 'Pushover'; + } + + if ($settings->slack_enabled) { + $channels[] = 'Slack'; + } + + if ($settings->telegram_enabled) { + $channels[] = 'Telegram'; + } + + return $channels; + } + + public function render() + { + return view('livewire.deprecated-notification-channels-banner'); + } +} diff --git a/app/Livewire/LatestResultStats.php b/app/Livewire/LatestResultStats.php new file mode 100644 index 000000000..64217b8ab --- /dev/null +++ b/app/Livewire/LatestResultStats.php @@ -0,0 +1,24 @@ +latest() + ->first(); + } + + public function render() + { + return view('livewire.latest-result-stats'); + } +} diff --git a/app/Livewire/NextSpeedtestBanner.php b/app/Livewire/NextSpeedtestBanner.php new file mode 100644 index 000000000..2e20874ac --- /dev/null +++ b/app/Livewire/NextSpeedtestBanner.php @@ -0,0 +1,22 @@ +count(); + $failedResults = Result::where('status', ResultStatus::Failed)->count(); + + return [ + 'total' => Number::format($totalResults), + 'completed' => Number::format($completedResults), + 'failed' => Number::format($failedResults), + ]; + } + + public function render() + { + return view('livewire.platform-stats'); + } +} diff --git a/app/Livewire/Topbar/RunSpeedtestAction.php b/app/Livewire/Topbar/Actions.php similarity index 63% rename from app/Livewire/Topbar/RunSpeedtestAction.php rename to app/Livewire/Topbar/Actions.php index b615b5f2f..9077724c8 100644 --- a/app/Livewire/Topbar/RunSpeedtestAction.php +++ b/app/Livewire/Topbar/Actions.php @@ -13,21 +13,23 @@ use Filament\Forms\Contracts\HasForms; use Filament\Notifications\Notification; use Filament\Support\Enums\IconPosition; +use Filament\Support\Enums\Size; use Illuminate\Support\Facades\Auth; use Livewire\Component; -class RunSpeedtestAction extends Component implements HasActions, HasForms +class Actions extends Component implements HasActions, HasForms { use InteractsWithActions, InteractsWithForms; + public bool $showDashboard = true; + public function dashboardAction(): Action { - return Action::make('home') - ->label('Public Dashboard') - ->icon('heroicon-o-chart-bar') - ->iconPosition(IconPosition::Before) + return Action::make('metrics') + ->iconButton() + ->icon('tabler-chart-histogram') ->color('gray') - ->url(shouldOpenInNewTab: true, url: route('home')) + ->url(url: route('home')) ->extraAttributes([ 'id' => 'dashboardAction', ]); @@ -38,12 +40,12 @@ public function speedtestAction(): Action return Action::make('speedtest') ->schema([ Select::make('server_id') - ->label('Select Server') - ->helperText('Leave empty to run the speedtest without specifying a server. Blocked servers will be skipped.') + ->label(__('results.select_server')) + ->helperText(__('results.select_server_helper')) ->options(function (): array { return array_filter([ - 'Manual servers' => Ookla::getConfigServers(), - 'Closest servers' => GetOoklaSpeedtestServers::run(), + __('results.manual_servers') => Ookla::getConfigServers(), + __('results.closest_servers') => GetOoklaSpeedtestServers::run(), ]); }) ->searchable(), @@ -53,20 +55,22 @@ public function speedtestAction(): Action RunSpeedtest::run( serverId: $serverId, + dispatchedBy: Auth::id(), ); Notification::make() - ->title('Speedtest started') + ->title(__('results.speedtest_started')) ->success() ->send(); }) - ->modalHeading('Run Speedtest') + ->modalHeading(__('results.speedtest')) ->modalWidth('lg') - ->modalSubmitActionLabel('Start') + ->modalSubmitActionLabel(__('results.start')) ->button() + ->size(request()->is('filament*') ? Size::Medium : Size::Large) ->color('primary') - ->label('Speedtest') - ->icon('heroicon-o-rocket-launch') + ->label(__('results.speedtest')) + ->icon('tabler-rocket') ->iconPosition(IconPosition::Before) ->hidden(! Auth::check() && Auth::user()->is_admin) ->extraAttributes([ @@ -76,6 +80,6 @@ public function speedtestAction(): Action public function render() { - return view('livewire.topbar.run-speedtest-action'); + return view('livewire.topbar.actions'); } } diff --git a/app/Mail/SpeedtestCompletedMail.php b/app/Mail/CompletedSpeedtestMail.php similarity index 93% rename from app/Mail/SpeedtestCompletedMail.php rename to app/Mail/CompletedSpeedtestMail.php index 6f7295771..109d95360 100644 --- a/app/Mail/SpeedtestCompletedMail.php +++ b/app/Mail/CompletedSpeedtestMail.php @@ -12,7 +12,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; -class SpeedtestCompletedMail extends Mailable implements ShouldQueue +class CompletedSpeedtestMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; @@ -41,7 +41,7 @@ public function envelope(): Envelope public function content(): Content { return new Content( - markdown: 'emails.speedtest-completed', + markdown: 'mail.speedtest.completed', with: [ 'id' => $this->result->id, 'service' => Str::title($this->result->service->getLabel()), diff --git a/app/Mail/SpeedtestThresholdMail.php b/app/Mail/SpeedtestThresholdMail.php deleted file mode 100644 index 94ad14af9..000000000 --- a/app/Mail/SpeedtestThresholdMail.php +++ /dev/null @@ -1,57 +0,0 @@ -result->id, - ); - } - - /** - * Get the message content definition. - */ - public function content(): Content - { - return new Content( - markdown: 'emails.speedtest-threshold', - with: [ - 'id' => $this->result->id, - 'service' => Str::title($this->result->service->getLabel()), - 'serverName' => $this->result->server_name, - 'serverId' => $this->result->server_id, - 'isp' => $this->result->isp, - 'speedtest_url' => $this->result->result_url, - 'url' => url('/admin/results'), - 'metrics' => $this->metrics, - ], - ); - } -} diff --git a/app/Mail/Test.php b/app/Mail/TestMail.php similarity index 87% rename from app/Mail/Test.php rename to app/Mail/TestMail.php index 5fe6fd88b..c6e43a269 100644 --- a/app/Mail/Test.php +++ b/app/Mail/TestMail.php @@ -9,7 +9,7 @@ use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; -class Test extends Mailable implements ShouldQueue +class TestMail extends Mailable implements ShouldQueue { use Queueable, SerializesModels; @@ -29,7 +29,7 @@ public function envelope(): Envelope public function content(): Content { return new Content( - markdown: 'emails.test', + markdown: 'mail.test', ); } } diff --git a/app/Mail/UnhealthySpeedtestMail.php b/app/Mail/UnhealthySpeedtestMail.php new file mode 100644 index 000000000..e22f4ec28 --- /dev/null +++ b/app/Mail/UnhealthySpeedtestMail.php @@ -0,0 +1,83 @@ +result->id, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + $benchmarks = []; + + foreach ($this->result->benchmarks as $metric => $benchmark) { + $benchmarks[] = $this->formatBenchmark($metric, $benchmark); + } + + return new Content( + markdown: 'mail.speedtest.unhealthy', + with: [ + 'id' => $this->result->id, + 'service' => str($this->result->service->getLabel())->title(), + 'isp' => $this->result->isp, + 'url' => url('/admin/results'), + 'benchmarks' => $benchmarks, + ], + ); + } + + /** + * Format a benchmark for display in the email. + */ + private function formatBenchmark(string $metric, array $benchmark): array + { + $metricName = str($metric)->title(); + $type = str($benchmark['type'])->title(); + $thresholdValue = $benchmark['value'].' '.str($benchmark['unit'])->title(); + + // Get the actual result value + $resultValue = match ($metric) { + 'download' => Number::toBitRate($this->result->download_bits, 2), + 'upload' => Number::toBitRate($this->result->upload_bits, 2), + 'ping' => round(Number::castToType($this->result->ping, 'float'), 2).' ms', + default => 'N/A', + }; + + return [ + 'metric' => $metricName, + 'type' => $type, + 'threshold_value' => $thresholdValue, + 'result_value' => $resultValue, + 'passed' => $benchmark['passed'], + ]; + } +} diff --git a/app/Models/Result.php b/app/Models/Result.php index 3ac4caa77..084c04097 100644 --- a/app/Models/Result.php +++ b/app/Models/Result.php @@ -6,9 +6,11 @@ use App\Enums\ResultStatus; use App\Models\Traits\ResultDataAttributes; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class Result extends Model { @@ -45,4 +47,22 @@ public function prunable(): Builder { return static::where('created_at', '<=', now()->subDays(config('speedtest.prune_results_older_than'))); } + + /** + * Get the user who dispatched this speedtest. + */ + public function dispatchedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'dispatched_by'); + } + + /** + * Determine if the result was unscheduled. + */ + protected function unscheduled(): Attribute + { + return Attribute::make( + get: fn (): bool => ! $this->scheduled, + ); + } } diff --git a/app/Notifications/Apprise/AppriseMessage.php b/app/Notifications/Apprise/AppriseMessage.php new file mode 100644 index 000000000..a510ded7b --- /dev/null +++ b/app/Notifications/Apprise/AppriseMessage.php @@ -0,0 +1,66 @@ +urls = $urls; + + return $this; + } + + public function title(string $title): self + { + $this->title = $title; + + return $this; + } + + public function body(string $body): self + { + $this->body = $body; + + return $this; + } + + public function type(string $type): self + { + $this->type = $type; + + return $this; + } + + public function format(string $format): self + { + $this->format = $format; + + return $this; + } + + public function tag(string $tag): self + { + $this->tag = $tag; + + return $this; + } +} diff --git a/app/Notifications/Apprise/SpeedtestNotification.php b/app/Notifications/Apprise/SpeedtestNotification.php new file mode 100644 index 000000000..710466e57 --- /dev/null +++ b/app/Notifications/Apprise/SpeedtestNotification.php @@ -0,0 +1,42 @@ + + */ + public function via(object $notifiable): array + { + return ['apprise']; + } + + /** + * Get the Apprise message representation of the notification. + */ + public function toApprise(object $notifiable): AppriseMessage + { + return AppriseMessage::create() + ->urls($notifiable->routes['apprise_urls']) + ->title($this->title) + ->body($this->body) + ->type($this->type) + ->format($this->format); + } +} diff --git a/app/Notifications/Apprise/TestNotification.php b/app/Notifications/Apprise/TestNotification.php new file mode 100644 index 000000000..8ba9011cc --- /dev/null +++ b/app/Notifications/Apprise/TestNotification.php @@ -0,0 +1,38 @@ + + */ + public function via(object $notifiable): array + { + return ['apprise']; + } + + /** + * Get the Apprise message representation of the notification. + */ + public function toApprise(object $notifiable): AppriseMessage + { + $body = '👋 This is a test notification from **'.config('app.name')."**.\n\n"; + $body .= "If you're seeing this, your Apprise notification channel is configured correctly!\n\n"; + + return AppriseMessage::create() + ->urls($notifiable->routes['apprise_urls']) + ->title('Test Notification') + ->body($body) + ->type('info') + ->format('markdown'); + } +} diff --git a/app/Notifications/AppriseChannel.php b/app/Notifications/AppriseChannel.php index af5ac3683..c6fe6a1c3 100644 --- a/app/Notifications/AppriseChannel.php +++ b/app/Notifications/AppriseChannel.php @@ -2,9 +2,12 @@ namespace App\Notifications; +use App\Settings\NotificationSettings; +use Exception; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Throwable; class AppriseChannel { @@ -20,35 +23,54 @@ public function send(object $notifiable, Notification $notification): void return; } - $appriseUrl = config('services.apprise.url'); + $settings = app(NotificationSettings::class); + $appriseUrl = $settings->apprise_server_url ?? ''; + + if (empty($appriseUrl)) { + Log::warning('Apprise notification skipped: No Server URL configured'); + + return; + } try { - $response = Http::timeout(5) + $request = Http::timeout(30) ->withHeaders([ 'Content-Type' => 'application/json', - ]) - // ->when(true, function ($http) { - // $http->withoutVerifying(); - // }) - ->post("{$appriseUrl}/notify", [ - 'urls' => $message->urls, - 'title' => $message->title, - 'body' => $message->body, - 'type' => $message->type ?? 'info', - 'format' => $message->format ?? 'text', - 'tag' => $message->tag ?? null, ]); - if ($response->failed()) { - Log::error('Apprise notification failed', [ - 'status' => $response->status(), - 'body' => $response->body(), - ]); + // If SSL verification is disabled in settings, skip it + if (! $settings->apprise_verify_ssl) { + $request = $request->withoutVerifying(); + } + + $response = $request->post($appriseUrl, [ + 'urls' => $message->urls, + 'title' => $message->title, + 'body' => $message->body, + 'type' => $message->type ?? 'info', + 'format' => $message->format ?? 'text', + 'tag' => $message->tag ?? null, + ]); + + // Only accept 200 OK responses as successful + if ($response->status() !== 200) { + throw new Exception('Apprise returned an error, please check Apprise logs for details'); } - } catch (\Exception $e) { - Log::error('Apprise notification exception', [ + + Log::info('Apprise notification sent', [ + 'channel' => $message->urls, + 'instance' => $appriseUrl, + ]); + } catch (Throwable $e) { + Log::error('Apprise notification failed', [ + 'channel' => $message->urls ?? 'unknown', + 'instance' => $appriseUrl, 'message' => $e->getMessage(), + 'exception' => get_class($e), ]); + + // Re-throw the exception so it can be handled by the queue + throw $e; } } } diff --git a/app/OpenApi/Annotations/V1/ResultsAnnotations.php b/app/OpenApi/Annotations/V1/ResultsAnnotations.php index 9bfe6a691..76b794c90 100644 --- a/app/OpenApi/Annotations/V1/ResultsAnnotations.php +++ b/app/OpenApi/Annotations/V1/ResultsAnnotations.php @@ -19,7 +19,7 @@ class ResultsAnnotations parameters: [ new OA\Parameter(ref: '#/components/parameters/AcceptHeader'), new OA\Parameter( - name: 'per_page', + name: 'per.page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', minimum: 1, maximum: 500, default: 25), diff --git a/app/OpenApi/Schemas/ResultsCollectionSchema.php b/app/OpenApi/Schemas/ResultsCollectionSchema.php index 2a4ac55a7..2f8d82c5f 100644 --- a/app/OpenApi/Schemas/ResultsCollectionSchema.php +++ b/app/OpenApi/Schemas/ResultsCollectionSchema.php @@ -47,7 +47,7 @@ ) ), new OA\Property(property: 'path', type: 'string'), - new OA\Property(property: 'per_page', type: 'integer'), + new OA\Property(property: 'per.page', type: 'integer'), new OA\Property(property: 'to', type: 'integer'), new OA\Property(property: 'total', type: 'integer'), ], diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e4ea6209f..ba434c79e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,11 +4,14 @@ use App\Enums\UserRole; use App\Models\User; +use App\Notifications\AppriseChannel; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Http\Request; +use Illuminate\Notifications\ChannelManager; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; @@ -44,12 +47,25 @@ public function boot(): void $this->defineGates(); $this->forceHttps(); $this->setApiRateLimit(); + $this->registerNotificationChannels(); AboutCommand::add('Speedtest Tracker', fn () => [ 'Version' => config('speedtest.build_version'), ]); } + /** + * Register custom notification channels. + */ + protected function registerNotificationChannels(): void + { + Notification::resolved(function (ChannelManager $service) { + $service->extend('apprise', function ($app) { + return new AppriseChannel; + }); + }); + } + /** * Define custom if statements, these were added to make the blade templates more readable. * diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 5d90f93d4..3a6ba6f40 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,12 +2,10 @@ namespace App\Providers\Filament; -use App\Services\GitHub\Repository; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Navigation\NavigationGroup; -use Filament\Navigation\NavigationItem; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; @@ -32,6 +30,7 @@ public function panel(Panel $panel): Panel ->colors([ 'primary' => Color::Amber, ]) + ->viteTheme('resources/css/filament/admin/theme.css') ->favicon(asset('img/speedtest-tracker-icon.png')) ->sidebarCollapsibleOnDesktop() ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') @@ -40,6 +39,7 @@ public function panel(Panel $panel): Panel ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([]) ->databaseNotifications() + ->databaseNotificationsPolling('5s') ->maxContentWidth(config('speedtest.content_width')) ->middleware([ EncryptCookies::class, @@ -57,25 +57,8 @@ public function panel(Panel $panel): Panel ]) ->navigationGroups([ NavigationGroup::make() - ->label('Settings'), - NavigationGroup::make() - ->label('Links') + ->label(__('general.settings')) ->collapsible(false), - ]) - ->navigationItems([ - NavigationItem::make('Documentation') - ->url('https://docs.speedtest-tracker.dev/', shouldOpenInNewTab: true) - ->icon('heroicon-o-book-open') - ->group('Links'), - NavigationItem::make('Donate') - ->url('https://github.com/sponsors/alexjustesen', shouldOpenInNewTab: true) - ->icon('heroicon-o-banknotes') - ->group('Links'), - NavigationItem::make(config('speedtest.build_version')) - ->url('https://github.com/alexjustesen/speedtest-tracker', shouldOpenInNewTab: true) - ->icon('tabler-brand-github') - ->badge(fn (): string => Repository::updateAvailable() ? 'Update Available!' : 'Up to Date') - ->group('Links'), ]); } } diff --git a/app/Providers/FilamentServiceProvider.php b/app/Providers/FilamentServiceProvider.php index 96274b131..99e95cd2e 100644 --- a/app/Providers/FilamentServiceProvider.php +++ b/app/Providers/FilamentServiceProvider.php @@ -2,8 +2,6 @@ namespace App\Providers; -use Filament\Support\Assets\Css; -use Filament\Support\Facades\FilamentAsset; use Filament\Support\Facades\FilamentView; use Filament\View\PanelsRenderHook; use Illuminate\Support\Facades\Blade; @@ -24,13 +22,9 @@ public function register(): void */ public function boot(): void { - FilamentAsset::register([ - Css::make('panel', __DIR__.'/../../resources/css/panel.css'), - ]); - FilamentView::registerRenderHook( PanelsRenderHook::GLOBAL_SEARCH_BEFORE, - fn (): string => Blade::render("@livewire('topbar.run-speedtest-action')"), + fn (): string => Blade::render("@livewire('topbar.actions')"), ); } } diff --git a/app/Rules/AppriseScheme.php b/app/Rules/AppriseScheme.php new file mode 100644 index 000000000..03a50059e --- /dev/null +++ b/app/Rules/AppriseScheme.php @@ -0,0 +1,22 @@ +caseSensitive ? $value : strtolower($value); + $needle = $this->caseSensitive ? $this->needle : strtolower($this->needle); + + if (! str_contains($haystack, $needle)) { + $fail("The :attribute must contain '{$this->needle}'."); + } + } +} diff --git a/app/Rules/Cron.php b/app/Rules/Cron.php index ad376df1a..ef59213d0 100644 --- a/app/Rules/Cron.php +++ b/app/Rules/Cron.php @@ -17,7 +17,7 @@ class Cron implements ValidationRule public function validate(string $attribute, mixed $value, Closure $fail): void { if (! CronExpression::isValidExpression($value)) { - $fail('Cron expression is not valid'); + $fail(__('errors.cron_invalid')); } } } diff --git a/app/Services/PrometheusMetricsService.php b/app/Services/PrometheusMetricsService.php new file mode 100644 index 000000000..7b433369d --- /dev/null +++ b/app/Services/PrometheusMetricsService.php @@ -0,0 +1,249 @@ +emptyMetrics(); + } + + $lastResult = Result::find($resultId); + + if (! $lastResult) { + return $this->emptyMetrics(); + } + + $this->registerMetrics($registry, $lastResult); + + $renderer = new RenderTextFormat; + + return $renderer->render($registry->getMetricFamilySamples()); + } + + protected function registerMetrics(CollectorRegistry $registry, Result $result): void + { + $labels = $this->buildLabels($result); + $labelNames = array_keys($labels); + $labelValues = array_values($labels); + + // Download speed in bytes + $downloadBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_bytes', + 'Download speed in bytes per second', + $labelNames + ); + $downloadBytesGauge->set($result->download, $labelValues); + + // Upload speed in bytes + $uploadBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_bytes', + 'Upload speed in bytes per second', + $labelNames + ); + $uploadBytesGauge->set($result->upload, $labelValues); + + // Download speed in bits per second + $downloadBitsGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_bits', + 'Download speed in bits per second', + $labelNames + ); + $downloadBitsGauge->set(toBits($result->download), $labelValues); + + // Upload speed in bits per second + $uploadBitsGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_bits', + 'Upload speed in bits per second', + $labelNames + ); + $uploadBitsGauge->set(toBits($result->upload), $labelValues); + + // Ping latency in milliseconds + $pingGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_ms', + 'Ping latency in milliseconds', + $labelNames + ); + $pingGauge->set($result->ping, $labelValues); + + // Ping jitter + $pingJitterGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_jitter_ms', + 'Ping jitter in milliseconds', + $labelNames + ); + $pingJitterGauge->set($result->ping_jitter ?? 0, $labelValues); + + // Download jitter + $downloadJitterGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_jitter_ms', + 'Download jitter in milliseconds', + $labelNames + ); + $downloadJitterGauge->set($result->download_jitter ?? 0, $labelValues); + + // Upload jitter + $uploadJitterGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_jitter_ms', + 'Upload jitter in milliseconds', + $labelNames + ); + $uploadJitterGauge->set($result->upload_jitter ?? 0, $labelValues); + + // Packet loss + $packetLossGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'packet_loss_percent', + 'Packet loss percentage', + $labelNames + ); + $packetLossGauge->set($result->packet_loss ?? 0, $labelValues); + + // Ping latency low/high + $pingLowGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_low_ms', + 'Ping low latency in milliseconds', + $labelNames + ); + $pingLowGauge->set($result->ping_low ?? 0, $labelValues); + + $pingHighGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'ping_high_ms', + 'Ping high latency in milliseconds', + $labelNames + ); + $pingHighGauge->set($result->ping_high ?? 0, $labelValues); + + // Download latency metrics (IQM = Interquartile Mean - more reliable than average) + $downloadLatencyIqmGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_latency_iqm_ms', + 'Download latency interquartile mean in milliseconds', + $labelNames + ); + $downloadLatencyIqmGauge->set($result->downloadlatencyiqm ?? 0, $labelValues); + + $downloadLatencyLowGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_latency_low_ms', + 'Download latency low in milliseconds', + $labelNames + ); + $downloadLatencyLowGauge->set($result->downloadlatency_low ?? 0, $labelValues); + + $downloadLatencyHighGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_latency_high_ms', + 'Download latency high in milliseconds', + $labelNames + ); + $downloadLatencyHighGauge->set($result->downloadlatency_high ?? 0, $labelValues); + + // Upload latency metrics + $uploadLatencyIqmGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_latency_iqm_ms', + 'Upload latency interquartile mean in milliseconds', + $labelNames + ); + $uploadLatencyIqmGauge->set($result->uploadlatencyiqm ?? 0, $labelValues); + + $uploadLatencyLowGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_latency_low_ms', + 'Upload latency low in milliseconds', + $labelNames + ); + $uploadLatencyLowGauge->set($result->uploadlatency_low ?? 0, $labelValues); + + $uploadLatencyHighGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_latency_high_ms', + 'Upload latency high in milliseconds', + $labelNames + ); + $uploadLatencyHighGauge->set($result->uploadlatency_high ?? 0, $labelValues); + + // Bytes transferred during test + $downloadedBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'downloaded_bytes', + 'Total bytes downloaded during test', + $labelNames + ); + $downloadedBytesGauge->set($result->downloaded_bytes ?? 0, $labelValues); + + $uploadedBytesGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'uploaded_bytes', + 'Total bytes uploaded during test', + $labelNames + ); + $uploadedBytesGauge->set($result->uploaded_bytes ?? 0, $labelValues); + + // Test duration + $downloadElapsedGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'download_elapsed_ms', + 'Download test duration in milliseconds', + $labelNames + ); + $downloadElapsedGauge->set($result->download_elapsed ?? 0, $labelValues); + + $uploadElapsedGauge = $registry->getOrRegisterGauge( + 'speedtest_tracker', + 'upload_elapsed_ms', + 'Upload test duration in milliseconds', + $labelNames + ); + $uploadElapsedGauge->set($result->upload_elapsed ?? 0, $labelValues); + } + + protected function buildLabels(Result $result): array + { + return [ + 'server_id' => (string) ($result->server_id ?? ''), + 'server_name' => $result->server_name ?? '', + 'server_country' => $result->server_country ?? '', + 'server_location' => $result->server_location ?? '', + 'isp' => $result->isp ?? '', + 'scheduled' => $result->scheduled ? 'true' : 'false', + 'healthy' => $result->healthy ? 'true' : 'false', + 'status' => $result->status->value, + 'app_name' => config('app.name', 'Speedtest Tracker'), + ]; + } + + protected function emptyMetrics(): string + { + return "# no data available\n"; + } +} diff --git a/app/Services/ScheduledSpeedtestService.php b/app/Services/ScheduledSpeedtestService.php new file mode 100644 index 000000000..8f9b85fc6 --- /dev/null +++ b/app/Services/ScheduledSpeedtestService.php @@ -0,0 +1,29 @@ +getNextRunDate(timeZone: config('app.display_timezone')) + ); + } +} diff --git a/app/Settings/DataIntegrationSettings.php b/app/Settings/DataIntegrationSettings.php index 2cc29515e..31811c768 100644 --- a/app/Settings/DataIntegrationSettings.php +++ b/app/Settings/DataIntegrationSettings.php @@ -18,6 +18,10 @@ class DataIntegrationSettings extends Settings public bool $influxdb_v2_verify_ssl; + public bool $prometheus_enabled; + + public array $prometheus_allowed_ips = []; + public static function group(): string { return 'dataintegration'; diff --git a/app/Settings/NotificationSettings.php b/app/Settings/NotificationSettings.php index 0796be61a..0f332c68b 100644 --- a/app/Settings/NotificationSettings.php +++ b/app/Settings/NotificationSettings.php @@ -86,6 +86,18 @@ class NotificationSettings extends Settings public ?array $gotify_webhooks; + public bool $apprise_enabled; + + public ?string $apprise_server_url; + + public bool $apprise_on_speedtest_run; + + public bool $apprise_on_threshold_failure; + + public bool $apprise_verify_ssl; + + public ?array $apprise_channel_urls; + public static function group(): string { return 'notification'; diff --git a/boost.json b/boost.json deleted file mode 100644 index 34f823756..000000000 --- a/boost.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "agents": [ - "claude_code", - "copilot" - ], - "editors": [ - "claude_code", - "vscode" - ], - "guidelines": [], - "sail": true -} diff --git a/compose.yaml b/compose.yaml index d072cb3c5..a4ccb9e57 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,8 +23,9 @@ services: depends_on: - pgsql - mailpit + - apprise pgsql: - image: 'postgres:17-alpine' + image: 'postgres:18-alpine' ports: - '${FORWARD_DB_PORT:-5432}:5432' environment: @@ -55,9 +56,29 @@ services: - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025' networks: - sail + apprise: + image: 'caronc/apprise:latest' + ports: + - '${FORWARD_APPRISE_PORT:-8000}:8000' + volumes: + - 'sail-apprise:/config' + networks: + - sail + healthcheck: + test: + - CMD + - 'wget' + - '--quiet' + - '--tries=1' + - '--spider' + - 'http://localhost:8000/health' + retries: 3 + timeout: 5s networks: sail: driver: bridge volumes: sail-pgsql: driver: local + sail-apprise: + driver: local diff --git a/composer.json b/composer.json index 362dc3a78..4fe2349e4 100644 --- a/composer.json +++ b/composer.json @@ -16,39 +16,41 @@ "require": { "php": "^8.2", "chrisullyott/php-filesize": "^4.2.1", + "codewithdennis/filament-simple-alert": "^4.0.2", "dragonmantank/cron-expression": "^3.6.0", "filament/filament": "4.1.0", "filament/spatie-laravel-settings-plugin": "^4.1", - "geerlingguy/ping": "^1.2.1", "influxdata/influxdb-client-php": "^3.8", "laravel-notification-channels/telegram": "^6.0", - "laravel/framework": "^12.38.1", - "laravel/prompts": "^0.3.7", - "laravel/sanctum": "^4.2.0", - "livewire/livewire": "^3.6.4", + "laravel/framework": "^12.41.1", + "laravel/prompts": "^0.3.8", + "laravel/sanctum": "^4.2.1", + "livewire/livewire": "^3.7.1", "lorisleiva/laravel-actions": "^2.9.1", "maennchen/zipstream-php": "^2.4", - "secondnetwork/blade-tabler-icons": "^3.35.0", + "promphp/prometheus_client_php": "^2.14.1", + "saloonphp/laravel-plugin": "^3.7", + "secondnetwork/blade-tabler-icons": "^3.35", "spatie/laravel-json-api-paginate": "^1.16.3", "spatie/laravel-query-builder": "^6.3.6", - "spatie/laravel-settings": "^3.5.0", + "spatie/laravel-settings": "^3.6.0", "spatie/laravel-webhook-server": "^3.8.3", - "zircote/swagger-php": "^5.7.0" + "spatie/ping": "^1.1.1", + "zircote/swagger-php": "^5.7.6" }, "require-dev": { "fakerphp/faker": "^1.24.1", - "laravel/boost": "^1.8", - "laravel/pail": "^1.2.3", - "laravel/pint": "^1.25.1", - "laravel/sail": "^1.48.0", - "laravel/telescope": "^5.15.0", - "laravel/tinker": "^2.10.1", + "laravel/boost": "^1.8.3", + "laravel/pail": "^1.2.4", + "laravel/pint": "^1.26.0", + "laravel/sail": "^1.50.0", + "laravel/telescope": "^5.15.1", + "laravel/tinker": "^2.10.2", "mockery/mockery": "^1.6.12", - "nunomaduro/collision": "^8.8.2", + "nunomaduro/collision": "^8.8.3", "pestphp/pest": "^3.8.4", "pestphp/pest-plugin-laravel": "^3.2", - "spatie/laravel-ignition": "^2.9.1", - "tightenco/duster": "^3.3.0" + "spatie/laravel-ignition": "^2.9.1" }, "autoload": { "files": [ @@ -74,7 +76,8 @@ "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force", "@php artisan vendor:publish --tag=livewire:assets --ansi --force", - "@php artisan boost:update --ansi" + "@php artisan boost:update --ansi", + "composer bump" ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" diff --git a/composer.lock b/composer.lock index fd19a1301..6c492d7d9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ef4555e79a9a20191d718ae6b212ca2c", + "content-hash": "374762e19dbfc99374c14f3f12a4ae3e", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -224,16 +224,16 @@ }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -272,7 +272,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -280,7 +280,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -353,16 +353,16 @@ }, { "name": "chillerlan/php-qrcode", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "390393e97a6e42ccae0e0d6205b8d4200f7ddc43" + "reference": "7b66282572fc14075c0507d74d9837dab25b38d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/390393e97a6e42ccae0e0d6205b8d4200f7ddc43", - "reference": "390393e97a6e42ccae0e0d6205b8d4200f7ddc43", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/7b66282572fc14075c0507d74d9837dab25b38d6", + "reference": "7b66282572fc14075c0507d74d9837dab25b38d6", "shasum": "" }, "require": { @@ -373,7 +373,7 @@ "require-dev": { "chillerlan/php-authenticator": "^4.3.1 || ^5.2.1", "ext-fileinfo": "*", - "phan/phan": "^5.5.1", + "phan/phan": "^5.5.2", "phpcompatibility/php-compatibility": "10.x-dev", "phpmd/phpmd": "^2.15", "phpunit/phpunit": "^9.6", @@ -442,7 +442,7 @@ "type": "Ko-Fi" } ], - "time": "2025-09-19T17:30:27+00:00" + "time": "2025-11-23T23:51:44+00:00" }, { "name": "chillerlan/php-settings-container", @@ -625,6 +625,79 @@ ], "time": "2023-12-20T15:40:13+00:00" }, + { + "name": "codewithdennis/filament-simple-alert", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/CodeWithDennis/filament-simple-alert.git", + "reference": "d30b0cad908f3ade1bed153d486fd564ac312ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CodeWithDennis/filament-simple-alert/zipball/d30b0cad908f3ade1bed153d486fd564ac312ffd", + "reference": "d30b0cad908f3ade1bed153d486fd564ac312ffd", + "shasum": "" + }, + "require": { + "filament/filament": "^4.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.15.0" + }, + "require-dev": { + "laravel/pint": "^1.16", + "nunomaduro/collision": "^7.9", + "orchestra/testbench": "^8.0", + "pestphp/pest": "^2.1", + "pestphp/pest-plugin-arch": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "SimpleAlert": "CodeWithDennis\\SimpleAlert\\Facades\\SimpleAlert" + }, + "providers": [ + "CodeWithDennis\\SimpleAlert\\SimpleAlertServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "CodeWithDennis\\SimpleAlert\\": "src/", + "CodeWithDennis\\SimpleAlert\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CodeWithDennis", + "role": "Developer" + } + ], + "description": "A plugin for adding straightforward alerts to your filament pages", + "homepage": "https://github.com/codewithdennis/filament-simple-alert", + "keywords": [ + "CodeWithDennis", + "filament-simple-alert", + "laravel" + ], + "support": { + "issues": "https://github.com/codewithdennis/filament-simple-alert/issues", + "source": "https://github.com/codewithdennis/filament-simple-alert" + }, + "funding": [ + { + "url": "https://github.com/CodeWithDennis", + "type": "github" + } + ], + "time": "2025-06-21T18:43:06+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -1637,31 +1710,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -1692,7 +1765,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -1704,44 +1777,7 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" - }, - { - "name": "geerlingguy/ping", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/geerlingguy/Ping.git", - "reference": "e0206326e23c99e3e8820e24705f8ca517adff93" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/geerlingguy/Ping/zipball/e0206326e23c99e3e8820e24705f8ca517adff93", - "reference": "e0206326e23c99e3e8820e24705f8ca517adff93", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "JJG/Ping.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeff Geerling", - "email": "jeff@jeffgeerling.com" - } - ], - "description": "A PHP class to ping hosts.", - "support": { - "issues": "https://github.com/geerlingguy/Ping/issues", - "source": "https://github.com/geerlingguy/Ping/tree/1.2.1" - }, - "time": "2019-07-29T21:54:12+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", @@ -2403,16 +2439,16 @@ }, { "name": "laravel/framework", - "version": "v12.38.1", + "version": "v12.41.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "7f3012af6059f5f64a12930701cd8caed6cf7c17" + "reference": "3e229b05935fd0300c632fb1f718c73046d664fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/7f3012af6059f5f64a12930701cd8caed6cf7c17", - "reference": "7f3012af6059f5f64a12930701cd8caed6cf7c17", + "url": "https://api.github.com/repos/laravel/framework/zipball/3e229b05935fd0300c632fb1f718c73046d664fc", + "reference": "3e229b05935fd0300c632fb1f718c73046d664fc", "shasum": "" }, "require": { @@ -2524,7 +2560,7 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.7.0", + "orchestra/testbench-core": "^10.8.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -2618,20 +2654,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-13T02:12:47+00:00" + "time": "2025-12-03T01:02:13+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.7", + "version": "v0.3.8", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", "shasum": "" }, "require": { @@ -2647,7 +2683,7 @@ "require-dev": { "illuminate/collections": "^10.0|^11.0|^12.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", + "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", "phpstan/phpstan-mockery": "^1.1.3" }, @@ -2675,22 +2711,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.7" + "source": "https://github.com/laravel/prompts/tree/v0.3.8" }, - "time": "2025-09-19T13:47:56+00:00" + "time": "2025-11-21T20:52:52+00:00" }, { "name": "laravel/sanctum", - "version": "v4.2.0", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", - "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664", + "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664", "shasum": "" }, "require": { @@ -2704,9 +2740,8 @@ }, "require-dev": { "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^11.3" + "orchestra/testbench": "^9.15|^10.8", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -2741,20 +2776,20 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-07-09T19:45:24+00:00" + "time": "2025-11-21T13:59:03+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.6", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "038ce42edee619599a1debb7e81d7b3759492819" + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819", - "reference": "038ce42edee619599a1debb7e81d7b3759492819", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { @@ -2763,7 +2798,7 @@ "require-dev": { "illuminate/support": "^10.0|^11.0|^12.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", "symfony/var-dumper": "^6.2.0|^7.0.0" }, @@ -2802,20 +2837,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-10-09T13:42:30+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", "shasum": "" }, "require": { @@ -2852,7 +2887,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -2909,7 +2944,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2025-11-26T21:48:24+00:00" }, { "name": "league/config", @@ -3274,33 +3309,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3328,6 +3368,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -3340,9 +3381,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -3352,7 +3395,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -3360,34 +3403,37 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-components", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-components.git", - "reference": "4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f" + "reference": "ffa1215dbee72ee4b7bc08d983d25293812456c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f", - "reference": "4aabf0e2f2f9421ffcacab35be33e4fb5e63c44f", + "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/ffa1215dbee72ee4b7bc08d983d25293812456c2", + "reference": "ffa1215dbee72ee4b7bc08d983d25293812456c2", "shasum": "" }, "require": { - "league/uri": "^7.5", + "league/uri": "^7.6", "php": "^8.1" }, "suggest": { + "bakame/aide-uri": "A polyfill for PHP8.1 until PHP8.4 to add support to PHP Native URI parser", "ext-bcmath": "to improve IPV4 host parsing", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "ext-mbstring": "to use the sorting algorithm of URLSearchParams", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3434,7 +3480,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-components/tree/7.5.1" + "source": "https://github.com/thephpleague/uri-components/tree/7.6.0" }, "funding": [ { @@ -3442,26 +3488,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -3469,6 +3514,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3493,7 +3539,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -3518,7 +3564,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -3526,20 +3572,20 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "livewire/livewire", - "version": "v3.6.4", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "ef04be759da41b14d2d129e670533180a44987dc" + "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc", - "reference": "ef04be759da41b14d2d129e670533180a44987dc", + "url": "https://api.github.com/repos/livewire/livewire/zipball/214da8f3a1199a88b56ab2fe901d4a607f784805", + "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805", "shasum": "" }, "require": { @@ -3594,7 +3640,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.6.4" + "source": "https://github.com/livewire/livewire/tree/v3.7.1" }, "funding": [ { @@ -3602,7 +3648,7 @@ "type": "github" } ], - "time": "2025-07-17T05:12:15+00:00" + "time": "2025-12-03T22:41:13+00:00" }, { "name": "lorisleiva/laravel-actions", @@ -4065,16 +4111,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "bdb375400dcd162624531666db4799b36b64e4a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", + "reference": "bdb375400dcd162624531666db4799b36b64e4a1", "shasum": "" }, "require": { @@ -4082,9 +4128,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -4166,7 +4212,7 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2025-12-02T21:04:28+00:00" }, { "name": "nette/php-generator", @@ -4307,20 +4353,20 @@ }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -4343,7 +4389,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -4390,9 +4436,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.1.0" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2025-12-01T17:49:23+00:00" }, { "name": "nikic/php-parser", @@ -4454,31 +4500,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.3.4" + "symfony/console": "^7.3.6" }, "require-dev": { "illuminate/console": "^11.46.1", "laravel/pint": "^1.25.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.4", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3", "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.3.4", + "symfony/var-dumper": "^7.3.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -4521,7 +4567,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" }, "funding": [ { @@ -4537,7 +4583,7 @@ "type": "github" } ], - "time": "2025-10-18T11:10:27+00:00" + "time": "2025-11-20T02:34:59+00:00" }, { "name": "openspout/openspout", @@ -4703,16 +4749,16 @@ }, { "name": "php-http/client-common", - "version": "2.7.2", + "version": "2.7.3", "source": { "type": "git", "url": "https://github.com/php-http/client-common.git", - "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + "reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", - "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "url": "https://api.github.com/repos/php-http/client-common/zipball/dcc6de29c90dd74faab55f71b79d89409c4bf0c1", + "reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1", "shasum": "" }, "require": { @@ -4722,15 +4768,13 @@ "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.0 || ^2.0", - "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/polyfill-php80": "^1.17" }, "require-dev": { "doctrine/instantiator": "^1.1", "guzzlehttp/psr7": "^1.4", "nyholm/psr7": "^1.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "phpspec/prophecy": "^1.10.2", "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" }, "suggest": { @@ -4766,9 +4810,9 @@ ], "support": { "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.7.2" + "source": "https://github.com/php-http/client-common/tree/2.7.3" }, - "time": "2024-09-24T06:21:48+00:00" + "time": "2025-11-29T19:12:34+00:00" }, { "name": "php-http/discovery", @@ -5082,16 +5126,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { @@ -5134,9 +5178,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-21T15:09:14+00:00" }, { "name": "phpoption/phpoption", @@ -5379,6 +5423,74 @@ }, "time": "2025-09-19T23:02:26+00:00" }, + { + "name": "promphp/prometheus_client_php", + "version": "v2.14.1", + "source": { + "type": "git", + "url": "https://github.com/PromPHP/prometheus_client_php.git", + "reference": "a283aea8269287dc35313a0055480d950c59ac1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/a283aea8269287dc35313a0055480d950c59ac1f", + "reference": "a283aea8269287dc35313a0055480d950c59ac1f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4|^8.0" + }, + "replace": { + "endclothing/prometheus_client_php": "*", + "jimdo/prometheus_client_php": "*", + "lkaemmerling/prometheus_client_php": "*" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.3|^7.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5.4", + "phpstan/phpstan-phpunit": "^1.1.0", + "phpstan/phpstan-strict-rules": "^1.1.0", + "phpunit/phpunit": "^9.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/polyfill-apcu": "^1.6" + }, + "suggest": { + "ext-apc": "Required if using APCu.", + "ext-pdo": "Required if using PDO.", + "ext-redis": "Required if using Redis.", + "promphp/prometheus_push_gateway_php": "An easy client for using Prometheus PushGateway.", + "symfony/polyfill-apcu": "Required if you use APCu on PHP8.0+" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Prometheus\\": "src/Prometheus/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Lukas Kämmerling", + "email": "kontakt@lukas-kaemmerling.de" + } + ], + "description": "Prometheus instrumentation library for PHP applications.", + "support": { + "issues": "https://github.com/PromPHP/prometheus_client_php/issues", + "source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.14.1" + }, + "time": "2025-04-14T07:59:43+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -6067,6 +6179,155 @@ ], "time": "2025-02-25T09:09:36+00:00" }, + { + "name": "saloonphp/laravel-plugin", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/saloonphp/laravel-plugin.git", + "reference": "85e423202c5da8be6c3c30f6f7dac854d0975325" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/saloonphp/laravel-plugin/zipball/85e423202c5da8be6c3c30f6f7dac854d0975325", + "reference": "85e423202c5da8be6c3c30f6f7dac854d0975325", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0 || ^v12.39.0", + "illuminate/support": "^11.0 || ^12.39.0", + "php": "^8.2", + "saloonphp/saloon": "^3.5", + "symfony/finder": "^6.4 || ^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.48", + "orchestra/testbench": "^9.15 || ^10.7", + "pestphp/pest": "^3.0|^4.0", + "phpstan/phpstan": "^1.10.57|^2.0.2" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Saloon": "Saloon\\Laravel\\Facades\\Saloon" + }, + "providers": [ + "Saloon\\Laravel\\SaloonServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Saloon\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Carré", + "email": "29132017+Sammyjo20@users.noreply.github.com", + "role": "Developer" + } + ], + "description": "The official Laravel plugin for Saloon", + "homepage": "https://github.com/saloonphp/laravel-plugin", + "keywords": [ + "api", + "api-integrations", + "saloon", + "saloonphp", + "sdk" + ], + "support": { + "source": "https://github.com/saloonphp/laravel-plugin/tree/v3.7.0" + }, + "time": "2025-11-21T23:45:44+00:00" + }, + { + "name": "saloonphp/saloon", + "version": "v3.14.2", + "source": { + "type": "git", + "url": "https://github.com/saloonphp/saloon.git", + "reference": "634be16ca5eb0b71ab01533f58dc88d174a2e28b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/saloonphp/saloon/zipball/634be16ca5eb0b71ab01533f58dc88d174a2e28b", + "reference": "634be16ca5eb0b71ab01533f58dc88d174a2e28b", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.6", + "guzzlehttp/promises": "^1.5 || ^2.0", + "guzzlehttp/psr7": "^2.0", + "php": "^8.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "sammyjo20/saloon": "*" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "illuminate/collections": "^10.0 || ^11.0 || ^12.0", + "league/flysystem": "^3.0", + "pestphp/pest": "^2.36.0 || ^3.8.2 || ^4.1.4", + "phpstan/phpstan": "^2.1.13", + "saloonphp/xml-wrangler": "^1.1", + "spatie/invade": "^2.1", + "symfony/dom-crawler": "^6.0 || ^7.0", + "symfony/var-dumper": "^6.3 || ^7.0" + }, + "suggest": { + "illuminate/collections": "Required for the response collect() method.", + "saloonphp/xml-wrangler": "Required for the response xmlReader() method.", + "symfony/dom-crawler": "Required for the response dom() method.", + "symfony/var-dumper": "Required for default debugging drivers." + }, + "type": "library", + "autoload": { + "psr-4": { + "Saloon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Carré", + "email": "29132017+Sammyjo20@users.noreply.github.com", + "role": "Developer" + } + ], + "description": "Build beautiful API integrations and SDKs with Saloon", + "homepage": "https://github.com/saloonphp/saloon", + "keywords": [ + "api", + "api-integrations", + "saloon", + "sammyjo20", + "sdk" + ], + "support": { + "issues": "https://github.com/saloonphp/saloon/issues", + "source": "https://github.com/saloonphp/saloon/tree/v3.14.2" + }, + "funding": [ + { + "url": "https://github.com/sammyjo20", + "type": "github" + } + ], + "time": "2025-11-20T21:42:32+00:00" + }, { "name": "scrivo/highlight.php", "version": "v9.18.1.10", @@ -6464,16 +6725,16 @@ }, { "name": "spatie/laravel-settings", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-settings.git", - "reference": "bdb12449ce1f7afcf12fac59f6c7a63a39513fe7" + "reference": "fae93dadb8f748628ecaf5710f494adf790255b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-settings/zipball/bdb12449ce1f7afcf12fac59f6c7a63a39513fe7", - "reference": "bdb12449ce1f7afcf12fac59f6c7a63a39513fe7", + "url": "https://api.github.com/repos/spatie/laravel-settings/zipball/fae93dadb8f748628ecaf5710f494adf790255b2", + "reference": "fae93dadb8f748628ecaf5710f494adf790255b2", "shasum": "" }, "require": { @@ -6533,7 +6794,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-settings/issues", - "source": "https://github.com/spatie/laravel-settings/tree/3.5.0" + "source": "https://github.com/spatie/laravel-settings/tree/3.6.0" }, "funding": [ { @@ -6545,7 +6806,7 @@ "type": "github" } ], - "time": "2025-10-24T13:01:51+00:00" + "time": "2025-12-03T10:29:27+00:00" }, { "name": "spatie/laravel-webhook-server", @@ -6621,6 +6882,65 @@ ], "time": "2025-02-14T12:55:41+00:00" }, + { + "name": "spatie/ping", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/ping.git", + "reference": "6123a6209148e8919f58121d256f43c75856ab35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ping/zipball/6123a6209148e8919f58121d256f43c75856ab35", + "reference": "6123a6209148e8919f58121d256f43c75856ab35", + "shasum": "" + }, + "require": { + "php": "^8.4", + "symfony/process": "^7.0" + }, + "require-dev": { + "laravel/pint": "^1.0", + "pestphp/pest": "^3.0", + "spatie/pest-expectations": "^1.13", + "spatie/ray": "^1.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ping\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Run an ICMP ping and get structured results", + "homepage": "https://github.com/spatie/ping", + "keywords": [ + "ping", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/ping/issues", + "source": "https://github.com/spatie/ping/tree/1.1.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-08-12T20:58:12+00:00" + }, { "name": "spatie/shiki-php", "version": "2.3.2", @@ -6749,22 +7069,21 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/clock": "^1.0", - "symfony/polyfill-php83": "^1.28" + "php": ">=8.4", + "psr/clock": "^1.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -6803,7 +7122,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v8.0.0" }, "funding": [ { @@ -6814,25 +7133,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:46:48+00:00" }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", "shasum": "" }, "require": { @@ -6840,7 +7163,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -6854,16 +7177,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6897,7 +7220,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.0" }, "funding": [ { @@ -6917,20 +7240,20 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "84321188c4754e64273b46b406081ad9b18e8614" + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/84321188c4754e64273b46b406081ad9b18e8614", - "reference": "84321188c4754e64273b46b406081ad9b18e8614", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab862f478513e7ca2fe9ec117a6f01a8da6e1135", + "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135", "shasum": "" }, "require": { @@ -6966,7 +7289,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.6" + "source": "https://github.com/symfony/css-selector/tree/v7.4.0" }, "funding": [ { @@ -6986,7 +7309,7 @@ "type": "tidelift" } ], - "time": "2025-10-29T17:24:25+00:00" + "time": "2025-10-30T13:39:42+00:00" }, { "name": "symfony/deprecation-contracts", @@ -7057,32 +7380,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "bbe40bfab84323d99dab491b716ff142410a92a8" + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/bbe40bfab84323d99dab491b716ff142410a92a8", - "reference": "bbe40bfab84323d99dab491b716ff142410a92a8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", + "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -7114,7 +7438,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.6" + "source": "https://github.com/symfony/error-handler/tree/v7.4.0" }, "funding": [ { @@ -7134,28 +7458,28 @@ "type": "tidelift" } ], - "time": "2025-10-31T19:12:50+00:00" + "time": "2025-11-05T14:29:59+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -7164,13 +7488,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -7198,7 +7523,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -7218,7 +7543,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7298,23 +7623,23 @@ }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7342,7 +7667,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -7362,27 +7687,28 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "3855e827adb1b675adcb98ad7f92681e293f2d77" + "reference": "5b0bbcc3600030b535dd0b17a0e8c56243f96d7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/3855e827adb1b675adcb98ad7f92681e293f2d77", - "reference": "3855e827adb1b675adcb98ad7f92681e293f2d77", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/5b0bbcc3600030b535dd0b17a0e8c56243f96d7f", + "reference": "5b0bbcc3600030b535dd0b17a0e8c56243f96d7f", "shasum": "" }, "require": { "ext-dom": "*", "league/uri": "^6.5|^7.0", "masterminds/html5": "^2.7.2", - "php": ">=8.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -7415,7 +7741,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v7.3.6" + "source": "https://github.com/symfony/html-sanitizer/tree/v7.4.0" }, "funding": [ { @@ -7435,27 +7761,26 @@ "type": "tidelift" } ], - "time": "2025-10-30T13:22:58+00:00" + "time": "2025-10-30T13:39:42+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.7", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" + "reference": "769c1720b68e964b13b58529c17d4a385c62167b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", - "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/769c1720b68e964b13b58529c17d4a385c62167b", + "reference": "769c1720b68e964b13b58529c17d4a385c62167b", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -7464,13 +7789,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7498,7 +7823,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.0" }, "funding": [ { @@ -7518,29 +7843,29 @@ "type": "tidelift" } ], - "time": "2025-11-08T16:41:12+00:00" + "time": "2025-11-13T08:49:24+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.7", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce" + "reference": "7348193cd384495a755554382e4526f27c456085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/10b8e9b748ea95fa4539c208e2487c435d3c87ce", - "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7348193cd384495a755554382e4526f27c456085", + "reference": "7348193cd384495a755554382e4526f27c456085", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -7550,6 +7875,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -7567,27 +7893,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -7616,7 +7942,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.7" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.0" }, "funding": [ { @@ -7636,20 +7962,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T11:38:40+00:00" + "time": "2025-11-27T13:38:24+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba" + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/fd497c45ba9c10c37864e19466b090dcb60a50ba", - "reference": "fd497c45ba9c10c37864e19466b090dcb60a50ba", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", + "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", "shasum": "" }, "require": { @@ -7657,8 +7983,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -7669,10 +7995,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7700,7 +8026,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.5" + "source": "https://github.com/symfony/mailer/tree/v7.4.0" }, "funding": [ { @@ -7720,24 +8046,25 @@ "type": "tidelift" } ], - "time": "2025-10-24T14:27:20+00:00" + "time": "2025-11-21T15:26:00+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -7752,11 +8079,11 @@ "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -7788,7 +8115,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.0" }, "funding": [ { @@ -7808,24 +8135,24 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -7859,7 +8186,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -7879,7 +8206,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8712,16 +9039,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -8753,7 +9080,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.0" }, "funding": [ { @@ -8773,20 +9100,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/routing", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091" + "reference": "4720254cb2644a0b876233d258a32bf017330db7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/c97abe725f2a1a858deca629a6488c8fc20c3091", - "reference": "c97abe725f2a1a858deca629a6488c8fc20c3091", + "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", + "reference": "4720254cb2644a0b876233d258a32bf017330db7", "shasum": "" }, "require": { @@ -8800,11 +9127,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -8838,7 +9165,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.6" + "source": "https://github.com/symfony/routing/tree/v7.4.0" }, "funding": [ { @@ -8858,7 +9185,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T07:57:47+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/service-contracts", @@ -8949,34 +9276,34 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "f929eccf09531078c243df72398560e32fa4cf4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", + "reference": "f929eccf09531078c243df72398560e32fa4cf4f", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -9015,7 +9342,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.0" }, "funding": [ { @@ -9035,38 +9362,31 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-09-11T14:37:55+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "82ab368a6fca6358d995b6dd5c41590fb42c03e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/82ab368a6fca6358d995b6dd5c41590fb42c03e6", + "reference": "82ab368a6fca6358d995b6dd5c41590fb42c03e6", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, "conflict": { "nikic/php-parser": "<5.0", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<6.4", - "symfony/yaml": "<6.4" + "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -9074,17 +9394,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -9115,7 +9435,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v8.0.0" }, "funding": [ { @@ -9135,7 +9455,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2025-11-27T08:09:45+00:00" }, { "name": "symfony/translation-contracts", @@ -9221,16 +9541,16 @@ }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", + "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", "shasum": "" }, "require": { @@ -9238,7 +9558,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -9275,7 +9595,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.0" }, "funding": [ { @@ -9286,25 +9606,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-09-25T11:02:55+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -9316,10 +9640,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -9358,7 +9682,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -9378,32 +9702,32 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -9434,7 +9758,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.0" }, "funding": [ { @@ -9454,7 +9778,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -9740,16 +10064,16 @@ }, { "name": "zircote/swagger-php", - "version": "5.7.0", + "version": "5.7.6", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "39fcd46e79c2f3cfbf56cf5a92a86108c8eed401" + "reference": "e4727bad28cf426b026421162af384f893c0142c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/39fcd46e79c2f3cfbf56cf5a92a86108c8eed401", - "reference": "39fcd46e79c2f3cfbf56cf5a92a86108c8eed401", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/e4727bad28cf426b026421162af384f893c0142c", + "reference": "e4727bad28cf426b026421162af384f893c0142c", "shasum": "" }, "require": { @@ -9759,8 +10083,8 @@ "phpstan/phpdoc-parser": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/deprecation-contracts": "^2 || ^3", - "symfony/finder": "^5.0 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "conflict": { "symfony/process": ">=6, <6.4.14" @@ -9822,9 +10146,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.7.0" + "source": "https://github.com/zircote/swagger-php/tree/5.7.6" }, - "time": "2025-11-11T03:41:35+00:00" + "time": "2025-12-04T01:33:01+00:00" } ], "packages-dev": [ @@ -10229,25 +10553,25 @@ }, { "name": "laravel/boost", - "version": "v1.8.0", + "version": "v1.8.3", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "3475be16be7552b11c57ce18a0c5e204d696da50" + "reference": "26572e858e67334952779c0110ca4c378a44d28d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/3475be16be7552b11c57ce18a0c5e204d696da50", - "reference": "3475be16be7552b11c57ce18a0c5e204d696da50", + "url": "https://api.github.com/repos/laravel/boost/zipball/26572e858e67334952779c0110ca4c378a44d28d", + "reference": "26572e858e67334952779c0110ca4c378a44d28d", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.10", + "guzzlehttp/guzzle": "^7.9", "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "laravel/mcp": "^0.3.2", + "laravel/mcp": "^0.3.4", "laravel/prompts": "0.1.25|^0.3.6", "laravel/roster": "^0.2.9", "php": "^8.1" @@ -10291,20 +10615,20 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-11-11T14:15:11+00:00" + "time": "2025-11-26T14:12:52+00:00" }, { "name": "laravel/mcp", - "version": "v0.3.3", + "version": "v0.3.4", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "feb475f819809e7db0a46e9f2cbcee6d77af2a14" + "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/feb475f819809e7db0a46e9f2cbcee6d77af2a14", - "reference": "feb475f819809e7db0a46e9f2cbcee6d77af2a14", + "url": "https://api.github.com/repos/laravel/mcp/zipball/0b86fb613a0df971cec89271c674a677c2cb4f77", + "reference": "0b86fb613a0df971cec89271c674a677c2cb4f77", "shasum": "" }, "require": { @@ -10364,20 +10688,20 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-11-11T22:50:25+00:00" + "time": "2025-11-18T14:41:05+00:00" }, { "name": "laravel/pail", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", - "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "url": "https://api.github.com/repos/laravel/pail/zipball/49f92285ff5d6fc09816e976a004f8dec6a0ea30", + "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30", "shasum": "" }, "require": { @@ -10394,9 +10718,9 @@ "require-dev": { "laravel/framework": "^10.24|^11.0|^12.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.0|^10.0", - "pestphp/pest": "^2.20|^3.0", - "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "orchestra/testbench-core": "^8.13|^9.17|^10.8", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, @@ -10443,20 +10767,20 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-06-05T13:55:57+00:00" + "time": "2025-11-20T16:29:35+00:00" }, { "name": "laravel/pint", - "version": "v1.25.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -10467,13 +10791,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" @@ -10499,6 +10823,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -10509,7 +10834,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "laravel/roster", @@ -10574,16 +10899,16 @@ }, { "name": "laravel/sail", - "version": "v1.48.0", + "version": "v1.50.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "1bf3b8870b72a258a3b6b5119435835ece522e8a" + "reference": "9177d5de1c8247166b92ea6049c2b069d2a1802f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/1bf3b8870b72a258a3b6b5119435835ece522e8a", - "reference": "1bf3b8870b72a258a3b6b5119435835ece522e8a", + "url": "https://api.github.com/repos/laravel/sail/zipball/9177d5de1c8247166b92ea6049c2b069d2a1802f", + "reference": "9177d5de1c8247166b92ea6049c2b069d2a1802f", "shasum": "" }, "require": { @@ -10633,20 +10958,20 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-11-09T14:46:21+00:00" + "time": "2025-12-03T17:16:36+00:00" }, { "name": "laravel/telescope", - "version": "v5.15.0", + "version": "v5.15.1", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "cbdd61b025dddeccaffefc3b54d327c4e0a410b6" + "reference": "45e38e057343a94c570c5daad3273e9e29819738" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/cbdd61b025dddeccaffefc3b54d327c4e0a410b6", - "reference": "cbdd61b025dddeccaffefc3b54d327c4e0a410b6", + "url": "https://api.github.com/repos/laravel/telescope/zipball/45e38e057343a94c570c5daad3273e9e29819738", + "reference": "45e38e057343a94c570c5daad3273e9e29819738", "shasum": "" }, "require": { @@ -10659,10 +10984,9 @@ "require-dev": { "ext-gd": "*", "guzzlehttp/guzzle": "^6.0|^7.0", - "laravel/octane": "^1.4|^2.0|dev-develop", - "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0|^10.5|^11.5" + "laravel/octane": "^1.4|^2.0", + "orchestra/testbench": "^6.47.1|^7.55|^8.36|^9.15|^10.8", + "phpstan/phpstan": "^1.10" }, "type": "library", "extra": { @@ -10700,22 +11024,22 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.15.0" + "source": "https://github.com/laravel/telescope/tree/v5.15.1" }, - "time": "2025-10-23T15:19:35+00:00" + "time": "2025-11-25T14:45:17+00:00" }, { "name": "laravel/tinker", - "version": "v2.10.1", + "version": "v2.10.2", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", - "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c", + "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c", "shasum": "" }, "require": { @@ -10766,9 +11090,9 @@ ], "support": { "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.10.1" + "source": "https://github.com/laravel/tinker/tree/v2.10.2" }, - "time": "2025-01-27T14:24:01+00:00" + "time": "2025-11-20T16:29:12+00:00" }, { "name": "mockery/mockery", @@ -10915,16 +11239,16 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.2", + "version": "v8.8.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4", + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4", "shasum": "" }, "require": { @@ -10946,7 +11270,7 @@ "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", "orchestra/testbench-core": "^9.12.0 || ^10.4", - "pestphp/pest": "^3.8.2", + "pestphp/pest": "^3.8.2 || ^4.0.0", "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", @@ -11010,7 +11334,7 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2025-11-20T02:55:25+00:00" }, { "name": "pestphp/pest", @@ -11530,16 +11854,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.5", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", "shasum": "" }, "require": { @@ -11588,9 +11912,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-11-27T19:50:05+00:00" }, { "name": "phpunit/php-code-coverage", @@ -12038,16 +12362,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.14", + "version": "v0.12.15", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "95c29b3756a23855a30566b745d218bee690bef2" + "reference": "38953bc71491c838fcb6ebcbdc41ab7483cd549c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/95c29b3756a23855a30566b745d218bee690bef2", - "reference": "95c29b3756a23855a30566b745d218bee690bef2", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/38953bc71491c838fcb6ebcbdc41ab7483cd549c", + "reference": "38953bc71491c838fcb6ebcbdc41ab7483cd549c", "shasum": "" }, "require": { @@ -12111,9 +12435,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.14" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.15" }, - "time": "2025-10-27T17:15:31+00:00" + "time": "2025-11-28T00:00:14+00:00" }, { "name": "sebastian/cli-parser", @@ -13595,16 +13919,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -13633,7 +13957,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.0" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -13641,75 +13965,7 @@ "type": "github" } ], - "time": "2025-11-13T13:44:09+00:00" - }, - { - "name": "tightenco/duster", - "version": "v3.3.0", - "source": { - "type": "git", - "url": "https://github.com/tighten/duster.git", - "reference": "0260abaaecabd9655a0836e4038238e6585a8b45" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tighten/duster/zipball/0260abaaecabd9655a0836e4038238e6585a8b45", - "reference": "0260abaaecabd9655a0836e4038238e6585a8b45", - "shasum": "" - }, - "require": { - "php": "^8.2.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.73", - "laravel-zero/framework": "^11.36", - "laravel/pint": "^1.21", - "nunomaduro/termwind": "^2.0", - "spatie/invade": "^1.1", - "squizlabs/php_codesniffer": "^3.12", - "tightenco/tlint": "^9.5" - }, - "bin": [ - "builds/duster" - ], - "type": "project", - "autoload": { - "psr-4": { - "App\\": "app/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matt Stauffer", - "email": "matt@tighten.com", - "homepage": "https://tighten.com", - "role": "Developer" - }, - { - "name": "Anthony Clark", - "email": "anthony@tighten.com", - "homepage": "https://tighten.com", - "role": "Developer" - } - ], - "description": "Automatic configuration for Laravel apps to apply Tighten's standard linting & code standards.", - "homepage": "https://github.com/tighten/duster", - "keywords": [ - "Code style", - "duster", - "laravel", - "php", - "tightenco" - ], - "support": { - "issues": "https://github.com/tighten/duster/issues", - "source": "https://github.com/tighten/duster" - }, - "time": "2025-11-07T15:00:12+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/assert", @@ -13782,5 +14038,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/app.php b/config/app.php index d670df243..639fb4881 100644 --- a/config/app.php +++ b/config/app.php @@ -2,19 +2,165 @@ return [ + /* + |-------------------------------------------------------------------------- + | Application Name + |-------------------------------------------------------------------------- + | + | This value is the name of your application, which will be used when the + | framework needs to place the application's name in a notification or + | other UI elements where an application name needs to be displayed. + | + */ + 'name' => env('APP_NAME', 'Speedtest Tracker'), + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + 'env' => env('APP_ENV', 'production'), + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'force_https' => env('FORCE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => env('APP_TIMEZONE', 'UTC'), + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + + // TODO: move to speedtest.php configuration file + + /* + |-------------------------------------------------------------------------- + | Chart Configuration + |-------------------------------------------------------------------------- + | + | Here you may specify the default settings for charts used in the application. + | + */ + 'chart_begin_at_zero' => env('CHART_BEGIN_AT_ZERO', true), 'chart_datetime_format' => env('CHART_DATETIME_FORMAT', 'M. j - G:i'), - 'datetime_format' => env('DATETIME_FORMAT', 'M. jS, Y g:ia'), + /* + |-------------------------------------------------------------------------- + | Display Configuration + |-------------------------------------------------------------------------- + | + | Here you may specify the default settings for displaying data in the application. + | + */ + + 'datetime_format' => env('DATETIME_FORMAT', 'M. j, Y g:ia'), 'display_timezone' => env('DISPLAY_TIMEZONE', 'UTC'), - 'force_https' => env('FORCE_HTTPS', false), + /* + |-------------------------------------------------------------------------- + | Admin Configuration + |-------------------------------------------------------------------------- + | + | Here you may specify the default account settings for the admin user at installation. + | + */ 'admin_name' => env('ADMIN_NAME', 'Admin'), diff --git a/config/json-api-paginate.php b/config/json-api-paginate.php new file mode 100644 index 000000000..6ff3c2afa --- /dev/null +++ b/config/json-api-paginate.php @@ -0,0 +1,17 @@ + env('API_MAX_RESULTS', 500), + + /* + * The default number of results that will be returned + * when using the JSON API paginator. + */ + 'default_size' => 25, + +]; diff --git a/config/services.php b/config/services.php index 20a57409b..c7f3bddd7 100644 --- a/config/services.php +++ b/config/services.php @@ -2,12 +2,13 @@ return [ - 'apprise' => [ - 'url' => env('APPRISE_URL', 'http://apprise:8000'), - ], - 'telegram-bot-api' => [ 'token' => env('TELEGRAM_BOT_TOKEN'), ], + 'unifi-api' => [ + 'base_url' => env('UNIFI_API_BASE_URL', 'https://192.168.1.1'), + 'token' => env('UNIFI_API_TOKEN'), + ], + ]; diff --git a/config/session.php b/config/session.php index 13d86a4ac..d9dbaea98 100644 --- a/config/session.php +++ b/config/session.php @@ -32,7 +32,7 @@ | */ - 'lifetime' => (int) env('SESSION_LIFETIME', 120), + 'lifetime' => (int) env('SESSION_LIFETIME', 10800), # 1 week 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), diff --git a/config/speedtest.php b/config/speedtest.php index 7afdd56f5..4226f5a0a 100644 --- a/config/speedtest.php +++ b/config/speedtest.php @@ -6,9 +6,9 @@ /** * General settings. */ - 'build_date' => Carbon::parse('2025-11-13'), + 'build_date' => Carbon::parse('2026-01-08'), - 'build_version' => 'v1.7.4', + 'build_version' => 'v1.13.5', 'content_width' => env('CONTENT_WIDTH', '7xl'), @@ -16,7 +16,7 @@ 'public_dashboard' => env('PUBLIC_DASHBOARD', false), - 'default_chart_range' => env('DEFAULT_CHART_RANGE', '24h'), + 'default_chart_range' => strtolower(env('DEFAULT_CHART_RANGE', '24h')), /** * Speedtest settings. @@ -29,15 +29,17 @@ 'interface' => env('SPEEDTEST_INTERFACE'), - 'checkinternet_url' => env('SPEEDTEST_CHECKINTERNET_URL', 'https://icanhazip.com'), + 'preflight' => [ + 'external_ip_url' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_EXTERNAL_IP_URL', 'https://icanhazip.com'), + 'internet_check_hostname' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_INTERNET_CHECK_HOSTNAME', 'icanhazip.com'), + 'skip_ips' => env('SPEEDTEST_SKIP_IPS'), + ], /** * IP filtering settings. */ 'allowed_ips' => env('ALLOWED_IPS'), - 'skip_ips' => env('SPEEDTEST_SKIP_IPS', ''), - /** * Threshold settings. */ diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..510736be9 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,13 @@ +base_path: /lang +preserve_hierarchy: true +pull_request_labels: + - "chore" + +files: + # PHP language files - root level + - source: /en/*.php + translation: /%locale_with_underscore%/%original_file_name% + + # PHP language files - settings subdirectory + - source: /en/settings/*.php + translation: /%locale_with_underscore%/settings/%original_file_name% diff --git a/database/migrations/2025_11_24_151719_add_dispatched_by_to_results_table.php b/database/migrations/2025_11_24_151719_add_dispatched_by_to_results_table.php new file mode 100644 index 000000000..473c9b776 --- /dev/null +++ b/database/migrations/2025_11_24_151719_add_dispatched_by_to_results_table.php @@ -0,0 +1,18 @@ +foreignId('dispatched_by')->nullable()->constrained('users')->nullOnDelete(); + }); + } +}; diff --git a/database/settings/2024_12_31_164343_create_apprise_notification.php b/database/settings/2024_12_31_164343_create_apprise_notification.php new file mode 100644 index 000000000..1be9ac906 --- /dev/null +++ b/database/settings/2024_12_31_164343_create_apprise_notification.php @@ -0,0 +1,16 @@ +migrator->add('notification.apprise_enabled', false); + $this->migrator->add('notification.apprise_server_url', null); + $this->migrator->add('notification.apprise_on_speedtest_run', false); + $this->migrator->add('notification.apprise_on_threshold_failure', false); + $this->migrator->add('notification.apprise_verify_ssl', true); + $this->migrator->add('notification.apprise_channel_urls', null); + } +}; diff --git a/database/settings/2025_11_25_191005_create_prometheus_settings.php b/database/settings/2025_11_25_191005_create_prometheus_settings.php new file mode 100644 index 000000000..da7c8025f --- /dev/null +++ b/database/settings/2025_11_25_191005_create_prometheus_settings.php @@ -0,0 +1,12 @@ +migrator->add('dataintegration.prometheus_enabled', false); + $this->migrator->add('dataintegration.prometheus_allowed_ips', []); + } +} diff --git a/docker/8.4/Dockerfile b/docker/8.4/Dockerfile index 0ce8d812f..262aa0055 100644 --- a/docker/8.4/Dockerfile +++ b/docker/8.4/Dockerfile @@ -7,6 +7,7 @@ ARG NODE_VERSION=22 ARG MYSQL_CLIENT="mysql-client" ARG POSTGRES_VERSION=17 ARG SPEEDTEST_VERSION=1.2.0 +ARG LIBRESPEED_VERSION=1.0.12 WORKDIR /var/www/html @@ -49,14 +50,19 @@ RUN apt-get update && apt-get upgrade -y \ && ARCH=$(uname -m) \ && if [ "$ARCH" = "x86_64" ]; then \ PLATFORM="x86_64"; \ + LIBRESPEED_PLATFORM="amd64"; \ elif [ "$ARCH" = "aarch64" ]; then \ PLATFORM="aarch64"; \ + LIBRESPEED_PLATFORM="arm64"; \ else \ echo "Unsupported architecture: $ARCH"; exit 1; \ fi \ && curl -o /tmp/speedtest-cli.tgz -L \ "https://install.speedtest.net/app/cli/ookla-speedtest-$SPEEDTEST_VERSION-linux-$PLATFORM.tgz" \ && tar -xzf /tmp/speedtest-cli.tgz -C /usr/bin \ + && curl -o /tmp/librespeed-cli.tar.gz -L \ + "https://github.com/librespeed/speedtest-cli/releases/download/v$LIBRESPEED_VERSION/librespeed-cli_${LIBRESPEED_VERSION}_linux_${LIBRESPEED_PLATFORM}.tar.gz" \ + && tar -xzf /tmp/librespeed-cli.tar.gz -C /usr/bin \ && apt-get -y autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/lang/de_DE/api_tokens.php b/lang/de_DE/api_tokens.php new file mode 100644 index 000000000..ec533b9f6 --- /dev/null +++ b/lang/de_DE/api_tokens.php @@ -0,0 +1,30 @@ + 'API-Token', + 'label' => 'API-Token', + + // Token management + 'api_token' => 'API token', + 'api_tokens' => 'API-Token', + 'create_api_token' => 'API-Token erstellen', + 'your_token' => 'Dein Token', + 'token_status' => 'Token-Status', + + // Token lists + 'active_tokens' => 'Aktive Token', + 'expired_tokens' => 'Abgelaufene Token', + 'all_tokens' => 'Alle Token', + + // Token properties + 'expires_at' => 'Gültig bis', + 'expires_at_helper_text' => 'Leer lassen, wenn kein Ablaufdatum gewünscht wird', + 'last_used_at' => 'Zuletzt verwendet am', + + // Abilities/Permissions + 'abilities' => 'Fähigkeiten', + 'read_results' => 'Ergebnisse lesen', + 'read_results_description' => 'Das Token hat die Berechtigung zum Lesen von Ergebnissen und Statistiken.', + 'run_speedtest_description' => 'Der Token wird die Berechtigung haben, Geschwindigkeitstest auszuführen.', + 'list_servers_description' => 'Das Token wird die Berechtigung haben, Server zu listen.', +]; diff --git a/lang/de_DE/auth.php b/lang/de_DE/auth.php index f9306fe60..ed5c9f604 100644 --- a/lang/de_DE/auth.php +++ b/lang/de_DE/auth.php @@ -13,8 +13,9 @@ | */ - 'failed' => 'Die eingegebenen Zugangsdaten stimmen nicht überein.', - 'password' => 'Das eingegebene Passwort ist falsch.', - 'throttle' => 'Zu viele Anmeldeversuche. Bitte warte :seconds Sekunden und versuche es erneut.', + 'sign_in' => 'Anmelden', + 'failed' => 'Diese Zugangsdaten stimmen nicht mit unseren Datensätzen überein.', + 'password' => 'Das angegebene Passwort ist falsch.', + 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', ]; diff --git a/lang/de_DE/dashboard.php b/lang/de_DE/dashboard.php new file mode 100644 index 000000000..093e2caab --- /dev/null +++ b/lang/de_DE/dashboard.php @@ -0,0 +1,14 @@ + 'Dashboard', + 'no_speedtests_scheduled' => 'Keine Geschwindigkeitstests geplant.', + 'next_speedtest_at' => 'Nächster Geschwindigkeitstest um', + + // Widgets + 'recent_results' => 'Neueste Ergebnisse', + 'statistics' => 'Statistiken', + 'latest_download' => 'Letzter Download', + 'latest_upload' => 'Letzter Upload', + 'latest_ping' => 'Letztes Ping', +]; diff --git a/lang/de_DE/enums.php b/lang/de_DE/enums.php new file mode 100644 index 000000000..c088858bc --- /dev/null +++ b/lang/de_DE/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Benchmarking', + 'checking' => 'Prüfe', + 'completed' => 'Abgeschlossen', + 'failed' => 'Fehler', + 'running' => 'Laufend', + 'started' => 'Gestartet', + 'skipped' => 'Übersprungen', + 'waiting' => 'Warten', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Faker', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/de_DE/errors.php b/lang/de_DE/errors.php new file mode 100644 index 000000000..fc3319ec3 --- /dev/null +++ b/lang/de_DE/errors.php @@ -0,0 +1,23 @@ + 'Serverfehler', + 'oops_server_error' => 'Hoppla, Serverfehler!', + 'error_message' => 'Fehlermeldung', + 'error_fetching_servers' => 'Fehler beim Abrufen der Server', + 'servers_refreshed_successfully' => 'Server erfolgreich aktualisiert', + 'copied_to_clipboard' => 'In Zwischenablage kopiert', + + // Speedtest specific errors + 'ookla_error' => 'Beim Auflisten von Speedtest Servern ist ein Fehler aufgetreten. Überprüfen Sie die Logs.', + 'cron_invalid' => 'Ungültiger Cron-Ausdruck', + + // Status fix command + 'status_fix' => [ + 'confirm' => 'Möchten Sie fortfahren?', + 'fail' => 'Befehl abgebrochen.', + 'finished' => '✅ fertig!', + 'info_1' => 'Dies prüft alle Ergebnisse und korrigiert den Status auf "abgeschlossen" oder "fehlgeschlagen" basierend auf den Daten.', + 'info_2' => '📖 Lesen Sie die Dokumentation: https://docs.speedtest-tracker.dev/other/commands', + ], +]; diff --git a/lang/de_DE/general.php b/lang/de_DE/general.php new file mode 100644 index 000000000..4cbb97ede --- /dev/null +++ b/lang/de_DE/general.php @@ -0,0 +1,121 @@ + 'Aktuelle Version', + 'latest_version' => 'Neueste Version', + 'github' => 'GitHub', + 'repository' => 'Repository', + + // Common actions + 'save' => 'Speichern', + 'cancel' => 'Abbrechen', + 'delete' => 'Löschen', + 'edit' => 'Bearbeiten', + 'create' => 'Anlegen', + 'search' => 'Suchen', + 'filter' => 'Filtern', + 'export' => 'Exportieren', + 'actions' => 'Aktionen', + 'enable' => 'Aktivieren', + 'yes' => 'Ja', + 'no' => 'Nein', + 'options' => 'Optionen', + 'details' => 'Details', + 'view' => 'Anzeigen', + + // Common labels + 'name' => 'Name', + 'email' => 'E-Mail', + 'email_address' => 'E-Mail-Adresse', + 'password' => 'Passwort', + 'password_confirmation' => 'Passwortbestätigung', + 'id' => 'ID', + 'status' => 'Status', + 'message' => 'Nachricht', + 'comment' => 'Kommentar', + 'comments' => 'Kommentare', + 'created_at' => 'Erstellt am', + 'updated_at' => 'Aktualisiert am', + 'url' => 'URL', + 'server' => 'Server', + 'servers' => 'Server', + 'stats' => 'Statistiken', + 'statistics' => 'Statistiken', + + // Navigation + 'dashboard' => 'Dashboard', + 'results' => 'Ergebnisse', + 'settings' => 'Einstellungen', + 'users' => 'Benutzer', + 'documentation' => 'Dokumentation', + 'view_documentation' => 'Dokumentation anzeigen', + 'links' => 'Links', + 'donate' => 'Spenden', + 'donations' => 'Spenden', + + // Roles + 'admin' => 'Admin', + 'user' => 'Benutzer', + 'role' => 'Rolle', + + // Date ranges + 'last_24h' => 'Letzte 24 Stunden', + 'last_week' => 'Letzte Woche', + 'last_month' => 'Letzten Monat', + + // Metrics + 'metrics' => 'Metriken', + 'average' => 'Durchschnitt', + 'high' => 'Hoch', + 'low' => 'Niedrig', + 'faster' => 'schneller', + 'slower' => 'langsamer', + 'healthy' => 'Gesund', + 'not_measured' => 'Nicht gemessen', + 'unhealthy' => 'fehlerhaft', + + // Units + 'ms' => 'M', + 'mbps' => 'Mbps', + + // Speed test metrics + 'download' => 'Download', + 'upload' => 'Hochladen', + 'ping' => 'Ping', + 'jitter' => 'Jitter', + + // Metric labels with units + 'download_mbps' => 'Download (Mbps)', + 'upload_mbps' => 'Upload (Mbps)', + 'ping_ms' => 'Ping (ms)', + 'download_ms' => 'Download (ms)', + 'upload_ms' => 'Upload (ms)', + 'average_ms' => 'Durchschnitt (ms)', + 'high_ms' => 'Hoch (ms)', + 'low_ms' => 'Niedrig (ms)', + 'ping_ms_label' => 'Ping (ms)', + + // Latency + 'download_latency' => 'Download-Latenz', + 'upload_latency' => 'Upload-Latenz', + + // Actions + 'run_speedtest' => 'Schnelltest ausführen', + 'list_servers' => 'Server auflisten', + 'export_current_results' => 'Aktuelle Ergebnisse exportieren', + 'test' => 'Testen', + + // Common + 'token' => 'Token', + + // Application + 'speedtest_tracker' => 'Speedtest Tracker', + 'platform' => 'Plattform', + + // Update status + 'update_available' => 'Update verfügbar!', + 'up_to_date' => 'Aktuell', + + // Notifications + 'token_created' => 'Token erstellt', +]; diff --git a/lang/de_DE/pagination.php b/lang/de_DE/pagination.php deleted file mode 100644 index a08736d9d..000000000 --- a/lang/de_DE/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Zurück', - 'next' => 'Weiter »', - -]; diff --git a/lang/de_DE/passwords.php b/lang/de_DE/passwords.php index 4d02f3c49..b4625311f 100644 --- a/lang/de_DE/passwords.php +++ b/lang/de_DE/passwords.php @@ -13,11 +13,8 @@ | */ - 'reset' => 'Dein Passwort wurde erfolgreich zurückgesetzt!', - 'sent' => 'Wir haben dir einen Link zum Zurücksetzen des Passworts per E-Mail geschickt!', - 'password' => 'Das Passwort muss mindestens 6 Zeichen lang sein und mit der Bestätigung übereinstimmen.', - 'throttled' => 'Bitte warte einen Moment, bevor du es erneut versuchst.', - 'token' => 'Der Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen.', - 'user' => 'Zu dieser E-Mail-Adresse existiert kein Benutzerkonto.', + 'reset' => 'Ihr Passwort wurde zurückgesetzt!', + 'sent' => 'Wir haben Ihren Link zum Zurücksetzen des Passworts per E-Mail gesendet!', + 'password' => 'Das Passwort und die Bestätigung müssen übereinstimmen und mindestens sechs Zeichen enthalten.', ]; diff --git a/lang/de_DE/results.php b/lang/de_DE/results.php new file mode 100644 index 000000000..a506bce62 --- /dev/null +++ b/lang/de_DE/results.php @@ -0,0 +1,78 @@ + 'Ergebnisse', + 'result_overview' => 'Ergebnisübersicht', + 'error_message_title' => 'Fehlermeldung', + + // Metrics + 'download' => 'Download', + 'download_latency_high' => 'Download-Latenz hoch', + 'download_latency_low' => 'Download-Latenz niedrig', + 'download_latency_iqm' => 'Download-Latenz IQM', + 'download_latency_jitter' => 'Download-Latenz-Jitter', + + 'upload' => 'Upload', + 'upload_latency_high' => 'Upload-Latenz hoch', + 'upload_latency_low' => 'Upload-Latenz niedrig', + 'upload_latency_iqm' => 'Upload-Latenz IQM', + 'upload_latency_jitter' => 'Upload-Latenz-Jitter', + + 'ping' => 'Ping', + 'ping_details' => 'Ping-Details', + 'ping_jitter' => 'Ping-Jitter', + 'ping_high' => 'Ping hoch', + 'ping_low' => 'Ping niedrig', + + 'packet_loss' => 'Paketverlust', + 'iqm' => 'IQM', + + // Server & metadata + 'server_&_metadata' => 'Server & Metadaten', + 'server_id' => 'Server-ID', + 'server_host' => 'Server Host', + 'server_name' => 'Servername', + 'server_location' => 'Serverstandort', + 'service' => 'Service', + 'isp' => 'ISP', + 'ip_address' => 'IP-Adresse', + 'scheduled' => 'Geplant', + + // Filters + 'only_healthy_speedtests' => 'Nur gesunde Geschwindigkeitstests', + 'only_unhealthy_speedtests' => 'Nur ungesunde Geschwindigkeitstests', + 'only_manual_speedtests' => 'Nur manuelle Geschwindigkeitstests', + 'only_scheduled_speedtests' => 'Nur geplante Geschwindigkeitstests', + 'created_from' => 'Erstellt von', + 'created_until' => 'Erstellt bis', + + // Export + 'export_all_results' => 'Alle Ergebnisse exportieren', + 'export_all_results_description' => 'Exportiert jede Spalte für alle Ergebnisse.', + 'export_completed' => 'Export abgeschlossen, :count :rows exportiert.', + 'failed_export' => ':count :rows konnte nicht exportiert werden.', + 'row' => '{1} :count Zeile|[2,*] :count Zeilen', + + // Actions + 'update_comments' => 'Kommentare aktualisieren', + 'view_on_speedtest_net' => 'Auf Speedtest.net anzeigen', + + // Notifications + 'speedtest_benchmark_passed' => 'Geschwindigkeits-Benchmark bestanden', + 'speedtest_benchmark_failed' => 'Geschwindigkeits-Benchmark fehlgeschlagen', + 'speedtest_started' => 'Geschwindigkeit gestartet', + 'speedtest_completed' => 'Geschwindigkeit, abgeschlossen', + 'speedtest_failed' => 'Geschwindigkeit fehlgeschlagen', + 'download_threshold_breached' => 'Download-Schwelle gebrochen!', + 'upload_threshold_breached' => 'Upload-Schwelle gebrochen!', + 'ping_threshold_breached' => 'Ping-Schwelle gebrochen!', + + // Run Speedtest Action + 'speedtest' => 'Schnelligkeit', + 'select_server' => 'Server auswählen', + 'select_server_helper' => 'Leer lassen, um den Speedtest auszuführen, ohne einen Server anzugeben. Blockierte Server werden übersprungen.', + 'manual_servers' => 'Manuelle Server', + 'closest_servers' => 'Closest Server', + 'run_speedtest' => 'Speedtest ausführen', + 'start' => 'Start', +]; diff --git a/lang/de_DE/settings.php b/lang/de_DE/settings.php new file mode 100644 index 000000000..0d327fd2b --- /dev/null +++ b/lang/de_DE/settings.php @@ -0,0 +1,13 @@ + 'Einstellungen', + 'label' => 'Einstellungen', + + // Common settings labels + 'triggers' => 'Auslöser', + 'verify_ssl' => 'SSL überprüfen', + 'username' => 'Benutzername', + 'username_placeholder' => 'Benutzername für Basic Auth (optional)', + 'password_placeholder' => 'Passwort für Basic Auth (optional)', +]; diff --git a/lang/de_DE/settings/data_integration.php b/lang/de_DE/settings/data_integration.php new file mode 100644 index 000000000..71d1936dc --- /dev/null +++ b/lang/de_DE/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Datenintegration', + 'label' => 'Datenintegration', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'Wenn aktiviert, werden alle neuen Speedtest-Ergebnisse auch an InfluxDB gesendet.', + 'influxdb_v2_enabled' => 'Aktivieren', + 'influxdb_v2_url' => 'URL', + 'influxdb_v2_url_placeholder' => 'http://dein-influxdb-Instanz', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Eimer', + 'influxdb_v2_bucket_placeholder' => 'speedtest-Tracker', + 'influxdb_v2_token' => 'Token', + 'influxdb_v2_verify_ssl' => 'SSL überprüfen', + + // Actions + 'test_connection' => 'Verbindung testen', + 'starting_bulk_data_write_to_influxdb' => 'Starte Massendaten in InfluxDB schreiben', + 'sending_test_data_to_influxdb' => 'Senden von Testdaten an InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Influxdb-Test fehlgeschlagen', + 'influxdb_test_failed_body' => 'Überprüfen Sie die Protokolle für weitere Details.', + 'influxdb_test_success' => 'Testdaten erfolgreich an Influxdb gesendet', + 'influxdb_test_success_body' => 'Testdaten wurden an InfluxDB gesendet. Überprüfen Sie, ob die Daten empfangen wurden.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Fehler beim Schreiben von Massendaten in InfluxDB.', + 'influxdb_bulk_write_failed_body' => 'Überprüfen Sie die Protokolle für weitere Details.', + 'influxdb_bulk_write_success' => 'Massendatenlade für Influxdb abgeschlossen.', + 'influxdb_bulk_write_success_body' => 'Daten wurden an InfluxDB gesendet. Überprüfen Sie, ob die Daten empfangen wurden.', + + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Aktivieren', + 'prometheus_enabled_helper_text' => 'Wenn aktiviert, werden neue Messungen für jeden neuen Geschwindigkeitstest am /prometheus Endpunkt verfügbar sein.', + 'prometheus_allowed_ips' => 'Erlaubte IP-Adressen', + 'prometheus_allowed_ips_helper' => 'Liste der IP-Adressen oder CIDR-Bereiche (z.B. 192.168.1.0/24) denen es erlaubt ist, auf den Mess-Endpunkt zuzugreifen. Leer lassen, um alle IPs zu erlauben.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Eimer', +]; diff --git a/lang/de_DE/settings/notifications.php b/lang/de_DE/settings/notifications.php new file mode 100644 index 000000000..8ec3ee6dc --- /dev/null +++ b/lang/de_DE/settings/notifications.php @@ -0,0 +1,61 @@ + 'Benachrichtigungen', + 'label' => 'Benachrichtigungen', + + // Database notifications + 'database' => 'Datenbank', + 'database_description' => 'Benachrichtigungen, die an diesen Kanal gesendet werden, werden unter 🔔 Symbol in der Kopfzeile angezeigt.', + 'test_database_channel' => 'Datenbankkanal testen', + + // Mail notifications + 'mail' => 'Mail', + 'recipients' => 'Empfänger', + 'test_mail_channel' => 'Mail-Kanal testen', + + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Apprise Benachrichtigungen aktivieren', + 'apprise_server' => 'Apprise Server', + 'apprise_server_url' => 'Apprise Server URL', + 'apprise_verify_ssl' => 'SSL verifizieren', + 'apprise_channels' => 'Apprise Kanäle', + 'apprise_channel_url' => 'Kanal URL', + 'apprise_hint_description' => 'Lesen Sie für weitere Informationen zum Einrichten von Apprise die Dokumentation.', + 'apprise_channel_url_helper' => 'Geben Sie die Service Endpoint URL für Benachrichtigung an.', + 'test_apprise_channel' => 'Apprise testen', + 'apprise_channel_url_validation_error' => 'Die Apprise Channel URL muss nicht mit "HTTP" oder "HTTPS" starten. Geben Sie ein valides Apprise URL Schema an.', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Webhook-Kanal testen', + 'webhook_hint_description' => 'Dies sind allgemeine Webhooks. Für Payload-Beispiele und Implementierungsdetails lesen Sie die Dokumentation.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Benachrichtigung bei jedem geplanten Geschwindigkeitstest', + 'notify_on_threshold_failures' => 'Benachrichtigung bei Schwellenausfällen für geplante Geschwindigkeitstests', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'Ich sage: Ping', + 'pong' => 'Sie sagen: Pong', + 'received' => 'Testdatenbank-Benachrichtigung erhalten!', + 'sent' => 'Testdatenbank-Benachrichtigung gesendet.', + ], + 'mail' => [ + 'add' => 'E-Mail-Empfänger hinzufügen!', + 'sent' => 'Test-E-Mail-Benachrichtigung gesendet.', + ], + 'webhook' => [ + 'add' => 'Webhook URLs hinzufügen!', + 'sent' => 'Webhook Benachrichtigung gesendet.', + 'payload' => 'Teste Webhook-Benachrichtigung', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Grenzwert-Benachrichtigungen werden an die /fail Route in der URL gesendet.', +]; diff --git a/lang/de_DE/settings/thresholds.php b/lang/de_DE/settings/thresholds.php new file mode 100644 index 000000000..4a47322ff --- /dev/null +++ b/lang/de_DE/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Grenzwerte', + 'label' => 'Grenzwerte', + + // Absolute thresholds + 'absolute' => 'Absolut', + 'absolute_description' => 'Absolute Schwellenwerte berücksichtigen nicht den vorherigen Verlauf und könnten bei jedem Test ausgelöst werden.', + 'absolute_enabled' => 'absolute Schwellenwerte aktivieren', + + // Metrics section + 'metrics' => 'Metriken', + 'metrics_helper_text' => 'Setze Null, um diese Metrik zu deaktivieren.', + + // General threshold labels + 'thresholds' => 'Grenzwerte', + 'threshold_enabled' => 'Grenzwert aktiviert', + 'threshold_download' => 'Schwellen-Download', + 'threshold_upload' => 'Schwellenwert hochladen', + 'threshold_ping' => 'Grenzwert Ping', +]; diff --git a/lang/de_DE/tools.php b/lang/de_DE/tools.php new file mode 100644 index 000000000..a2cfe5d73 --- /dev/null +++ b/lang/de_DE/tools.php @@ -0,0 +1,6 @@ + 'Ookla Server', +]; diff --git a/lang/de_DE/users.php b/lang/de_DE/users.php new file mode 100644 index 000000000..fd70d8582 --- /dev/null +++ b/lang/de_DE/users.php @@ -0,0 +1,15 @@ + 'Benutzer', + 'label' => 'Benutzer', + + // User prompts and messages + 'user_change' => [ + 'info' => 'Benutzerrolle aktualisiert.', + 'password_updated_info' => ':email Passwort aktualisiert.', + 'what_is_password' => 'Was ist das neue Passwort?', + 'what_is_the_email_address' => 'Wie lautet die E-Mail-Adresse?', + 'what_role' => 'Welche Rolle soll der Benutzer spielen?', + ], +]; diff --git a/lang/de_DE/validation.php b/lang/de_DE/validation.php index 9cbe6946e..d52ae0600 100644 --- a/lang/de_DE/validation.php +++ b/lang/de_DE/validation.php @@ -13,145 +13,6 @@ | */ - 'accepted' => ':attribute muss akzeptiert werden.', - 'accepted_if' => ':attribute muss akzeptiert werden, wenn :other :value ist.', - 'active_url' => ':attribute muss eine gültige URL sein.', - 'after' => ':attribute muss ein Datum nach :date sein.', - 'after_or_equal' => ':attribute muss ein Datum nach oder am :date sein.', - 'alpha' => ':attribute darf nur Buchstaben enthalten.', - 'alpha_dash' => ':attribute darf nur Buchstaben, Zahlen, Binde- und Unterstriche enthalten.', - 'alpha_num' => ':attribute darf nur Buchstaben und Zahlen enthalten.', - 'array' => ':attribute muss eine Liste sein.', - 'ascii' => ':attribute darf nur Standardzeichen enthalten.', - 'before' => ':attribute muss ein Datum vor :date sein.', - 'before_or_equal' => ':attribute muss ein Datum vor oder am :date sein.', - 'between' => [ - 'array' => ':attribute muss zwischen :min und :max Einträge haben.', - 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.', - 'numeric' => ':attribute muss zwischen :min und :max liegen.', - 'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.', - ], - 'boolean' => ':attribute muss wahr oder falsch sein.', - 'can' => ':attribute enthält einen ungültigen Wert.', - 'confirmed' => 'Die Eingabe bei :attribute stimmt nicht mit der Bestätigung überein.', - 'current_password' => 'Das eingegebene Passwort ist falsch.', - 'date' => ':attribute ist kein gültiges Datum.', - 'date_equals' => ':attribute muss genau am :date liegen.', - 'date_format' => ':attribute entspricht nicht dem erforderlichen Format (:format).', - 'decimal' => ':attribute muss :decimal Nachkommastellen haben.', - 'declined' => ':attribute muss abgelehnt werden.', - 'declined_if' => ':attribute muss abgelehnt werden, wenn :other den Wert ":value" hat.', - 'different' => ':attribute und :other müssen verschieden sein.', - 'digits' => ':attribute muss :digits Ziffern lang sein.', - 'digits_between' => ':attribute muss zwischen :min und :max Ziffern lang sein.', - 'dimensions' => ':attribute hat falsche Bildmaße.', - 'distinct' => ':attribute enthält doppelte Werte.', - 'doesnt_end_with' => ':attribute darf nicht mit folgenden Werten enden: :values.', - 'doesnt_start_with' => ':attribute darf nicht mit folgenden Werten beginnen: :values.', - 'email' => ':attribute muss eine gültige E-Mail-Adresse sein.', - 'ends_with' => ':attribute muss mit einem der folgenden Werte enden: :values.', - 'enum' => 'Die gewählte Option bei :attribute ist ungültig.', - 'exists' => ':attribute existiert bereits.', - 'file' => ':attribute muss eine Datei sein.', - 'filled' => ':attribute darf nicht leer sein.', - 'gt' => [ - 'array' => ':attribute muss mehr als :value Einträge enthalten.', - 'file' => ':attribute muss größer als :value Kilobytes sein.', - 'numeric' => ':attribute muss größer als :value sein.', - 'string' => ':attribute muss länger als :value Zeichen sein.', - ], - 'gte' => [ - 'array' => ':attribute muss mindestens :value Einträge enthalten.', - 'file' => ':attribute muss mindestens :value Kilobytes groß sein.', - 'numeric' => ':attribute muss mindestens :value betragen.', - 'string' => ':attribute muss mindestens :value Zeichen lang sein.', - ], - 'image' => ':attribute muss ein Bild sein.', - 'in' => ':attribute ist ungültig.', - 'in_array' => ':attribute muss in :other enthalten sein.', - 'integer' => ':attribute muss eine ganze Zahl sein.', - 'ip' => ':attribute muss eine gültige IP-Adresse sein.', - 'ipv4' => ':attribute muss eine gültige IPv4-Adresse sein.', - 'ipv6' => ':attribute muss eine gültige IPv6-Adresse sein.', - 'json' => ':attribute muss ein gültiges JSON sein.', - 'lowercase' => ':attribute darf nur Kleinbuchstaben enthalten.', - 'lt' => [ - 'array' => ':attribute darf maximal :value Einträge enthalten.', - 'file' => ':attribute muss kleiner als :value Kilobytes sein.', - 'numeric' => ':attribute muss kleiner als :value sein.', - 'string' => ':attribute muss kürzer als :value Zeichen sein.', - ], - 'lte' => [ - 'array' => ':attribute darf maximal :value Einträge enthalten.', - 'file' => ':attribute darf höchstens :value Kilobytes groß sein.', - 'numeric' => ':attribute darf maximal :value betragen.', - 'string' => ':attribute darf maximal :value Zeichen lang sein.', - ], - 'mac_address' => ':attribute muss eine gültige MAC-Adresse sein.', - 'max' => [ - 'array' => ':attribute darf maximal :max Einträge enthalten.', - 'file' => ':attribute darf höchstens :max Kilobytes groß sein.', - 'numeric' => ':attribute darf maximal :max betragen.', - 'string' => ':attribute darf maximal :max Zeichen lang sein.', - ], - 'max_digits' => ':attribute darf maximal :max Ziffern enthalten.', - 'mimes' => ':attribute muss eine Datei vom Typ :values sein.', - 'mimetypes' => ':attribute muss eine Datei im Format :values sein.', - 'min' => [ - 'array' => ':attribute muss mindestens :min Einträge enthalten.', - 'file' => ':attribute muss mindestens :min Kilobytes groß sein.', - 'numeric' => ':attribute muss mindestens :min betragen.', - 'string' => ':attribute muss mindestens :min Zeichen enthalten.', - ], - 'min_digits' => ':attribute muss mindestens :min Ziffern enthalten.', - 'missing' => ':attribute darf nicht angegeben werden.', - 'missing_if' => 'Das Feld :attribute muss fehlen, wenn :other „:value“ ist.', - 'missing_unless' => 'Das Feld :attribute muss fehlen, außer :other ist :value.', - 'missing_with' => 'Das Feld :attribute muss fehlen, wenn :values vorhanden ist.', - 'missing_with_all' => 'Das Feld :attribute muss fehlen, wenn :values vorhanden sind.', - 'multiple_of' => ':attribute muss ein Vielfaches von :value sein.', - 'not_in' => 'Die Auswahl :attribute ist ungültig.', - 'not_regex' => ':attribute hat ein ungültiges Format.', - 'numeric' => ':attribute muss eine Zahl sein.', - 'password' => [ - 'letters' => ':attribute muss mindestens einen Buchstaben enthalten.', - 'mixed' => ':attribute muss mindestens einen Klein- und einen Großbuchstaben enthalten.', - 'numbers' => ':attribute muss mindestens eine Zahl enthalten.', - 'symbols' => ':attribute muss mindestens ein Sonderzeichen enthalten.', - 'uncompromised' => 'Das :attribute wurde in einem Datenleck gefunden. Bitte wählen Sie ein anderes :attribute.', - ], - 'present' => ':attribute muss vorhanden sein.', - 'prohibited' => ':attribute darf nicht angegeben werden.', - 'prohibited_if' => ':attribute darf nicht angegeben werden, wenn :other ":value" ist.', - 'prohibited_unless' => ':attribute darf nur angegeben werden, wenn :other den Wert ":values" hat.', - 'prohibits' => ':attribute darf nicht gemeinsam mit :other angegeben werden.', - 'regex' => ':attribute hat ein ungültiges Format.', - 'required' => ':attribute ist ein Pflichtfeld.', - 'required_array_keys' => ':attribute muss Einträge für folgende Werte enthalten: :values.', - 'required_if' => ':attribute ist erforderlich, wenn :other ":value" ist.', - 'required_if_accepted' => ':attribute ist erforderlich, wenn :other akzeptiert wird.', - 'required_unless' => ':attribute ist erforderlich, außer wenn :other den Wert ":values" hat.', - 'required_with' => ':attribute ist erforderlich, wenn :values vorhanden ist.', - 'required_with_all' => ':attribute ist erforderlich, wenn alle Felder :values ausgefüllt sind.', - 'required_without' => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.', - 'required_without_all' => ':attribute ist erforderlich, wenn keines der Felder :values ausgefüllt ist.', - 'same' => ':attribute muss mit :other übereinstimmen.', - 'size' => [ - 'array' => ':attribute muss genau :size Einträge enthalten.', - 'file' => ':attribute muss :size Kilobytes groß sein.', - 'numeric' => ':attribute muss genau :size betragen.', - 'string' => ':attribute muss genau :size Zeichen lang sein.', - ], - 'starts_with' => ':attribute muss mit einem der folgenden Werte beginnen: :values.', - 'string' => ':attribute muss ein Text sein.', - 'timezone' => ':attribute muss eine gültige Zeitzone sein.', - 'unique' => ':attribute wurde bereits verwendet.', - 'uploaded' => ':attribute konnte nicht hochgeladen werden.', - 'uppercase' => ':attribute darf nur Großbuchstaben enthalten.', - 'url' => ':attribute muss eine gültige URL sein.', - 'ulid' => ':attribute muss eine gültige ULID sein.', - 'uuid' => ':attribute muss eine gültige UUID sein.', - /* |-------------------------------------------------------------------------- | Custom Validation Language Lines @@ -165,7 +26,7 @@ 'custom' => [ 'attribute-name' => [ - 'rule-name' => 'custom-message', + ], ], @@ -181,50 +42,50 @@ */ 'attributes' => [ - 'address' => 'Adresse', - 'age' => 'Alter', - 'body' => 'Inhalt', - 'cell' => 'Zelle', - 'city' => 'Stadt', - 'country' => 'Land', - 'date' => 'Datum', - 'day' => 'Tag', - 'excerpt' => 'Zusammenfassung', - 'first_name' => 'Vorname', + 'address' => 'adresse', + 'age' => 'alt', + 'body' => 'inhalt', + 'cell' => 'zelle', + 'city' => 'stadt', + 'country' => 'land', + 'date' => 'datum', + 'day' => 'tag', + 'excerpt' => 'summary', + 'first_name' => 'vorname', 'gender' => 'Geschlecht', - 'marital_status' => 'Familienstand', + 'marital_status' => 'ehelicher Status', 'profession' => 'Beruf', 'nationality' => 'Nationalität', - 'hour' => 'Stunde', + 'hour' => 'std', 'last_name' => 'Nachname', - 'message' => 'Nachricht', - 'minute' => 'Minute', - 'mobile' => 'Handynummer', - 'month' => 'Monat', - 'name' => 'Name', + 'message' => 'nachricht', + 'minute' => 'minute', + 'mobile' => 'mobile', + 'month' => 'monat', + 'name' => 'name', 'zipcode' => 'Postleitzahl', - 'company_name' => 'Firmenname', - 'neighborhood' => 'Stadtteil', - 'number' => 'Nummer', - 'password' => 'Passwort', - 'phone' => 'Telefonnummer', - 'second' => 'Sekunde', - 'sex' => 'Geschlecht', - 'state' => 'Bundesland', + 'company_name' => 'firmenname', + 'neighborhood' => 'Nachbarschaft', + 'number' => 'anzahl', + 'password' => 'passwort', + 'phone' => 'telefon', + 'second' => 'sekunde', + 'sex' => 'sex', + 'state' => 'status', 'street' => 'Straße', - 'subject' => 'Betreff', - 'text' => 'Text', + 'subject' => 'thema', + 'text' => 'text', 'time' => 'Zeit', - 'title' => 'Titel', - 'username' => 'Benutzername', - 'year' => 'Jahr', - 'description' => 'Beschreibung', - 'password_confirmation' => 'Passwort bestätigen', - 'current_password' => 'Aktuelles Passwort', - 'complement' => 'Zusatz', - 'modality' => 'Modalität', - 'category' => 'Kategorie', - 'blood_type' => 'Blutgruppe', + 'title' => 'titel', + 'username' => 'benutzername', + 'year' => 'jahr', + 'description' => 'beschreibung', + 'password_confirmation' => 'passwort bestätigen', + 'current_password' => 'aktuelles Passwort', + 'complement' => 'ergänzen', + 'modality' => 'modalität', + 'category' => 'kategorie', + 'blood_type' => 'blutiger Typ', 'birth_date' => 'Geburtsdatum', ], ]; diff --git a/lang/en/api_tokens.php b/lang/en/api_tokens.php new file mode 100644 index 000000000..7324ea6b4 --- /dev/null +++ b/lang/en/api_tokens.php @@ -0,0 +1,30 @@ + 'API Tokens', + 'label' => 'API Tokens', + + // Token management + 'api_token' => 'API token', + 'api_tokens' => 'API tokens', + 'create_api_token' => 'Create API token', + 'your_token' => 'Your token', + 'token_status' => 'Token status', + + // Token lists + 'active_tokens' => 'Active tokens', + 'expired_tokens' => 'Expired tokens', + 'all_tokens' => 'All tokens', + + // Token properties + 'expires_at' => 'Expires at', + 'expires_at_helper_text' => 'Leave empty if you don\'t want an expiration date', + 'last_used_at' => 'Last used at', + + // Abilities/Permissions + 'abilities' => 'Abilities', + 'read_results' => 'Read results', + 'read_results_description' => 'The token will have permission to read results and statistics.', + 'run_speedtest_description' => 'The token will have permission to run speedtest.', + 'list_servers_description' => 'The token will have permission to list servers.', +]; diff --git a/lang/zh_TW/auth.php b/lang/en/auth.php similarity index 66% rename from lang/zh_TW/auth.php rename to lang/en/auth.php index 74b841287..f0d112f16 100644 --- a/lang/zh_TW/auth.php +++ b/lang/en/auth.php @@ -13,8 +13,9 @@ | */ - 'failed' => '您輸入的帳號密碼與系統記錄不符。', - 'password' => '您輸入的密碼不正確。', - 'throttle' => '登入嘗試次數太多,請於 :seconds 秒後再試。', + 'sign_in' => 'Sign in', + 'failed' => 'These credentials do not match our records.', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', ]; diff --git a/lang/en/dashboard.php b/lang/en/dashboard.php new file mode 100644 index 000000000..14e843caa --- /dev/null +++ b/lang/en/dashboard.php @@ -0,0 +1,14 @@ + 'Dashboard', + 'no_speedtests_scheduled' => 'No speedtests scheduled.', + 'next_speedtest_at' => 'Next speedtest at', + + // Widgets + 'recent_results' => 'Recent Results', + 'statistics' => 'Statistics', + 'latest_download' => 'Latest download', + 'latest_upload' => 'Latest upload', + 'latest_ping' => 'Latest ping', +]; diff --git a/lang/en/enums.php b/lang/en/enums.php new file mode 100644 index 000000000..c1ff432af --- /dev/null +++ b/lang/en/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Benchmarking', + 'checking' => 'Checking', + 'completed' => 'Completed', + 'failed' => 'Failed', + 'running' => 'Running', + 'started' => 'Started', + 'skipped' => 'Skipped', + 'waiting' => 'Waiting', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Faker', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/en/errors.php b/lang/en/errors.php new file mode 100644 index 000000000..9287fe21a --- /dev/null +++ b/lang/en/errors.php @@ -0,0 +1,23 @@ + 'Server Error', + 'oops_server_error' => 'Oops, server error!', + 'error_message' => 'Error message', + 'error_fetching_servers' => 'Error fetching servers', + 'servers_refreshed_successfully' => 'Servers refreshed successfully', + 'copied_to_clipboard' => 'Copied to clipboard', + + // Speedtest specific errors + 'ookla_error' => 'An error occurred when listing speedtest servers, check the logs.', + 'cron_invalid' => 'Invalid cron expression', + + // Status fix command + 'status_fix' => [ + 'confirm' => 'Do you wish to continue?', + 'fail' => 'Command aborted.', + 'finished' => '✅ done!', + 'info_1' => 'This will check all results and fix the status to "completed" or "failed" based on the data.', + 'info_2' => '📖 Read the documentation: https://docs.speedtest-tracker.dev/other/commands', + ], +]; diff --git a/lang/en/general.php b/lang/en/general.php new file mode 100644 index 000000000..2d39844c2 --- /dev/null +++ b/lang/en/general.php @@ -0,0 +1,125 @@ + 'Current version', + 'latest_version' => 'Latest version', + 'github' => 'GitHub', + 'repository' => 'Repository', + + // Common actions + 'save' => 'Save', + 'cancel' => 'Cancel', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'create' => 'Create', + 'search' => 'Search', + 'filter' => 'Filter', + 'export' => 'Export', + 'actions' => 'Actions', + 'enable' => 'Enable', + 'yes' => 'Yes', + 'no' => 'No', + 'options' => 'Options', + 'details' => 'Details', + 'view' => 'View', + + // Common labels + 'name' => 'Name', + 'email' => 'Email', + 'email_address' => 'Email address', + 'password' => 'Password', + 'password_confirmation' => 'Password confirmation', + 'id' => 'ID', + 'status' => 'Status', + 'message' => 'Message', + 'comment' => 'Comment', + 'comments' => 'Comments', + 'created_at' => 'Created at', + 'updated_at' => 'Updated at', + 'url' => 'URL', + 'server' => 'Server', + 'servers' => 'Servers', + 'stats' => 'Stats', + 'statistics' => 'Statistics', + + // Navigation + 'dashboard' => 'Dashboard', + 'results' => 'Results', + 'settings' => 'Settings', + 'users' => 'Users', + 'documentation' => 'Documentation', + 'view_documentation' => 'View documentation', + 'links' => 'Links', + 'donate' => 'Donate', + 'donations' => 'Donations', + + // Roles + 'admin' => 'Admin', + 'user' => 'User', + 'role' => 'Role', + + // Date ranges + 'last_24h' => 'Last 24 hours', + 'last_week' => 'Last week', + 'last_month' => 'Last month', + + // Metrics + 'metrics' => 'Metrics', + 'average' => 'Average', + 'high' => 'High', + 'low' => 'Low', + 'faster' => 'faster', + 'slower' => 'slower', + 'healthy' => 'Healthy', + 'not_measured' => 'Not measured', + 'unhealthy' => 'Unhealthy', + 'last_results' => 'Last results', + 'total_failed' => 'Total failed tests', + 'total_complted' => 'Total completed tests', + 'total' => 'Total', + + // Units + 'ms' => 'ms', + 'mbps' => 'Mbps', + + // Speed test metrics + 'download' => 'Download', + 'upload' => 'Upload', + 'ping' => 'Ping', + 'jitter' => 'Jitter', + + // Metric labels with units + 'download_mbps' => 'Download (Mbps)', + 'upload_mbps' => 'Upload (Mbps)', + 'ping_ms' => 'Ping (ms)', + 'download_ms' => 'Download (ms)', + 'upload_ms' => 'Upload (ms)', + 'average_ms' => 'Average (ms)', + 'high_ms' => 'High (ms)', + 'low_ms' => 'Low (ms)', + 'ping_ms_label' => 'Ping (ms)', + + // Latency + 'download_latency' => 'Download latency', + 'upload_latency' => 'Upload latency', + + // Actions + 'run_speedtest' => 'Run speedtest', + 'list_servers' => 'List servers', + 'export_current_results' => 'Export current results', + 'test' => 'Test', + + // Common + 'token' => 'Token', + + // Application + 'speedtest_tracker' => 'Speedtest Tracker', + 'platform' => 'Platform', + + // Update status + 'update_available' => 'Update available!', + 'up_to_date' => 'Up to date', + + // Notifications + 'token_created' => 'Token Created', +]; diff --git a/lang/en/results.php b/lang/en/results.php new file mode 100644 index 000000000..8b625d37a --- /dev/null +++ b/lang/en/results.php @@ -0,0 +1,78 @@ + 'Results', + 'result_overview' => 'Result overview', + 'error_message_title' => 'Error message', + + // Metrics + 'download' => 'Download', + 'download_latency_high' => 'Download latency high', + 'download_latency_low' => 'Download latency low', + 'download_latency_iqm' => 'Download latency IQM', + 'download_latency_jitter' => 'Download latency jitter', + + 'upload' => 'Upload', + 'upload_latency_high' => 'Upload latency high', + 'upload_latency_low' => 'Upload latency low', + 'upload_latency_iqm' => 'Upload latency IQM', + 'upload_latency_jitter' => 'Upload latency jitter', + + 'ping' => 'Ping', + 'ping_details' => 'Ping details', + 'ping_jitter' => 'Ping jitter', + 'ping_high' => 'Ping high', + 'ping_low' => 'Ping low', + + 'packet_loss' => 'Packet loss', + 'iqm' => 'IQM', + + // Server & metadata + 'server_&_metadata' => 'Server & Metadata', + 'server_id' => 'Server ID', + 'server_host' => 'Server host', + 'server_name' => 'Server name', + 'server_location' => 'Server location', + 'service' => 'Service', + 'isp' => 'ISP', + 'ip_address' => 'IP address', + 'scheduled' => 'Scheduled', + + // Filters + 'only_healthy_speedtests' => 'Only healthy speedtests', + 'only_unhealthy_speedtests' => 'Only unhealthy speedtests', + 'only_manual_speedtests' => 'Only manual speedtests', + 'only_scheduled_speedtests' => 'Only scheduled speedtests', + 'created_from' => 'Created from', + 'created_until' => 'Created until', + + // Export + 'export_all_results' => 'Export all results', + 'export_all_results_description' => 'Will export every column for all results.', + 'export_completed' => 'Export completed, :count :rows exported.', + 'failed_export' => ':count :rows failed to export.', + 'row' => '{1} :count row|[2,*] :count rows', + + // Actions + 'update_comments' => 'Update comments', + 'view_on_speedtest_net' => 'View on Speedtest.net', + + // Notifications + 'speedtest_benchmark_passed' => 'Speedtest benchmark passed', + 'speedtest_benchmark_failed' => 'Speedtest benchmark failed', + 'speedtest_started' => 'Speedtest started', + 'speedtest_completed' => 'Speedtest completed', + 'speedtest_failed' => 'Speedtest failed', + 'download_threshold_breached' => 'Download threshold breached!', + 'upload_threshold_breached' => 'Upload threshold breached!', + 'ping_threshold_breached' => 'Ping threshold breached!', + + // Run Speedtest Action + 'speedtest' => 'Speedtest', + 'select_server' => 'Select Server', + 'select_server_helper' => 'Leave empty to run the speedtest without specifying a server. Blocked servers will be skipped.', + 'manual_servers' => 'Manual servers', + 'closest_servers' => 'Closest servers', + 'run_speedtest' => 'Run Speedtest', + 'start' => 'Start', +]; diff --git a/lang/en/settings.php b/lang/en/settings.php new file mode 100644 index 000000000..31933b420 --- /dev/null +++ b/lang/en/settings.php @@ -0,0 +1,13 @@ + 'Settings', + 'label' => 'Settings', + + // Common settings labels + 'triggers' => 'Triggers', + 'verify_ssl' => 'Verify SSL', + 'username' => 'Username', + 'username_placeholder' => 'Username for Basic Auth (optional)', + 'password_placeholder' => 'Password for Basic Auth (optional)', +]; diff --git a/lang/en/settings/data_integration.php b/lang/en/settings/data_integration.php new file mode 100644 index 000000000..60ee353d6 --- /dev/null +++ b/lang/en/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Data Integration', + 'label' => 'Data Integration', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'When enabled, all new Speedtest results will also be sent to InfluxDB.', + 'influxdb_v2_enabled' => 'Enable', + 'influxdb_v2_url' => 'URL', + 'influxdb_v2_url_placeholder' => 'http://your-influxdb-instance', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Bucket', + 'influxdb_v2_bucket_placeholder' => 'speedtest-tracker', + 'influxdb_v2_token' => 'Token', + 'influxdb_v2_verify_ssl' => 'Verify SSL', + + // Actions + 'test_connection' => 'Test connection', + 'starting_bulk_data_write_to_influxdb' => 'Starting bulk data write to InfluxDB', + 'sending_test_data_to_influxdb' => 'Sending test data to InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Influxdb test failed', + 'influxdb_test_failed_body' => 'Check the logs for more details.', + 'influxdb_test_success' => 'Successfully sent test data to Influxdb', + 'influxdb_test_success_body' => 'Test data has been sent to InfluxDB, check if the data was received.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Failed to bulk write to Influxdb.', + 'influxdb_bulk_write_failed_body' => 'Check the logs for more details.', + 'influxdb_bulk_write_success' => 'Finished bulk data load to Influxdb.', + 'influxdb_bulk_write_success_body' => 'Data has been sent to InfluxDB, check if the data was received.', + + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Enable', + 'prometheus_enabled_helper_text' => 'When enabled, metrics for each new speedtest will be available at the /prometheus endpoint.', + 'prometheus_allowed_ips' => 'Allowed IP Addresses', + 'prometheus_allowed_ips_helper' => 'List of IP addresses or CIDR ranges (e.g., 192.168.1.0/24) allowed to access the metrics endpoint. Leave empty to allow all IPs.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Bucket', +]; diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php new file mode 100644 index 000000000..8c3145532 --- /dev/null +++ b/lang/en/settings/notifications.php @@ -0,0 +1,65 @@ + 'Notifications', + 'label' => 'Notifications', + + // Database notifications + 'database' => 'Database', + 'database_description' => 'Notifications sent to this channel will show up under the 🔔 icon in the header.', + 'test_database_channel' => 'Test database channel', + + // Mail notifications + 'mail' => 'Mail', + 'recipients' => 'Recipients', + 'test_mail_channel' => 'Test mail channel', + + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Enable Apprise notifications', + 'apprise_server' => 'Apprise Server', + 'apprise_server_url' => 'Apprise Server URL', + 'apprise_server_url_helper' => 'The URL of your Apprise Server. The URL must end on /notify', + 'apprise_verify_ssl' => 'Verify SSL', + 'apprise_channels' => 'Notification Channels', + 'apprise_channel_url' => 'Service URL', + 'apprise_hint_description' => 'Apprise allows you to send notifications to 90+ services. You need to run an Apprise server and configure service URLs below.', + 'apprise_channel_url_helper' => 'Use Apprise URL format. Examples: discord://WebhookID/Token, slack://TokenA/TokenB/TokenC', + 'apprise_save_to_test' => 'Save your settings to test the notification.', + 'test_apprise_channel' => 'Test Apprise', + 'apprise_channel_url_validation_error' => 'Invalid Apprise URL. Must use Apprise format (e.g., discord://, slack://), not http:// or https://. See the Apprise documentation for more information', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Test webhook channel', + 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation. For services like Discord, Ntfy etc please use Apprise.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Notify on every completed scheduled speedtest run', + 'notify_on_every_speedtest_run_helper' => 'This will send a notification for every completed scheduled speedtest run, only for healthy or unbenchmarked tests', + 'notify_on_threshold_failures' => 'Notify on threshold failures for scheduled speedtests', + 'notify_on_threshold_failures_helper' => 'This will send a notification when a scheduled speedtest fails any configured thresholds', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'I say: ping', + 'pong' => 'You say: pong', + 'received' => 'Test database notification received!', + 'sent' => 'Test database notification sent.', + ], + 'mail' => [ + 'add' => 'Add email recipients!', + 'sent' => 'Test mail notification sent.', + ], + 'webhook' => [ + 'add' => 'Add webhook URLs!', + 'sent' => 'Test webhook notification sent.', + 'payload' => 'Testing webhook notification', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Threshold notifications will be sent to the /fail route in the URL.', +]; diff --git a/lang/en/settings/thresholds.php b/lang/en/settings/thresholds.php new file mode 100644 index 000000000..6746d607b --- /dev/null +++ b/lang/en/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Thresholds', + 'label' => 'Thresholds', + + // Absolute thresholds + 'absolute' => 'Absolute', + 'absolute_description' => 'Absolute thresholds do not take into account previous history and could be triggered on each test.', + 'absolute_enabled' => 'Enable absolute thresholds', + + // Metrics section + 'metrics' => 'Metrics', + 'metrics_helper_text' => 'Set to zero to disable this metric.', + + // General threshold labels + 'thresholds' => 'Thresholds', + 'threshold_enabled' => 'Threshold enabled', + 'threshold_download' => 'Threshold download', + 'threshold_upload' => 'Threshold upload', + 'threshold_ping' => 'Threshold ping', +]; diff --git a/lang/en/tools.php b/lang/en/tools.php new file mode 100644 index 000000000..f24f227c4 --- /dev/null +++ b/lang/en/tools.php @@ -0,0 +1,6 @@ + 'Ookla servers', +]; diff --git a/lang/en/users.php b/lang/en/users.php new file mode 100644 index 000000000..e1a2db217 --- /dev/null +++ b/lang/en/users.php @@ -0,0 +1,15 @@ + 'Users', + 'label' => 'Users', + + // User prompts and messages + 'user_change' => [ + 'info' => 'User role updated.', + 'password_updated_info' => ':email password updated.', + 'what_is_password' => 'What is the new password?', + 'what_is_the_email_address' => 'What is the email address?', + 'what_role' => 'What role should the user have?', + ], +]; diff --git a/lang/es_ES/api_tokens.php b/lang/es_ES/api_tokens.php new file mode 100644 index 000000000..762784c33 --- /dev/null +++ b/lang/es_ES/api_tokens.php @@ -0,0 +1,30 @@ + 'Tokens API', + 'label' => 'Tokens API', + + // Token management + 'api_token' => 'API token', + 'api_tokens' => 'Tokens de API', + 'create_api_token' => 'Crear token API', + 'your_token' => 'Tu token', + 'token_status' => 'Estado del token', + + // Token lists + 'active_tokens' => 'Tokens activos', + 'expired_tokens' => 'Tokens caducados', + 'all_tokens' => 'Todos los tokens', + + // Token properties + 'expires_at' => 'Expira el', + 'expires_at_helper_text' => 'Dejar en blanco si no desea una fecha de caducidad', + 'last_used_at' => 'Último usado en', + + // Abilities/Permissions + 'abilities' => 'Habilidades', + 'read_results' => 'Leer resultados', + 'read_results_description' => 'El token tendrá permiso para leer resultados y estadísticas.', + 'run_speedtest_description' => 'El token tendrá permiso para ejecutar el test de velocidad.', + 'list_servers_description' => 'El token tendrá permiso para listar servidores.', +]; diff --git a/lang/es_ES/auth.php b/lang/es_ES/auth.php index a6e861a77..b150afe6b 100644 --- a/lang/es_ES/auth.php +++ b/lang/es_ES/auth.php @@ -13,8 +13,9 @@ | */ + 'sign_in' => 'Iniciar sesión', 'failed' => 'Estas credenciales no coinciden con nuestros registros.', 'password' => 'La contraseña proporcionada es incorrecta.', - 'throttle' => 'Demasiados intentos de acceso. Por favor, inténtelo de nuevo en :seconds segundos.', + 'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, inténtalo de nuevo en :seconds segundos.', ]; diff --git a/lang/es_ES/dashboard.php b/lang/es_ES/dashboard.php new file mode 100644 index 000000000..b6783d90f --- /dev/null +++ b/lang/es_ES/dashboard.php @@ -0,0 +1,14 @@ + 'Tablero', + 'no_speedtests_scheduled' => 'No hay pruebas de velocidad programadas.', + 'next_speedtest_at' => 'Siguiente prueba de velocidad en', + + // Widgets + 'recent_results' => 'Resultados recientes', + 'statistics' => 'Estadísticas', + 'latest_download' => 'Última descarga', + 'latest_upload' => 'Última subida', + 'latest_ping' => 'Último ping', +]; diff --git a/lang/es_ES/enums.php b/lang/es_ES/enums.php new file mode 100644 index 000000000..3c8089b13 --- /dev/null +++ b/lang/es_ES/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Marcando', + 'checking' => 'Comprobando', + 'completed' => 'Completado', + 'failed' => 'Fallo', + 'running' => 'Ejecutando', + 'started' => 'Iniciado', + 'skipped' => 'Omitido', + 'waiting' => 'Esperando', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Faker', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/es_ES/errors.php b/lang/es_ES/errors.php new file mode 100644 index 000000000..c11da734d --- /dev/null +++ b/lang/es_ES/errors.php @@ -0,0 +1,23 @@ + 'Error del servidor', + 'oops_server_error' => '¡Uy, error del servidor!', + 'error_message' => 'Mensaje de error', + 'error_fetching_servers' => 'Error obteniendo servidores', + 'servers_refreshed_successfully' => 'Servidores actualizados con éxito', + 'copied_to_clipboard' => 'Copiado al portapapeles', + + // Speedtest specific errors + 'ookla_error' => 'Se ha producido un error al listar servidores de prueba de velocidad, comprobar los registros.', + 'cron_invalid' => 'Expresión cron no válida', + + // Status fix command + 'status_fix' => [ + 'confirm' => '¿Desea continuar?', + 'fail' => 'Comando abortado.', + 'finished' => '✅ ¡Hecho!', + 'info_1' => 'Esto comprobará todos los resultados y corregirá el estado a "completado" o "fallado" basado en los datos.', + 'info_2' => '📖 Lee la documentación: https://docs.speedtest-tracker.dev/other/commands', + ], +]; diff --git a/lang/es_ES/general.php b/lang/es_ES/general.php new file mode 100644 index 000000000..fddf7a982 --- /dev/null +++ b/lang/es_ES/general.php @@ -0,0 +1,121 @@ + 'Versión actual', + 'latest_version' => 'Última versión', + 'github' => 'GitHub', + 'repository' => 'Repositorio', + + // Common actions + 'save' => 'Guardar', + 'cancel' => 'Cancelar', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'create' => 'Crear', + 'search' => 'Buscar', + 'filter' => 'Filtro', + 'export' => 'Exportar', + 'actions' => 'Acciones', + 'enable' => 'Activar', + 'yes' => 'Sí', + 'no' => 'Nu', + 'options' => 'Opciones', + 'details' => 'Detalles', + 'view' => 'Ver', + + // Common labels + 'name' => 'Nombre', + 'email' => 'E-mail', + 'email_address' => 'Dirección de email', + 'password' => 'Contraseña', + 'password_confirmation' => 'Confirmación de contraseña', + 'id' => 'ID', + 'status' => 'Estado', + 'message' => 'Mensaje', + 'comment' => 'Comentario', + 'comments' => 'Comentarios', + 'created_at' => 'Creado el', + 'updated_at' => 'Actualizado el', + 'url' => 'URL', + 'server' => 'Servidor', + 'servers' => 'Servidores', + 'stats' => 'Estadísticas', + 'statistics' => 'Estadísticas', + + // Navigation + 'dashboard' => 'Tablero', + 'results' => 'Resultados', + 'settings' => 'Ajustes', + 'users' => 'Usuarios', + 'documentation' => 'Documentación', + 'view_documentation' => 'Ver documentación', + 'links' => 'Enlaces', + 'donate' => 'Donar', + 'donations' => 'Donaciones', + + // Roles + 'admin' => 'Admin', + 'user' => 'Usuario', + 'role' => 'Rol', + + // Date ranges + 'last_24h' => 'Últimas 24 horas', + 'last_week' => 'Última semana', + 'last_month' => 'Último mes', + + // Metrics + 'metrics' => 'Métricas', + 'average' => 'Promedio', + 'high' => 'Alta', + 'low' => 'Baja', + 'faster' => 'más rápido', + 'slower' => 'más lento', + 'healthy' => 'Saludable', + 'not_measured' => 'No medido', + 'unhealthy' => 'Poco saludable', + + // Units + 'ms' => 'm', + 'mbps' => 'Mbps', + + // Speed test metrics + 'download' => 'Descargar', + 'upload' => 'Subir', + 'ping' => 'Ping', + 'jitter' => 'Jitter', + + // Metric labels with units + 'download_mbps' => 'Descargar (Mbps)', + 'upload_mbps' => 'Subir (Mbps)', + 'ping_ms' => 'Ping (ms)', + 'download_ms' => 'Descargar (ms)', + 'upload_ms' => 'Subir (ms)', + 'average_ms' => 'Promedio (ms)', + 'high_ms' => 'Alto (ms)', + 'low_ms' => 'Baja (ms)', + 'ping_ms_label' => 'Ping (ms)', + + // Latency + 'download_latency' => 'Descargar latencia', + 'upload_latency' => 'Cargar latencia', + + // Actions + 'run_speedtest' => 'Ejecutar el test de velocidad', + 'list_servers' => 'Listar servidores', + 'export_current_results' => 'Exportar resultados actuales', + 'test' => 'Prueba', + + // Common + 'token' => 'Token', + + // Application + 'speedtest_tracker' => 'Rastreador más rápido', + 'platform' => 'Plataforma', + + // Update status + 'update_available' => '¡Actualización disponible!', + 'up_to_date' => 'Actualizado', + + // Notifications + 'token_created' => 'Token creado', +]; diff --git a/lang/es_ES/pagination.php b/lang/es_ES/pagination.php deleted file mode 100644 index f8f044e19..000000000 --- a/lang/es_ES/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Anterior', - 'next' => 'Siguiente »', - -]; diff --git a/lang/es_ES/passwords.php b/lang/es_ES/passwords.php index 488763598..c1132d746 100644 --- a/lang/es_ES/passwords.php +++ b/lang/es_ES/passwords.php @@ -13,8 +13,8 @@ | */ - 'reset' => '¡Su contraseña ha sido restablecida!', - 'sent' => '¡Hemos enviado por correo electrónico el enlace para restablecer su contraseña!', + 'reset' => '¡Tu contraseña ha sido restablecida!', + 'sent' => '¡Hemos enviado por correo electrónico tu enlace de restablecimiento de contraseña!', 'password' => 'La contraseña y la confirmación deben coincidir y contener al menos seis caracteres.', ]; diff --git a/lang/es_ES/results.php b/lang/es_ES/results.php new file mode 100644 index 000000000..89df7e984 --- /dev/null +++ b/lang/es_ES/results.php @@ -0,0 +1,78 @@ + 'Resultados', + 'result_overview' => 'Resumen de resultados', + 'error_message_title' => 'Mensaje de error', + + // Metrics + 'download' => 'Descargar', + 'download_latency_high' => 'Descargar latencia alta', + 'download_latency_low' => 'Descargar latencia baja', + 'download_latency_iqm' => 'Descargar latencia IQM', + 'download_latency_jitter' => 'Descargar jitter de latencia', + + 'upload' => 'Subir', + 'upload_latency_high' => 'Subir latencia alta', + 'upload_latency_low' => 'Subir latencia baja', + 'upload_latency_iqm' => 'Cargar latencia IQM', + 'upload_latency_jitter' => 'Subir jitter de latencia', + + 'ping' => 'Señal', + 'ping_details' => 'Detalles de ping', + 'ping_jitter' => 'Ping jitter', + 'ping_high' => 'Ping alto', + 'ping_low' => 'Ping bajo', + + 'packet_loss' => 'Pérdida del paquete', + 'iqm' => 'IQM', + + // Server & metadata + 'server_&_metadata' => 'Servidor y metadatos', + 'server_id' => 'ID del servidor', + 'server_host' => 'Servidor host', + 'server_name' => 'Nombre del servidor', + 'server_location' => 'Ubicación del servidor', + 'service' => 'Servicio', + 'isp' => 'ISP', + 'ip_address' => 'Dirección IP', + 'scheduled' => 'Programado', + + // Filters + 'only_healthy_speedtests' => 'Sólo pruebas de velocidad saludables', + 'only_unhealthy_speedtests' => 'Sólo pruebas de velocidad poco saludables', + 'only_manual_speedtests' => 'Sólo pruebas de velocidad manuales', + 'only_scheduled_speedtests' => 'Sólo pruebas de velocidad programadas', + 'created_from' => 'Creado a partir de', + 'created_until' => 'Creado hasta', + + // Export + 'export_all_results' => 'Exportar todos los resultados', + 'export_all_results_description' => 'Exportará cada columna para todos los resultados.', + 'export_completed' => 'Exportación completada, :count :rows exportadas.', + 'failed_export' => ':count :rows falló al exportar.', + 'row' => '{1} :count fila|[2,*] :count filas', + + // Actions + 'update_comments' => 'Actualizar comentarios', + 'view_on_speedtest_net' => 'Ver en Speedtest.net', + + // Notifications + 'speedtest_benchmark_passed' => 'La prueba de rendimiento de velocidad ha pasado', + 'speedtest_benchmark_failed' => 'Prueba de rendimiento de velocidad fallida', + 'speedtest_started' => 'Velocidad iniciada', + 'speedtest_completed' => 'Velocidad completada', + 'speedtest_failed' => 'Error en la prueba de velocidad', + 'download_threshold_breached' => '¡Umbral de descarga incumplido!', + 'upload_threshold_breached' => '¡Umbral de subida infringido!', + 'ping_threshold_breached' => '¡Umbral de ping infringido!', + + // Run Speedtest Action + 'speedtest' => 'Velocidad', + 'select_server' => 'Seleccionar Servidor', + 'select_server_helper' => 'Dejar en blanco para ejecutar el test de velocidad sin especificar un servidor. Se omitirán los servidores bloqueados.', + 'manual_servers' => 'Servidores manuales', + 'closest_servers' => 'Servidor más cerrado', + 'run_speedtest' => 'Ejecutar prueba de velocidad', + 'start' => 'Empezar', +]; diff --git a/lang/es_ES/settings.php b/lang/es_ES/settings.php new file mode 100644 index 000000000..102901771 --- /dev/null +++ b/lang/es_ES/settings.php @@ -0,0 +1,13 @@ + 'Ajustes', + 'label' => 'Ajustes', + + // Common settings labels + 'triggers' => 'Disparadores', + 'verify_ssl' => 'Verificar SSL', + 'username' => 'Usuario', + 'username_placeholder' => 'Nombre de usuario para Auth Básica (opcional)', + 'password_placeholder' => 'Contraseña para autenticación básica (opcional)', +]; diff --git a/lang/es_ES/settings/data_integration.php b/lang/es_ES/settings/data_integration.php new file mode 100644 index 000000000..a1b024848 --- /dev/null +++ b/lang/es_ES/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Integración de datos', + 'label' => 'Integración de datos', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'Cuando está activado, todos los nuevos resultados de Speedtest también serán enviados a InfluxDB.', + 'influxdb_v2_enabled' => 'Activar', + 'influxdb_v2_url' => 'URL', + 'influxdb_v2_url_placeholder' => 'http://su-instancia-influxdb', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Cubo', + 'influxdb_v2_bucket_placeholder' => 'rastreador de velocidad', + 'influxdb_v2_token' => 'Token', + 'influxdb_v2_verify_ssl' => 'Verificar SSL', + + // Actions + 'test_connection' => 'Probar conexión', + 'starting_bulk_data_write_to_influxdb' => 'Iniciando escritura masiva de datos en InfluxDB', + 'sending_test_data_to_influxdb' => 'Enviando datos de prueba a InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Prueba de Influxdb fallida', + 'influxdb_test_failed_body' => 'Revisa los registros para más detalles.', + 'influxdb_test_success' => 'Datos de prueba enviados con éxito a Influxdb', + 'influxdb_test_success_body' => 'Los datos de prueba han sido enviados a InfluxDB, compruebe si los datos han sido recibidos.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Error al escribir en masa a Influxdb.', + 'influxdb_bulk_write_failed_body' => 'Revisa los registros para más detalles.', + 'influxdb_bulk_write_success' => 'Carga de datos en masa a Influxdb.', + 'influxdb_bulk_write_success_body' => 'Los datos han sido enviados a InfluxDB, compruebe si los datos han sido recibidos.', + + // Prometheus + 'prometheus' => 'Prometeo', + 'prometheus_enabled' => 'Activar', + 'prometheus_enabled_helper_text' => 'Cuando está activado, las métricas para cada prueba de velocidad nueva estarán disponibles en el punto final /prometheus.', + 'prometheus_allowed_ips' => 'Direcciones IP permitidas', + 'prometheus_allowed_ips_helper' => 'Lista de direcciones IP o rangos CIDR (por ejemplo, 192.168.1.0/24) permitieron acceder al extremo de las métricas. Dejar en blanco para permitir todas las IPs.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Cubo', +]; diff --git a/lang/es_ES/settings/notifications.php b/lang/es_ES/settings/notifications.php new file mode 100644 index 000000000..83d8bbf20 --- /dev/null +++ b/lang/es_ES/settings/notifications.php @@ -0,0 +1,61 @@ + 'Notificaciones', + 'label' => 'Notificaciones', + + // Database notifications + 'database' => 'Base de datos', + 'database_description' => 'Las notificaciones enviadas a este canal se mostrarán bajo el icono :belell: en el encabezado.', + 'test_database_channel' => 'Probar canal de base de datos', + + // Mail notifications + 'mail' => 'Correo', + 'recipients' => 'Destinatarios', + 'test_mail_channel' => 'Canal de prueba de correo', + + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Habilitar notificaciones Apprise', + 'apprise_server' => 'Servidor Apprise', + 'apprise_server_url' => 'URL del servidor', + 'apprise_verify_ssl' => 'Verificar SSL', + 'apprise_channels' => 'Canales de expedición', + 'apprise_channel_url' => 'URL del canal', + 'apprise_hint_description' => 'Para más información sobre cómo configurar Apprise, vea la documentación.', + 'apprise_channel_url_helper' => 'Proporcionar la URL de los puntos finales del servicio para las notificaciones.', + 'test_apprise_channel' => 'Prueba de aviso', + 'apprise_channel_url_validation_error' => 'La URL del canal Apprise no debe comenzar con "http" o "https". Por favor, proporcione un esquema de URL de Apprise válido.', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Probar canal webhook', + 'webhook_hint_description' => 'Estos son webhooks genéricos. Para ejemplos de payload y detalles de la implementación, vea la documentación.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Notificar en cada prueba de velocidad programada', + 'notify_on_threshold_failures' => 'Notificar fallos de umbral para pruebas de velocidad programadas', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'Yo digo: ping', + 'pong' => 'Dice usted: pong', + 'received' => 'Notificación de la base de datos de prueba recibida!', + 'sent' => 'Notificación de prueba de base de datos enviada.', + ], + 'mail' => [ + 'add' => '¡Añadir destinatarios de correo!', + 'sent' => 'Notificación de correo de prueba enviada.', + ], + 'webhook' => [ + 'add' => '¡Añadir URL de webhook!', + 'sent' => 'Prueba de notificación de webhook enviada.', + 'payload' => 'Probando notificación de webhook', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Las notificaciones de umbral se enviarán a la ruta /fail en la URL.', +]; diff --git a/lang/es_ES/settings/thresholds.php b/lang/es_ES/settings/thresholds.php new file mode 100644 index 000000000..8fc4e7748 --- /dev/null +++ b/lang/es_ES/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Umbrales', + 'label' => 'Umbrales', + + // Absolute thresholds + 'absolute' => 'Absoluto', + 'absolute_description' => 'Los umbrales absolutos no tienen en cuenta la historia anterior y podrían ser activados en cada prueba.', + 'absolute_enabled' => 'Habilitar umbrales absolutos', + + // Metrics section + 'metrics' => 'Métricas', + 'metrics_helper_text' => 'Establecer en cero para desactivar esta métrica.', + + // General threshold labels + 'thresholds' => 'Umbrales', + 'threshold_enabled' => 'Umbral habilitado', + 'threshold_download' => 'Umbral de descarga', + 'threshold_upload' => 'Umbral de subida', + 'threshold_ping' => 'Umbral de ping', +]; diff --git a/lang/es_ES/tools.php b/lang/es_ES/tools.php new file mode 100644 index 000000000..249d79aae --- /dev/null +++ b/lang/es_ES/tools.php @@ -0,0 +1,6 @@ + 'Servidores Ookla', +]; diff --git a/lang/es_ES/users.php b/lang/es_ES/users.php new file mode 100644 index 000000000..b3e7c1a88 --- /dev/null +++ b/lang/es_ES/users.php @@ -0,0 +1,15 @@ + 'Usuarios', + 'label' => 'Usuarios', + + // User prompts and messages + 'user_change' => [ + 'info' => 'Rol de usuario actualizado.', + 'password_updated_info' => ':email contraseña actualizada.', + 'what_is_password' => '¿Cuál es la nueva contraseña?', + 'what_is_the_email_address' => '¿Cuál es la dirección de correo electrónico?', + 'what_role' => '¿Qué rol debe tener el usuario?', + ], +]; diff --git a/lang/es_ES/validation.php b/lang/es_ES/validation.php index 74b244c43..9eb5a792a 100644 --- a/lang/es_ES/validation.php +++ b/lang/es_ES/validation.php @@ -45,12 +45,12 @@ 'address' => 'dirección', 'age' => 'edad', 'body' => 'contenido', - 'cell' => 'celular', + 'cell' => 'celda', 'city' => 'ciudad', 'country' => 'país', 'date' => 'fecha', 'day' => 'día', - 'excerpt' => 'resumen', + 'excerpt' => 'summary', 'first_name' => 'nombre', 'gender' => 'género', 'marital_status' => 'estado civil', @@ -64,8 +64,8 @@ 'month' => 'mes', 'name' => 'nombre', 'zipcode' => 'código postal', - 'company_name' => 'nombre de la empresa', - 'neighborhood' => 'vecindario', + 'company_name' => 'nombre de empresa', + 'neighborhood' => 'vecindad', 'number' => 'número', 'password' => 'contraseña', 'phone' => 'teléfono', @@ -73,11 +73,11 @@ 'sex' => 'sexo', 'state' => 'estado', 'street' => 'calle', - 'subject' => 'asunto', + 'subject' => 'tema', 'text' => 'texto', - 'time' => 'hora', + 'time' => 'tiempo', 'title' => 'título', - 'username' => 'usuario', + 'username' => 'nombre de usuario', 'year' => 'año', 'description' => 'descripción', 'password_confirmation' => 'confirmación de contraseña', diff --git a/lang/fr_FR/api_tokens.php b/lang/fr_FR/api_tokens.php new file mode 100644 index 000000000..1511fc17a --- /dev/null +++ b/lang/fr_FR/api_tokens.php @@ -0,0 +1,30 @@ + 'Jetons API', + 'label' => 'Jetons API', + + // Token management + 'api_token' => 'jeton API', + 'api_tokens' => 'jetons API', + 'create_api_token' => 'Créer un jeton API', + 'your_token' => 'Votre jeton', + 'token_status' => 'Statut du jeton', + + // Token lists + 'active_tokens' => 'Jetons actifs', + 'expired_tokens' => 'Jetons expirés', + 'all_tokens' => 'Tous les jetons', + + // Token properties + 'expires_at' => 'Expire le', + 'expires_at_helper_text' => 'Laisser vide si vous ne voulez pas de date d\'expiration', + 'last_used_at' => 'Dernière utilisation le', + + // Abilities/Permissions + 'abilities' => 'Capacités', + 'read_results' => 'Lire les résultats', + 'read_results_description' => 'Le jeton aura la permission de lire les résultats et les statistiques.', + 'run_speedtest_description' => 'Le jeton aura la permission d\'exécuter des tests de vitesse.', + 'list_servers_description' => 'Le jeton aura la permission de lister les serveurs.', +]; diff --git a/lang/fr_FR/auth.php b/lang/fr_FR/auth.php index 4cc78b5cf..fc8d1390b 100644 --- a/lang/fr_FR/auth.php +++ b/lang/fr_FR/auth.php @@ -13,8 +13,9 @@ | */ - 'failed' => 'Ces crédentials ne correspondent pas à nos archives.', + 'sign_in' => 'Se connecter', + 'failed' => 'Ces identifiants ne correspondent pas à nos enregistrements.', 'password' => 'Le mot de passe fourni est incorrect.', - 'throttle' => 'Trop de tentatives de connexion échouées. Veuillez réessayer dans :seconds secondes.', + 'throttle' => 'Trop de tentatives de connexion. Veuillez réessayer dans :seconds secondes.', ]; diff --git a/lang/fr_FR/dashboard.php b/lang/fr_FR/dashboard.php new file mode 100644 index 000000000..7faae0a35 --- /dev/null +++ b/lang/fr_FR/dashboard.php @@ -0,0 +1,14 @@ + 'Tableau de bord', + 'no_speedtests_scheduled' => 'Aucun test de vitesse programmé.', + 'next_speedtest_at' => 'Prochain test de vitesse à', + + // Widgets + 'recent_results' => 'Résultats récents', + 'statistics' => 'Statistiques', + 'latest_download' => 'Dernier téléchargement', + 'latest_upload' => 'Dernier envoi', + 'latest_ping' => 'Dernière latence', +]; diff --git a/lang/fr_FR/enums.php b/lang/fr_FR/enums.php new file mode 100644 index 000000000..c5afd6771 --- /dev/null +++ b/lang/fr_FR/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Évaluation comparative', + 'checking' => 'En cours de vérification', + 'completed' => 'Terminé', + 'failed' => 'Échec', + 'running' => 'En cours d\'exécution', + 'started' => 'Démarré', + 'skipped' => 'Ignoré', + 'waiting' => 'En attente', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Faker', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/fr_FR/errors.php b/lang/fr_FR/errors.php new file mode 100644 index 000000000..c42fe736a --- /dev/null +++ b/lang/fr_FR/errors.php @@ -0,0 +1,23 @@ + 'Erreur serveur', + 'oops_server_error' => 'Oups, erreur de serveur !', + 'error_message' => 'Message d\'erreur', + 'error_fetching_servers' => 'Erreur lors de la récupération des serveurs', + 'servers_refreshed_successfully' => 'Serveurs actualisés avec succès', + 'copied_to_clipboard' => 'Copié dans le presse-papiers', + + // Speedtest specific errors + 'ookla_error' => 'Une erreur s’est produite pendant la création de la liste des serveurs, vérifiez les logs.', + 'cron_invalid' => 'Expression cron invalide', + + // Status fix command + 'status_fix' => [ + 'confirm' => 'Voulez-vous continuer ?', + 'fail' => 'Commande abandonnée.', + 'finished' => '✅ terminé !', + 'info_1' => 'Cela vérifiera tous les résultats et corrigera le statut à "terminé" ou "échec" en fonction des données.', + 'info_2' => '📖 Lisez la documentation: https://docs.speedtest-tracker.dev/other/commands', + ], +]; diff --git a/lang/fr_FR/general.php b/lang/fr_FR/general.php new file mode 100644 index 000000000..4b9422d7f --- /dev/null +++ b/lang/fr_FR/general.php @@ -0,0 +1,121 @@ + 'Version actuelle', + 'latest_version' => 'Dernière version', + 'github' => 'GitHub', + 'repository' => 'Dépôt', + + // Common actions + 'save' => 'Enregistrer', + 'cancel' => 'Abandonner', + 'delete' => 'Supprimer', + 'edit' => 'Modifier', + 'create' => 'Créer', + 'search' => 'Chercher', + 'filter' => 'Filtrer', + 'export' => 'Exporter', + 'actions' => 'Actions', + 'enable' => 'Activer', + 'yes' => 'Oui', + 'no' => 'Non', + 'options' => 'Options', + 'details' => 'Détails', + 'view' => 'Voir', + + // Common labels + 'name' => 'Nom', + 'email' => 'Email', + 'email_address' => 'Adresse e-mail', + 'password' => 'Mot de passe', + 'password_confirmation' => 'Confirmation du mot de passe', + 'id' => 'Identifiant', + 'status' => 'Statut', + 'message' => 'Message', + 'comment' => 'Commentaire', + 'comments' => 'Commentaires', + 'created_at' => 'Créé le', + 'updated_at' => 'Mis à jour le', + 'url' => 'URL', + 'server' => 'Serveur', + 'servers' => 'Serveurs', + 'stats' => 'Stats', + 'statistics' => 'Statistiques', + + // Navigation + 'dashboard' => 'Tableau de bord', + 'results' => 'Résultats', + 'settings' => 'Réglages', + 'users' => 'Utilisateurs', + 'documentation' => 'Documentation', + 'view_documentation' => 'Afficher la documentation', + 'links' => 'Liens', + 'donate' => 'Faire un don', + 'donations' => 'Dons', + + // Roles + 'admin' => 'Administrateur', + 'user' => 'Utilisateur', + 'role' => 'Rôle', + + // Date ranges + 'last_24h' => 'Dernières 24 heures', + 'last_week' => 'La semaine dernière', + 'last_month' => 'Le mois dernier', + + // Metrics + 'metrics' => 'Métriques', + 'average' => 'Moyenne', + 'high' => 'Élevé', + 'low' => 'Bas', + 'faster' => 'rapide', + 'slower' => 'lent', + 'healthy' => 'Sain', + 'not_measured' => 'Non mesuré', + 'unhealthy' => 'Malsain', + + // Units + 'ms' => 'ms', + 'mbps' => 'Mbit/s', + + // Speed test metrics + 'download' => 'Téléchargement', + 'upload' => 'Envoi', + 'ping' => 'Latence', + 'jitter' => 'Gigue', + + // Metric labels with units + 'download_mbps' => 'Téléchargement (Mbit/s)', + 'upload_mbps' => 'Envoi (Mbit/s)', + 'ping_ms' => 'Latence (ms)', + 'download_ms' => 'Téléchargement (ms)', + 'upload_ms' => 'Envoi (ms)', + 'average_ms' => 'Moyenne (ms)', + 'high_ms' => 'Élevé (ms)', + 'low_ms' => 'Bas (ms)', + 'ping_ms_label' => 'Latence (ms)', + + // Latency + 'download_latency' => 'Délai de téléchargement', + 'upload_latency' => 'Délai d\'envoi', + + // Actions + 'run_speedtest' => 'Test de vitesse', + 'list_servers' => 'Liste des serveurs', + 'export_current_results' => 'Exporter les résultats actuels', + 'test' => 'Tester', + + // Common + 'token' => 'Jeton', + + // Application + 'speedtest_tracker' => 'Suivi de test de vitesse', + 'platform' => 'Plateforme', + + // Update status + 'update_available' => 'Mise à jour disponible !', + 'up_to_date' => 'À jour', + + // Notifications + 'token_created' => 'Jeton créé', +]; diff --git a/lang/fr_FR/pagination.php b/lang/fr_FR/pagination.php deleted file mode 100644 index 8eff37464..000000000 --- a/lang/fr_FR/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Précédent', - 'next' => 'Suivant »', - -]; diff --git a/lang/fr_FR/passwords.php b/lang/fr_FR/passwords.php index e570f50a5..fc96f7ae3 100644 --- a/lang/fr_FR/passwords.php +++ b/lang/fr_FR/passwords.php @@ -13,11 +13,8 @@ | */ - 'reset' => 'Votre mot de passe a été réinitialisé!', - 'sent' => 'Nous vous avons envoyé un email contenant votre lien de réinitialisation!', - 'password' => 'Le mot de passe et le mot de passe de confirmation doivent contenir au moins 6 caractères.', - 'throttled' => 'Veuillez attendre avant de réessayer.', - 'token' => 'Le jeton de réinitialisation du mot de passe n\'est pas valide.', - 'user' => 'Aucun email trouvé pour cette adresse.', + 'reset' => 'Votre mot de passe a été réinitialisé !', + 'sent' => 'Nous avons envoyé un e-mail pour réinitialiser votre mot de passe !', + 'password' => 'Le mot de passe et la confirmation doivent correspondre et contenir au moins six caractères.', ]; diff --git a/lang/fr_FR/results.php b/lang/fr_FR/results.php new file mode 100644 index 000000000..ab2715930 --- /dev/null +++ b/lang/fr_FR/results.php @@ -0,0 +1,78 @@ + 'Résultats', + 'result_overview' => 'Aperçu des résultats', + 'error_message_title' => 'Message d\'erreur', + + // Metrics + 'download' => 'Téléchargement', + 'download_latency_high' => 'Latence de téléchargement élevée', + 'download_latency_low' => 'Latence de téléchargement bas', + 'download_latency_iqm' => 'Latence de téléchargement MIQ', + 'download_latency_jitter' => 'Latence de téléchargement gigue', + + 'upload' => 'Envoi', + 'upload_latency_high' => 'Latence d\'envoi élevée', + 'upload_latency_low' => 'Latence d\'envoi faible', + 'upload_latency_iqm' => 'Latence d\'envoi MIQ', + 'upload_latency_jitter' => 'Latence d\'envoi gigue', + + 'ping' => 'Latence', + 'ping_details' => 'Détails des latences', + 'ping_jitter' => 'Latence gigue', + 'ping_high' => 'Latence élevée', + 'ping_low' => 'Latence faible', + + 'packet_loss' => 'Perte de paquets', + 'iqm' => 'MIQ', + + // Server & metadata + 'server_&_metadata' => 'Serveur et Métadonnées', + 'server_id' => 'Identifiant du serveur', + 'server_host' => 'Hôte du serveur', + 'server_name' => 'Nom du serveur', + 'server_location' => 'Emplacement du serveur', + 'service' => 'Service', + 'isp' => 'FAI', + 'ip_address' => 'Adresse IP', + 'scheduled' => 'Planifié', + + // Filters + 'only_healthy_speedtests' => 'Uniquement les tests de vitesse sains', + 'only_unhealthy_speedtests' => 'Uniquement les tests de vitesse ratés', + 'only_manual_speedtests' => 'Uniquement les tests de vitesse manuels', + 'only_scheduled_speedtests' => 'Uniquement les tests de vitesse programmés', + 'created_from' => 'Créé à partir de', + 'created_until' => 'Créé jusqu\'au', + + // Export + 'export_all_results' => 'Exporter tous les résultats', + 'export_all_results_description' => 'Exporte chaque colonne pour tous les résultats.', + 'export_completed' => 'Exportation terminée, :count :rows exportée.', + 'failed_export' => ':count :rows a échoué à l\'exportation.', + 'row' => '{1} :count ligne|[2,*] :count lignes', + + // Actions + 'update_comments' => 'Actualiser les commentaires', + 'view_on_speedtest_net' => 'Voir sur Speedtest.net', + + // Notifications + 'speedtest_benchmark_passed' => 'Le benchmark du test de vitesse a été passé', + 'speedtest_benchmark_failed' => 'Le benchmark du test de vitesse a échoué', + 'speedtest_started' => 'Test de vitesse démarré', + 'speedtest_completed' => 'Test de vitesse terminé', + 'speedtest_failed' => 'Le test de vitesse a échoué', + 'download_threshold_breached' => 'Seuil de téléchargement dépassé !', + 'upload_threshold_breached' => 'Seuil d\'envoi dépassé !', + 'ping_threshold_breached' => 'Seuil de latence dépassé !', + + // Run Speedtest Action + 'speedtest' => 'Test de vitesse', + 'select_server' => 'Sélectionner un serveur', + 'select_server_helper' => 'Laisser vide pour exécuter le test de vitesse sans spécifier de serveur. Les serveurs bloqués seront ignorés.', + 'manual_servers' => 'Serveurs manuels', + 'closest_servers' => 'Serveurs les plus proches', + 'run_speedtest' => 'Lancer le test de vitesse', + 'start' => 'Démarrer', +]; diff --git a/lang/fr_FR/settings.php b/lang/fr_FR/settings.php new file mode 100644 index 000000000..3196d8eb5 --- /dev/null +++ b/lang/fr_FR/settings.php @@ -0,0 +1,13 @@ + 'Réglages', + 'label' => 'Réglages', + + // Common settings labels + 'triggers' => 'Déclencheurs', + 'verify_ssl' => 'Vérifier SSL', + 'username' => 'Nom d\'utilisateur', + 'username_placeholder' => 'Nom d\'utilisateur pour l\'authentification de base (facultatif)', + 'password_placeholder' => 'Mot de passe pour l\'authentification de base (facultatif)', +]; diff --git a/lang/fr_FR/settings/data_integration.php b/lang/fr_FR/settings/data_integration.php new file mode 100644 index 000000000..ce0af0775 --- /dev/null +++ b/lang/fr_FR/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Intégration des données', + 'label' => 'Intégration des données', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'Lorsque cette option est activée, tous les nouveaux résultats des tests de vitesse seront également envoyés à InfluxDB.', + 'influxdb_v2_enabled' => 'Activer', + 'influxdb_v2_url' => 'URL', + 'influxdb_v2_url_placeholder' => 'http://votre-instance-influxdb', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Seau', + 'influxdb_v2_bucket_placeholder' => 'test-de-vitesse-tracker', + 'influxdb_v2_token' => 'Jeton', + 'influxdb_v2_verify_ssl' => 'Vérifier SSL', + + // Actions + 'test_connection' => 'Tester la connexion', + 'starting_bulk_data_write_to_influxdb' => 'Démarrage de l\'écriture de données en masse sur InfluxDB', + 'sending_test_data_to_influxdb' => 'Envoi de données de test à InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Échec du test Influxdb', + 'influxdb_test_failed_body' => 'Consultez les journaux pour plus de détails.', + 'influxdb_test_success' => 'Données de test envoyées à Influxdb avec succès', + 'influxdb_test_success_body' => 'Les données de test ont été envoyées à InfluxDB, vérifiez si les données ont été reçues.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Impossible d\'écrire en masse sur Influxdb.', + 'influxdb_bulk_write_failed_body' => 'Consultez les journaux pour plus de détails.', + 'influxdb_bulk_write_success' => 'Charge de données en masse terminée sur Influxdb.', + 'influxdb_bulk_write_success_body' => 'Les données ont été envoyées à InfluxDB, vérifiez si les données ont été reçues.', + + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Activer', + 'prometheus_enabled_helper_text' => 'Lorsque cette option est activée, les métriques pour chaque nouveau test de vitesse seront disponibles au point de terminaison /prometheus.', + 'prometheus_allowed_ips' => 'Adresses IP autorisées', + 'prometheus_allowed_ips_helper' => 'Liste des adresses IP ou des plages CIDR (par exemple, 192.168.1.0/24) autorisés à accéder au point de terminaison des métriques. Laisser vide pour autoriser toutes les IPs.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Seau', +]; diff --git a/lang/fr_FR/settings/notifications.php b/lang/fr_FR/settings/notifications.php new file mode 100644 index 000000000..359a1424a --- /dev/null +++ b/lang/fr_FR/settings/notifications.php @@ -0,0 +1,61 @@ + 'Notifications', + 'label' => 'Notifications', + + // Database notifications + 'database' => 'Base de données', + 'database_description' => 'Les notifications envoyées à ce salon apparaîtront sous l\'icône 🔔 dans l\'entête.', + 'test_database_channel' => 'Tester le canal de base de données', + + // Mail notifications + 'mail' => 'Courrier', + 'recipients' => 'Destinataires', + 'test_mail_channel' => 'Tester le canal de messagerie', + + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Activer les notifications de base de données', + 'apprise_server' => 'Serveur Apprise', + 'apprise_server_url' => 'Serveur Apprise', + 'apprise_verify_ssl' => 'Vérifier SSL', + 'apprise_channels' => 'Canaux Apprise', + 'apprise_channel_url' => 'URL du canal de mise à jour', + 'apprise_hint_description' => 'Pour plus d\'informations sur la configuration d\'Apprise, consultez la documentation.', + 'apprise_channel_url_helper' => 'Fournir l\'URL de terminaison du service pour les notifications.', + 'test_apprise_channel' => 'Apprise de test', + 'apprise_channel_url_validation_error' => 'L\'URL du canal Apprise ne doit pas commencer par "http" ou "https". Veuillez fournir un schéma d\'URL Apprise valide.', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Tester le canal webhook', + 'webhook_hint_description' => 'Ce sont des webhooks génériques. Pour des exemples de charge utile et des détails d\'implémentation, consultez la documentation.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Notifier à chaque test de vitesse programmé', + 'notify_on_threshold_failures' => 'Notifier les pannes de seuil pour les tests de vitesse programmés', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'Je dis: ping', + 'pong' => 'Vous dites: pong', + 'received' => 'Notification de base de données de test reçue !', + 'sent' => 'Notification de base de données de test envoyée.', + ], + 'mail' => [ + 'add' => 'Ajouter des destinataires d\'e-mail!', + 'sent' => 'Notification de test envoyée par e-mail.', + ], + 'webhook' => [ + 'add' => 'Ajouter des URL de webhook !', + 'sent' => 'Notification de test du webhook envoyée.', + 'payload' => 'Test de la notification de webhook', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Les notifications de seuil seront envoyées à la route /fail dans l\'URL.', +]; diff --git a/lang/fr_FR/settings/thresholds.php b/lang/fr_FR/settings/thresholds.php new file mode 100644 index 000000000..26ee7a23b --- /dev/null +++ b/lang/fr_FR/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Seuils', + 'label' => 'Seuils', + + // Absolute thresholds + 'absolute' => 'Absolu', + 'absolute_description' => 'Les seuils absolus ne tiennent pas compte de l\'historique précédent et peuvent être déclenchés à chaque test.', + 'absolute_enabled' => 'Activer les seuils absolus', + + // Metrics section + 'metrics' => 'Métriques', + 'metrics_helper_text' => 'Mettre à zéro pour désactiver cette métrique.', + + // General threshold labels + 'thresholds' => 'Seuils', + 'threshold_enabled' => 'Seuil activé', + 'threshold_download' => 'Seuil de téléchargement', + 'threshold_upload' => 'Seuil d\'envoi', + 'threshold_ping' => 'Seuil de ping', +]; diff --git a/lang/fr_FR/tools.php b/lang/fr_FR/tools.php new file mode 100644 index 000000000..d579be2eb --- /dev/null +++ b/lang/fr_FR/tools.php @@ -0,0 +1,6 @@ + 'Serveurs Ookla', +]; diff --git a/lang/fr_FR/users.php b/lang/fr_FR/users.php new file mode 100644 index 000000000..d699452c9 --- /dev/null +++ b/lang/fr_FR/users.php @@ -0,0 +1,15 @@ + 'Utilisateurs', + 'label' => 'Utilisateurs', + + // User prompts and messages + 'user_change' => [ + 'info' => 'Rôle de l\'utilisateur mis à jour.', + 'password_updated_info' => 'Mot de passe de l\'adresse mail mis à jour.', + 'what_is_password' => 'Quel est le nouveau mot de passe ?', + 'what_is_the_email_address' => 'Quelle est l\'adresse e-mail ?', + 'what_role' => 'Quel devrait être le rôle de l\'utilisateur ?', + ], +]; diff --git a/lang/fr_FR/validation.php b/lang/fr_FR/validation.php index 2803207d4..292aec82d 100644 --- a/lang/fr_FR/validation.php +++ b/lang/fr_FR/validation.php @@ -13,145 +13,6 @@ | */ - 'accepted' => 'Le champ :attribute doit être valide.', - 'accepted_if' => 'Le champ :attribute doit être accepté lorsque :other est :value.', - 'active_url' => 'Le champ :attribute doit être une URL valide.', - 'after' => 'Le champ :attribute doit être une date postérieure à :date.', - 'after_or_equal' => 'Le champ :attribute doit être une date postérieure ou égale à :date.', - 'alpha' => 'Le champ :attribute ne doit contenir que des lettres.', - 'alpha_dash' => 'Le champ :attribute ne doit contenir que des lettres, des chiffres, des tirets ou underscore.', - 'alpha_num' => 'Le champ :attribute ne doit contenir que des lettres et des chiffres.', - 'array' => 'Le champ :attribute doit être un tableau.', - 'ascii' => 'Le champ :attribute ne doit contenir que des caractères alphanumériques ou des symboles ascii.', - 'before' => 'Le champ :attribute doit être une date antérieure à :date.', - 'before_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale à :date.', - 'between' => [ - 'array' => 'Le champ :attribute doit contenir entre :min et :max elements.', - 'file' => 'Le champ :attribute doit être compris entre :min et :max kilo-octets.', - 'numeric' => 'Le champ :attribute doit être comprise entre :min et :max.', - 'string' => 'Le champ :attribute doit contenir entre :min et :max caractères.', - ], - 'boolean' => 'Le champ :attribute doit être vrai ou faux.', - 'can' => 'Le champ :attribute contient une valeur non autorisée.', - 'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.', - 'current_password' => 'Le mot de passe est incorrect.', - 'date' => 'Le champ :attribute doit être une date valide.', - 'date_equals' => 'Le champ :attribute doit être une date égale à :date.', - 'date_format' => 'Le champ :attribute doit correspondre au format :format.', - 'decimal' => 'Le champ :attribute doit avoir :decimal chiffres décimaux.', - 'declined' => 'Le champ :attribute doit être refusé.', - 'declined_if' => 'Le champ :attribute doit être rejeté lorsque :other est :value.', - 'different' => 'Le champ :attribute et :other doivent être différents.', - 'digits' => 'Le champ :attribute doit être composé de :digits chiffres.', - 'digits_between' => 'Le champ :attribute doit être compris entre :min et :max.', - 'dimensions' => 'Le champ :attribute taille de la photo non valide.', - 'distinct' => 'Le champ :attribute a une valeur dupliquée.', - 'doesnt_end_with' => 'Le champ :attribute ne doit pas se terminer par l\'un des éléments suivants: :values.', - 'doesnt_start_with' => 'Le champ :attribute ne doit pas commencer par l\'un des éléments suivants: :values.', - 'email' => 'Le champ :attribute doit être une adresse email valide.', - 'ends_with' => 'Le champ :attribute doit se terminer par l\'un des éléments suivants: :values.', - 'enum' => ':attribute séléctionné non valide.', - 'exists' => ':attribute existe déjà.', - 'file' => 'Le champ :attribute doit être un fichier.', - 'filled' => 'Le champ :attribute doit contenir une valeur.', - 'gt' => [ - 'array' => 'Le champ :attribute doit contenir plus de :value éléments.', - 'file' => 'Le champ :attribute doit être supérieur à :value kilo-octets.', - 'numeric' => 'Le champ :attribute doit être supérieur à :value.', - 'string' => 'Le champ :attribute doit faire plus de :value caractères.', - ], - 'gte' => [ - 'array' => 'Le champ :attribute doit contenir au moins :value éléments.', - 'file' => 'Le champ :attribute doit être supérieur ou égal à :value kilo-octets.', - 'numeric' => 'Le champ :attribute doit être supérieur ou égal à :value.', - 'string' => 'Le champ :attribute doit être supérieur ou égal à :value caractères.', - ], - 'image' => 'Le champ :attribute doit être une photo.', - 'in' => ':attribute séléctionné non valide.', - 'in_array' => 'Le champ :attribute doit être contenant dans :other.', - 'integer' => 'Le champ :attribute doit être un nombre entier.', - 'ip' => 'Le champ :attribute doit être une adresse IP valide.', - 'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.', - 'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.', - 'json' => 'Le champ :attribute doit être une string JSON valide.', - 'lowercase' => 'Le champ :attribute doit être en minuscule.', - 'lt' => [ - 'array' => 'Le champ :attribute doit contenir moins de :value elements.', - 'file' => 'Le champ :attribute doit être inférieur à :value kilo-octets.', - 'numeric' => 'Le champ :attribute doit être inférieur à :value.', - 'string' => 'Le champ :attribute doit faire moins de :value caractères.', - ], - 'lte' => [ - 'array' => 'Le champ :attribute ne doit pas comporter plus de :value éléments.', - 'file' => 'Le champ :attribute doit être inférieur ou égal à :value kilo-octets.', - 'numeric' => 'Le champ :attribute doit être inférieur ou égal à :value.', - 'string' => 'Le champ :attribute doit être inférieur ou égal à :value caractères.', - ], - 'mac_address' => 'Le champ :attribute doit être une adresse MAC valide.', - 'max' => [ - 'array' => 'Le champ :attribute ne doit pas comporter plus de :max éléments.', - 'file' => 'Le champ :attribute ne doit pas être supérieur à :max kilo-octets.', - 'numeric' => 'Le champ :attribute ne doit pas être supérieur à :max.', - 'string' => 'Le champ :attribute non ne doit pas faire plus de :max caractères.', - ], - 'max_digits' => 'Le champ :attribute ne peut avoir plus de :max chiffres.', - 'mimes' => 'Le champ :attribute doit être un fichier de type: :values.', - 'mimetypes' => 'Le champ :attribute doit être un fichier de type: :values.', - 'min' => [ - 'array' => 'Le champ :attribute doit contenir au moins :min éléments.', - 'file' => 'Le champ :attribute doit être au minimum de :min kilo-octets.', - 'numeric' => 'Le champ :attribute doit être au moins :min.', - 'string' => 'Le champ :attribute deve contenir au moins :min caractères.', - ], - 'min_digits' => 'Le champ :attribute doit avoir au moins :min chiffres.', - 'missing' => 'Le champ :attribute doit être manquant.', - 'missing_if' => 'Le champ :attribute doit être absent lorsque :other est :value.', - 'missing_unless' => 'Le champ :attribute doit être manquant, sauf si :other est :value.', - 'missing_with' => 'Le champ :attribute doit être absent lorsque :values est présent.', - 'missing_with_all' => 'Le champ :attribute doit être manquant lorsque :values sont présentes.', - 'multiple_of' => 'Le champ :attribute doit être un multiple de :value.', - 'not_in' => ':attribute séléctionné non valide.', - 'not_regex' => 'Le format du champ :attribute est invalide.', - 'numeric' => 'Le champ :attribute doit être numérique.', - 'password' => [ - 'letters' => 'Le champ :attribute doit cotenir au moins une lettre.', - 'mixed' => 'Le champ :attribute doit conteniur au moins un caractère minuscule et un caractère majuscule.', - 'numbers' => 'Le champ :attribute doit contenir au moins un chiffre.', - 'symbols' => 'Le champ :attribute doit contenir au moins un symbole spécial.', - 'uncompromised' => 'L\':attribute est apparu dans une fuite de données. Veuillez choisir un autre :attribut.', - ], - 'present' => 'Le champ :attribute doit être présent.', - 'prohibited' => 'Le champ :attribute est interdit.', - 'prohibited_if' => 'Le champ :attribute est interdit quand :other est :value.', - 'prohibited_unless' => 'Le champ :attribute est interdit à moins que :other ne figure dans :values.', - 'prohibits' => 'Le champ :attribute interdit à :other d\'être présent.', - 'regex' => 'Le format du champ :attribute est invalide.', - 'required' => 'Le champ :attribute est obligatoire.', - 'required_array_keys' => 'Le champ :attribute doit contenir des entrées pour: :values.', - 'required_if' => 'Le champ :attribute est obligatoire quand :other est :value.', - 'required_if_accepted' => 'Le champ :attribute est nécessaire lorsque :other est accepté.', - 'required_unless' => 'Le champ :attribute est obligatoire, sauf si :other figure dans :values.', - 'required_with' => 'Le champ :attribute est obligatoire lorsque :values est présent.', - 'required_with_all' => 'Le champ :attribute est obligatoire lorsque :values est présent.', - 'required_without' => 'Le champ :attribute est requis lorsque :values n\'est pas présent', - 'required_without_all' => 'Le champ :attribute est nécessaire lorsqu\'aucune des valeurs :values n\'est présente.', - 'same' => 'Le champ :attribute doit correspondre à :other.', - 'size' => [ - 'array' => 'Le champ :attribute doit contenir des :size éléments.', - 'file' => 'Le champ :attribute doit être :size kilo-octets.', - 'numeric' => 'Le champ :attribute doit être :size.', - 'string' => 'Le champ :attribute doit faire :size caractères.', - ], - 'starts_with' => 'Le champ :attribute doit commencer avec: :values.', - 'string' => 'Le champ :attribute doit être une chaine de caractères.', - 'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.', - 'unique' => 'Il :attribute doit être unqieu.', - 'uploaded' => 'Il :attribute n\'a pas pu être téléchargé.', - 'uppercase' => 'Le champ :attribute doit être en majuscule.', - 'url' => 'Le champ :attribute doit être une URL valide.', - 'ulid' => 'Le champ :attribute doit être un ULID valide.', - 'uuid' => 'Le champ :attribute doit être un UUID valide.', - /* |-------------------------------------------------------------------------- | Custom Validation Language Lines @@ -165,7 +26,7 @@ 'custom' => [ 'attribute-name' => [ - 'rule-name' => 'custom-message', + ], ], @@ -182,7 +43,7 @@ 'attributes' => [ 'address' => 'adresse', - 'age' => 'age', + 'age' => 'âge', 'body' => 'contenu', 'cell' => 'cellule', 'city' => 'ville', @@ -192,31 +53,31 @@ 'excerpt' => 'résumé', 'first_name' => 'prénom', 'gender' => 'sexe', - 'marital_status' => 'situation familliale', + 'marital_status' => 'état civil', 'profession' => 'profession', 'nationality' => 'nationalité', 'hour' => 'heure', 'last_name' => 'nom de famille', 'message' => 'message', 'minute' => 'minute', - 'mobile' => 'mobile', + 'mobile' => 'portable', 'month' => 'mois', 'name' => 'nom', 'zipcode' => 'code postal', - 'company_name' => 'entreprise', + 'company_name' => 'nom de la société', 'neighborhood' => 'quartier', 'number' => 'numéro', 'password' => 'mot de passe', 'phone' => 'téléphone', 'second' => 'seconde', 'sex' => 'sexe', - 'state' => 'région', + 'state' => 'état', 'street' => 'rue', 'subject' => 'sujet', - 'text' => 'texte', + 'text' => 'teste', 'time' => 'temps', 'title' => 'titre', - 'username' => 'login', + 'username' => 'nom d\'utilisateur', 'year' => 'année', 'description' => 'description', 'password_confirmation' => 'confirmation du mot de passe', diff --git a/lang/hr_HR/auth.php b/lang/hr_HR/auth.php deleted file mode 100644 index adad2012c..000000000 --- a/lang/hr_HR/auth.php +++ /dev/null @@ -1,20 +0,0 @@ - 'A megadott hitelesítési adatok nem egyeznek.', - 'password' => 'A megadott jelszó hibás.', - 'throttle' => 'Túl sok bejelentkezési kísérlet. Kérlek, várj :seconds másodpercet, mielőtt újra próbálkozol.', - -]; diff --git a/lang/hr_HR/pagination.php b/lang/hr_HR/pagination.php deleted file mode 100644 index f5a6700fb..000000000 --- a/lang/hr_HR/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Prethodno', - 'next' => 'Sljedeće »', - -]; diff --git a/lang/hr_HR/passwords.php b/lang/hr_HR/passwords.php deleted file mode 100644 index cd56761c9..000000000 --- a/lang/hr_HR/passwords.php +++ /dev/null @@ -1,22 +0,0 @@ - 'Tvoja lozinka je uspješno resetirana!', - 'sent' => 'Poslali smo ti poveznicu za poništavanje lozinke putem e-pošte!', - 'throttled' => 'Molimo pričekaj malo prije nego što pokušaš ponovno.', - 'token' => 'Token za resetiranje lozinke je nevažeći ili je istekao.', - 'user' => 'Ne postoji korisnik s tom e-mail adresom.', - -]; diff --git a/lang/hr_HR/translations.php b/lang/hr_HR/translations.php deleted file mode 100644 index b538615a0..000000000 --- a/lang/hr_HR/translations.php +++ /dev/null @@ -1,290 +0,0 @@ - 'Admin', - 'User' => 'Korisnik', - 'abilities' => 'Sposobnosti', - 'active_tokens' => 'Aktivni tokeni', - 'all_tokens' => 'Svi tokeni', - 'api_token' => 'API token', - 'api_tokens' => 'API tokeni', - 'average' => 'Prosjek', - 'average_ms' => 'Prosjek (ms)', - 'Benchmarking' => 'Benchmarking', - 'bucket' => 'Kanta', - 'Checking' => 'Provjera', - 'comment' => 'Komentar', - 'comments' => 'Komentari', - 'Completed' => 'Završeno', - 'create_api_token' => 'Kreiraj API token', - 'created_at' => 'Kreirano', - 'created_from' => 'Kreirano od', - 'created_until' => 'Kreirano do', - 'cron_invalid' => 'Nevažeći cron izraz', - 'data_integration' => 'Integracija podataka', - 'database' => 'Baza podataka', - 'database_description' => 'Obavijesti poslane na ovaj kanal prikazuju se pod ikonom 🔔 u zaglavlju.', - 'details' => 'Detalji', - 'dashboard' => 'Nadzorna ploča', - 'discord' => 'Discord', - 'documentation' => 'Dokumentacija', - 'donate' => 'Doniraj', - 'download' => 'Preuzimanje', - 'download_latency' => 'Kašnjenje preuzimanja', - 'download_latency_high' => 'Visoko kašnjenje preuzimanja', - 'download_latency_iqm' => 'IQM kašnjenje preuzimanja', - 'download_latency_jitter' => 'Jitter kašnjenja preuzimanja', - 'download_latency_low' => 'Nisko kašnjenje preuzimanja', - 'download_mbps' => 'Preuzimanje (Mbps)', - 'download_ms' => 'Preuzimanje (ms)', - 'email' => 'Email', - 'email_address' => 'Email adresa', - 'enable' => 'Omogući', - 'enable_database_notifications' => 'Omogući obavijesti baze podataka', - 'enable_discord_webhook_notifications' => 'Omogući Discord webhook obavijesti', - 'enable_mail_notifications' => 'Omogući email obavijesti', - 'enable_pushover_webhook_notifications' => 'Omogući Pushover webhook obavijesti', - 'enable_telegram' => 'Omogući Telegram obavijesti', - 'enable_webhook_notifications' => 'Omogući webhook obavijesti', - 'error_message' => 'Poruka o grešci', - 'expired_tokens' => 'Istekli tokeni', - 'expires_at' => 'Ističe', - 'expires_at_helper_text' => 'Ostavite prazno ako ne želite datum isteka', - 'export_all_results' => 'Izvezi sve rezultate', - 'export_all_results_description' => 'Izvest će se svi stupci za sve rezultate.', - 'export_completed' => 'Izvoz završen, :count :rows izvezeno.', - 'export_current_results' => 'Izvezi trenutne rezultate', - 'Failed' => 'Neuspjelo', - 'failed_export' => ':count :rows nije uspjelo izvesti.', - 'Faker' => 'faker', - 'faster' => 'Brže', - 'general_settings' => [ - 'label' => 'Opće postavke', - 'description' => 'Ovdje se mogu postaviti opće postavke aplikacije.', - 'app_settings' => 'Postavke aplikacije', - 'speedtest_settings' => 'Postavke testa brzine', - 'api_settings' => 'Api postavke', - 'app_name' => 'Naziv aplikacije', - 'asset_url' => 'URL resursa', - 'app_timezone' => 'Vremenska zona aplikacije', - 'chart_begin_at_zero' => 'Graf počinje od nule', - 'chart_datetime_format' => 'Format datuma i vremena za graf', - 'datetime_format' => 'Format datuma i vremena', - 'display_timezone' => 'Prikazana vremenska zona', - 'public_dashboard' => 'Javna nadzorna ploča', - 'speedtest_skip_ips' => 'Speedtest preskočeni IP-ovi', - 'speedtest_schedule' => 'Raspored speedtesta', - 'speedtest_schedule_description' => 'Unesite valjane cron izraze. Primjer: * * * * * pokreće se svake minute.', - 'speedtest_servers' => 'Speedtest poslužitelji', - 'speedtest_blocked_servers' => 'Blokirani speedtest poslužitelji', - 'speedtest_interface' => 'Speedtest sučelje', - 'speedtest_checkinternet_url' => 'Speedtest URL za provjeru interneta', - 'threshold_enabled' => 'Prag omogućeno', - 'threshold_download' => 'Prag preuzimanja', - 'threshold_upload' => 'Prag slanja', - 'threshold_ping' => 'Prag pinga', - 'prune_results_older_than' => 'Izbriši rezultate starije od', - 'api_rate_limit' => 'API ograničenje brzine', - - ], - 'gotify' => 'Gotify', - 'gotify_enabled' => 'Omogući Gotify webhook obavijesti', - 'healthcheck_enabled' => 'Omogući healthcheck.io webhook obavijesti', - 'healthcheck_io' => 'Healthcheck.io', - 'healthy' => 'Zdravo', - 'high' => 'Visoko', - 'high_ms' => 'Visoko (ms)', - 'id' => 'ID', - 'infoluxdb' => 'InfluxDB v2', - 'infoluxdb_description' => 'Ako je omogućeno, novi Speedtest rezultati će biti poslani u InfluxDB.', - 'ip_address' => 'IP adresa', - 'iqm' => 'IQM', - 'isp' => 'ISP', - 'jitter' => 'Jitter', - 'last_24h' => 'Posljednjih 24 sata', - 'last_month' => 'Prošli mjesec', - 'last_used_at' => 'Zadnje korištenje', - 'last_week' => 'Prošli tjedan', - 'latest_download' => 'Zadnje preuzimanje', - 'latest_ping' => 'Zadnji ping', - 'latest_upload' => 'Zadnje slanje', - 'links' => 'Linkovi', - 'list_servers' => 'Popis servera', - 'list_servers_description' => 'Token dobiva ovlaštenje za popis servera.', - 'low' => 'Nisko', - 'low_ms' => 'Nisko (ms)', - 'mail' => 'E-mail', - 'message' => 'Poruka', - 'ms' => 'ms', - 'name' => 'Ime', - 'next_speedtest_at' => 'Sljedeći speedtest: ', - 'no' => 'Ne', - 'no_speedtests_scheduled' => 'Nema zakazanih speedtestova.', - 'notifications' => [ - 'label' => 'Obavijesti', - 'database' => [ - 'ping' => 'Kažem: ping', - 'pong' => 'Ti kažeš: pong', - 'received' => 'Primljena testna obavijest baze podataka!', - 'sent' => 'Poslana testna obavijest baze podataka.', - ], - 'discord' => [ - 'add' => 'Dodaj Discord URL-ove!', - 'sent' => 'Poslana testna Discord obavijest.', - 'payload' => '👋 Testiramo Discord kanal za obavijesti.', - ], - 'health_check' => [ - 'add' => 'Dodaj HealthCheck.io URL-ove!', - 'sent' => 'Poslana testna HealthCheck.io obavijest.', - 'payload' => '👋 Testiramo HealthCheck.io kanal za obavijesti.', - ], - 'gotfy' => [ - 'add' => 'Dodaj Gotify URL-ove!', - 'sent' => 'Poslana testna Gotify obavijest.', - 'payload' => '👋 Testiramo Gotify kanal za obavijesti.', - ], - 'mail' => [ - 'add' => 'Dodaj email primatelje!', - 'sent' => 'Poslana testna email obavijest.', - ], - 'ntfy' => [ - 'add' => 'Dodaj ntfy URL-ove!', - 'sent' => 'Poslana testna ntfy obavijest.', - 'payload' => '👋 Testiramo ntfy kanal za obavijesti.', - ], - 'pushover' => [ - 'add' => 'Dodaj Pushover URL-ove!', - 'sent' => 'Poslana testna Pushover obavijest.', - 'payload' => '👋 Testiramo Pushover kanal za obavijesti.', - ], - 'slack' => [ - 'add' => 'Dodaj Slack URL-ove!', - 'sent' => 'Poslana testna Slack obavijest.', - 'payload' => '👋 Testiramo Slack kanal za obavijesti.', - ], - 'telegram' => [ - 'add' => 'Dodaj Telegram primatelje!', - 'sent' => 'Poslana testna Telegram obavijest.', - ], - 'webhook' => [ - 'add' => 'Dodaj webhook URL-ove!', - 'sent' => 'Poslana testna webhook obavijest.', - 'payload' => 'Testiranje webhook obavijesti', - ], - ], - 'notify_on_every_speedtest_run' => 'Obavijesti za svaki speedtest', - 'notify_on_threshold_failures' => 'Obavijesti kod prekoračenja praga', - 'ntfy' => 'ntfy', - 'ntfy_enabled' => 'Omogući ntfy webhook obavijesti', - 'only_healthy_speedtests' => 'Samo zdravi speedtestovi', - 'only_manual_speedtests' => 'Samo ručni speedtestovi', - 'only_scheduled_speedtests' => 'Samo zakazani speedtestovi', - 'only_unhealthy_speedtests' => 'Samo neispravni speedtestovi', - 'Ookla' => 'Ookla', - 'ookla_error' => 'Došlo je do greške pri listanju speedtest servera, provjerite logove.', - 'options' => 'Opcije', - 'org' => 'Organizacija', - 'packet_loss' => 'Gubitak paketa', - 'password' => 'Lozinka', - 'password_confirmation' => 'Potvrda lozinke', - 'password_placeholder' => 'Lozinka za Basic Auth (opcionalno)', - 'ping' => 'Ping', - 'ping_details' => 'Detalji pinga', - 'ping_high' => 'Visoki ping', - 'ping_jitter' => 'Ping jitter', - 'ping_low' => 'Niski ping', - 'ping_ms' => 'Ping (ms)', - 'platform' => 'Platforma', - 'pushover' => 'Pushover', - 'pushover_webhooks' => 'Pushover webhookovi', - 'read_results' => 'Čitanje rezultata', - 'read_results_description' => 'Token dobiva ovlaštenje za čitanje rezultata i statistika.', - 'recipients' => 'Primatelji', - 'results' => 'Rezultati', - 'result_overview' => 'Pregled rezultata', - 'role' => 'Uloga', - 'row' => '{1} :count red|[2,*] :count redova', - 'run_speedtest' => 'Pokreni speedtest', - 'run_speedtest_description' => 'Token dobiva ovlaštenje za pokretanje speedtesta.', - 'running' => 'Pokreće se', - 'scheduled' => 'Zakazano', - 'sending_test_data_to_influxdb' => 'Slanje testnih podataka u InfluxDB', - 'server_&_metadata' => 'Server & metapodaci', - 'server_host' => 'Host servera', - 'server_id' => 'ID servera', - 'server_location' => 'Lokacija servera', - 'server_name' => 'Ime servera', - 'service' => 'Usluga', - 'settings' => 'Postavke', - 'Skipped' => 'Preskočeno', - 'slack' => 'Slack', - 'slack_enabled' => 'Omogući Slack webhook obavijesti', - 'slower' => 'Sporije', - 'speedtest_tracker' => 'speedtest-tracker', - 'Started' => 'Pokrenuto', - 'starting_bulk_data_write_to_influxdb' => 'Početak masovnog unosa podataka u InfluxDB', - 'status' => 'Status', - 'status_fix' => [ - 'confirm' => 'Želite li nastaviti?', - 'fail' => 'Naredba prekinuta.', - 'finished' => '✅ završeno!', - 'info_1' => 'Provjerava sve rezultate i popravlja status na „završeno” ili „neuspjelo” na osnovu podataka.', - 'info_2' => '📖 Pogledajte dokumentaciju: https://docs.speedtest-tracker.dev/other/commands', - ], - 'telegram' => 'Telegram', - 'telegram_chat_id' => 'Telegram chat ID', - 'telegram_disable_notification' => 'Šalji poruku tiho', - 'test_connection' => 'Testiraj vezu', - 'test_database_channel' => 'Testiraj kanal baze podataka', - 'test_discord_webhook' => 'Testiraj Discord webhook', - 'test_gotify_webhook' => 'Testiraj Gotify webhook', - 'test_healthcheck_webhook' => 'Testiraj healthcheck.io webhook', - 'test_mail_channel' => 'Testiraj email kanal', - 'test_ntfy_webhook' => 'Testiraj ntfy webhook', - 'test_pushover_webhook' => 'Testiraj Pushover webhook', - 'test_slack_webhook' => 'Testiraj Slack webhook', - 'test_telegram_channel' => 'Testiraj Telegram kanal', - 'test_webhook_channel' => 'Testiraj webhook kanal', - 'threshold_helper_text' => 'Obavijesti praga bit će poslane na /fail putanju u URL-u.', - 'thresholds' => 'Pragovi', - 'token' => 'Token', - 'token_created' => 'Token kreiran', - 'token_status' => 'Status tokena', - 'topic' => 'Tema', - 'triggers' => 'Okidači', - 'truncate' => 'Obriši', - 'truncate_results' => 'Obriši rezultate', - 'truncate_results_description' => 'Jeste li sigurni da želite obrisati sve rezultate? Ovo se ne može poništiti.', - 'update_comments' => 'Ažuriraj komentare', - 'updated_at' => 'Ažurirano', - 'update_available' => 'Dostupno ažuriranje!', - 'upload' => 'Slanje', - 'upload_latency' => 'Kašnjenje slanja', - 'upload_latency_high' => 'Visoko kašnjenje slanja', - 'upload_latency_jitter' => 'Jitter slanja', - 'upload_ms' => 'Slanje (ms)', - 'up_to_date' => 'Ažurirano', - 'url' => 'URL', - 'users' => 'Korisnici', - 'user_change' => [ - 'info' => 'Uloga korisnika ažurirana.', - 'password_updated_info' => 'Lozinka za :email ažurirana.', - 'what_is_password' => 'Koja je nova lozinka?', - 'what_is_the_email_address' => 'Koja je email adresa?', - 'what_role' => 'Koja uloga treba biti korisniku?', - ], - 'user_key' => 'Korisnički ključ', - 'username' => 'Korisničko ime', - 'username_placeholder' => 'Korisničko ime za Basic Auth (opcionalno)', - 'verify_ssl' => 'Provjeri SSL', - 'view_on_speedtest_net' => 'Pogledaj na Speedtest.net', - 'Waiting' => 'Čekanje', - 'webhook' => 'Webhook', - 'webhooks' => 'Webhookovi', - 'yes' => 'Da', - 'your_ntfy_server_url' => 'URL vašeg ntfy servera', - 'your_ntfy_topic' => 'Vaša ntfy tema', - 'your_pushover_api_token' => 'Vaš Pushover API token', - 'your_pushover_user_key' => 'Vaš Pushover korisnički ključ', - 'your_token' => 'Vaš token', -]; diff --git a/lang/hr_HR/validation.php b/lang/hr_HR/validation.php deleted file mode 100644 index 7d6b7345b..000000000 --- a/lang/hr_HR/validation.php +++ /dev/null @@ -1,230 +0,0 @@ - 'Polje :attribute mora biti prihvaćeno.', - 'accepted_if' => 'Polje :attribute mora biti prihvaćeno kada je :other jednako :value.', - 'active_url' => 'Polje :attribute nije valjani URL.', - 'after' => 'Polje :attribute mora biti datum nakon :date.', - 'after_or_equal' => 'Polje :attribute mora biti datum jednak ili nakon :date.', - 'alpha' => 'Polje :attribute može sadržavati samo slova.', - 'alpha_dash' => 'Polje :attribute može sadržavati samo slova, brojeve, crtice i donje crte.', - 'alpha_num' => 'Polje :attribute može sadržavati samo slova i brojeve.', - 'array' => 'Polje :attribute mora biti niz.', - 'ascii' => 'Polje :attribute može sadržavati samo ASCII znakove.', - 'before' => 'Polje :attribute mora biti datum prije :date.', - 'before_or_equal' => 'Polje :attribute mora biti datum jednak ili prije :date.', - 'between' => [ - 'array' => 'Polje :attribute mora imati između :min i :max stavki.', - 'file' => 'Polje :attribute mora biti između :min i :max kilobajta.', - 'numeric' => 'Polje :attribute mora biti između :min i :max.', - 'string' => 'Polje :attribute mora imati između :min i :max znakova.', - ], - 'boolean' => 'Polje :attribute mora biti istina ili laž.', - 'can' => 'Polje :attribute sadrži nevažeću vrijednost.', - 'confirmed' => 'Potvrda polja :attribute se ne podudara.', - 'current_password' => 'Unesena lozinka nije točna.', - 'date' => 'Polje :attribute nije valjani datum.', - 'date_equals' => 'Polje :attribute mora biti datum jednak :date.', - 'date_format' => 'Polje :attribute ne odgovara formatu :format.', - 'decimal' => 'Polje :attribute mora imati :decimal decimalnih mjesta.', - 'declined' => 'Polje :attribute mora biti odbijeno.', - 'declined_if' => 'Polje :attribute mora biti odbijeno kada je :other jednako ":value".', - 'different' => 'Polja :attribute i :other moraju biti različita.', - 'digits' => 'Polje :attribute mora imati :digits znamenki.', - 'digits_between' => 'Polje :attribute mora imati između :min i :max znamenki.', - 'dimensions' => 'Polje :attribute ima nevažeće dimenzije slike.', - 'distinct' => 'Polje :attribute ima dupliciranu vrijednost.', - 'doesnt_end_with' => 'Polje :attribute ne smije završavati sa: :values.', - 'doesnt_start_with' => 'Polje :attribute ne smije počinjati sa: :values.', - 'email' => 'Polje :attribute mora biti valjana email adresa.', - 'ends_with' => 'Polje :attribute mora završavati s jednom od sljedećih vrijednosti: :values.', - 'enum' => 'Odabrano polje :attribute nije važeće.', - 'exists' => 'Odabrano polje :attribute već postoji.', - 'file' => 'Polje :attribute mora biti datoteka.', - 'filled' => 'Polje :attribute mora imati vrijednost.', - 'gt' => [ - 'array' => 'Polje :attribute mora imati više od :value stavki.', - 'file' => 'Polje :attribute mora biti veće od :value kilobajta.', - 'numeric' => 'Polje :attribute mora biti veće od :value.', - 'string' => 'Polje :attribute mora biti dulje od :value znakova.', - ], - 'gte' => [ - 'array' => 'Polje :attribute mora imati najmanje :value stavki.', - 'file' => 'Polje :attribute mora biti najmanje :value kilobajta.', - 'numeric' => 'Polje :attribute mora biti najmanje :value.', - 'string' => 'Polje :attribute mora imati najmanje :value znakova.', - ], - 'image' => 'Polje :attribute mora biti slika.', - 'in' => 'Odabrano polje :attribute nije važeće.', - 'in_array' => 'Polje :attribute ne postoji u :other.', - 'integer' => 'Polje :attribute mora biti cijeli broj.', - 'ip' => 'Polje :attribute mora biti valjana IP adresa.', - 'ipv4' => 'Polje :attribute mora biti valjana IPv4 adresa.', - 'ipv6' => 'Polje :attribute mora biti valjana IPv6 adresa.', - 'json' => 'Polje :attribute mora biti valjani JSON.', - 'lowercase' => 'Polje :attribute može sadržavati samo mala slova.', - 'lt' => [ - 'array' => 'Polje :attribute mora imati manje od :value stavki.', - 'file' => 'Polje :attribute mora biti manje od :value kilobajta.', - 'numeric' => 'Polje :attribute mora biti manje od :value.', - 'string' => 'Polje :attribute mora biti kraće od :value znakova.', - ], - 'lte' => [ - 'array' => 'Polje :attribute ne smije imati više od :value stavki.', - 'file' => 'Polje :attribute ne smije biti veće od :value kilobajta.', - 'numeric' => 'Polje :attribute ne smije biti veće od :value.', - 'string' => 'Polje :attribute ne smije biti duže od :value znakova.', - ], - 'mac_address' => 'Polje :attribute mora biti valjana MAC adresa.', - 'max' => [ - 'array' => 'Polje :attribute ne smije imati više od :max stavki.', - 'file' => 'Polje :attribute ne smije biti veće od :max kilobajta.', - 'numeric' => 'Polje :attribute ne smije biti veće od :max.', - 'string' => 'Polje :attribute ne smije biti duže od :max znakova.', - ], - 'max_digits' => 'Polje :attribute ne smije imati više od :max znamenki.', - 'mimes' => 'Polje :attribute mora biti datoteka tipa: :values.', - 'mimetypes' => 'Polje :attribute mora biti datoteka tipa: :values.', - 'min' => [ - 'array' => 'Polje :attribute mora imati najmanje :min stavki.', - 'file' => 'Polje :attribute mora biti najmanje :min kilobajta.', - 'numeric' => 'Polje :attribute mora biti najmanje :min.', - 'string' => 'Polje :attribute mora imati najmanje :min znakova.', - ], - 'min_digits' => 'Polje :attribute mora imati najmanje :min znamenki.', - 'missing' => 'Polje :attribute mora biti odsutno.', - 'missing_if' => 'Polje :attribute mora biti odsutno kada je :other jednako ":value".', - 'missing_unless' => 'Polje :attribute mora biti odsutno osim ako je :other jednako ":value".', - 'missing_with' => 'Polje :attribute mora biti odsutno kada je prisutno :values.', - 'missing_with_all' => 'Polje :attribute mora biti odsutno kada su prisutna sva polja :values.', - 'multiple_of' => 'Polje :attribute mora biti višekratnik od :value.', - 'not_in' => 'Odabrano polje :attribute nije važeće.', - 'not_regex' => 'Format polja :attribute nije valjan.', - 'numeric' => 'Polje :attribute mora biti broj.', - 'password' => [ - 'letters' => 'Polje :attribute mora sadržavati barem jedno slovo.', - 'mixed' => 'Polje :attribute mora sadržavati barem jedno veliko i jedno malo slovo.', - 'numbers' => 'Polje :attribute mora sadržavati barem jedan broj.', - 'symbols' => 'Polje :attribute mora sadržavati barem jedan simbol.', - 'uncompromised' => 'Polje :attribute je kompromitirano u curenju podataka. Molimo odaberite drugo :attribute.', - ], - 'present' => 'Polje :attribute mora biti prisutno.', - 'prohibited' => 'Polje :attribute je zabranjeno.', - 'prohibited_if' => 'Polje :attribute je zabranjeno kada je :other jednako ":value".', - 'prohibited_unless' => 'Polje :attribute je zabranjeno osim ako je :other jednako ":values".', - 'prohibits' => 'Polje :attribute zabranjuje postojanje polja :other.', - 'regex' => 'Format polja :attribute nije valjan.', - 'required' => 'Polje :attribute je obavezno.', - 'required_array_keys' => 'Polje :attribute mora sadržavati ključeve: :values.', - 'required_if' => 'Polje :attribute je obavezno kada je :other jednako ":value".', - 'required_if_accepted' => 'Polje :attribute je obavezno kada je :other prihvaćeno.', - 'required_unless' => 'Polje :attribute je obavezno osim ako je :other jednako ":values".', - 'required_with' => 'Polje :attribute je obavezno kada je prisutno :values.', - 'required_with_all' => 'Polje :attribute je obavezno kada su prisutna sva polja :values.', - 'required_without' => 'Polje :attribute je obavezno kada nije prisutno :values.', - 'required_without_all' => 'Polje :attribute je obavezno kada nijedno od polja :values nije prisutno.', - 'same' => 'Polja :attribute i :other se moraju podudarati.', - 'size' => [ - 'array' => 'Polje :attribute mora sadržavati točno :size stavki.', - 'file' => 'Polje :attribute mora biti veličine :size kilobajta.', - 'numeric' => 'Polje :attribute mora biti :size.', - 'string' => 'Polje :attribute mora imati :size znakova.', - ], - 'starts_with' => 'Polje :attribute mora početi s jednom od sljedećih vrijednosti: :values.', - 'string' => 'Polje :attribute mora biti tekst.', - 'timezone' => 'Polje :attribute mora biti važeća vremenska zona.', - 'unique' => 'Polje :attribute već postoji.', - 'uploaded' => 'Učitavanje polja :attribute nije uspjelo.', - 'uppercase' => 'Polje :attribute može sadržavati samo velika slova.', - 'url' => 'Polje :attribute mora biti valjani URL.', - 'ulid' => 'Polje :attribute mora biti valjani ULID.', - 'uuid' => 'Polje :attribute mora biti valjani UUID.', - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => [ - 'attribute-name' => [ - 'rule-name' => 'custom-message', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap our attribute placeholder - | with something more reader friendly such as "E-Mail Address" instead - | of "email". This simply helps us make our message more expressive. - | - */ - - 'attributes' => [ - 'address' => 'Adresa', - 'age' => 'Dob', - 'body' => 'Sadržaj', - 'cell' => 'Mobitel', - 'city' => 'Grad', - 'country' => 'Država', - 'date' => 'Datum', - 'day' => 'Dan', - 'excerpt' => 'Izvadak', - 'first_name' => 'Ime', - 'gender' => 'Spol', - 'marital_status' => 'Bračni status', - 'profession' => 'Zanimanje', - 'nationality' => 'Nacionalnost', - 'hour' => 'Sat', - 'last_name' => 'Prezime', - 'message' => 'Poruka', - 'minute' => 'Minuta', - 'mobile' => 'Broj mobitela', - 'month' => 'Mjesec', - 'name' => 'Ime', - 'zipcode' => 'Poštanski broj', - 'company_name' => 'Naziv tvrtke', - 'neighborhood' => 'Kvart', - 'number' => 'Broj', - 'password' => 'Lozinka', - 'phone' => 'Telefon', - 'second' => 'Sekunda', - 'sex' => 'Spol', - 'state' => 'Županija / Pokrajina', - 'street' => 'Ulica', - 'subject' => 'Predmet', - 'text' => 'Tekst', - 'time' => 'Vrijeme', - 'title' => 'Naslov', - 'username' => 'Korisničko ime', - 'year' => 'Godina', - 'description' => 'Opis', - 'password_confirmation' => 'Potvrda lozinke', - 'current_password' => 'Trenutna lozinka', - 'complement' => 'Dodatak', - 'modality' => 'Mod', - 'category' => 'Kategorija', - 'blood_type' => 'Krvna grupa', - 'birth_date' => 'Datum rođenja', - ], -]; diff --git a/lang/hu_HU/auth.php b/lang/hu_HU/auth.php deleted file mode 100644 index adad2012c..000000000 --- a/lang/hu_HU/auth.php +++ /dev/null @@ -1,20 +0,0 @@ - 'A megadott hitelesítési adatok nem egyeznek.', - 'password' => 'A megadott jelszó hibás.', - 'throttle' => 'Túl sok bejelentkezési kísérlet. Kérlek, várj :seconds másodpercet, mielőtt újra próbálkozol.', - -]; diff --git a/lang/hu_HU/pagination.php b/lang/hu_HU/pagination.php deleted file mode 100644 index 7c5a6a894..000000000 --- a/lang/hu_HU/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Előző', - 'next' => 'Következő »', - -]; diff --git a/lang/hu_HU/passwords.php b/lang/hu_HU/passwords.php deleted file mode 100644 index 379c8f954..000000000 --- a/lang/hu_HU/passwords.php +++ /dev/null @@ -1,22 +0,0 @@ - 'A jelszavadat sikeresen visszaállítottuk!', - 'sent' => 'Elküldtük e-mailben a jelszó-visszaállítási linket!', - 'throttled' => 'Kérlek, várj egy kicsit, mielőtt újra próbálkozol.', - 'token' => 'A jelszó-visszaállító hivatkozás érvénytelen vagy lejárt.', - 'user' => 'Ehhez az e-mail címhez nem található felhasználó.', - -]; diff --git a/lang/hu_HU/translations.php b/lang/hu_HU/translations.php deleted file mode 100644 index 414bcecd8..000000000 --- a/lang/hu_HU/translations.php +++ /dev/null @@ -1,289 +0,0 @@ - 'Admin', - 'User' => 'Felhasználó', - 'abilities' => 'Képességek', - 'active_tokens' => 'Aktív tokenek', - 'all_tokens' => 'Összes token', - 'api_token' => 'API token', - 'api_tokens' => 'API tokenek', - 'average' => 'Átlag', - 'average_ms' => 'Átlag (ms)', - 'Benchmarking' => 'Teljesítménymérés', - 'bucket' => 'Vödör', - 'Checking' => 'Ellenőrzés', - 'comment' => 'Megjegyzés', - 'comments' => 'Megjegyzések', - 'Completed' => 'Befejezve', - 'create_api_token' => 'API token létrehozása', - 'created_at' => 'Létrehozva', - 'created_from' => 'Létrehozva ettől', - 'created_until' => 'Létrehozva eddig', - 'cron_invalid' => 'Érvénytelen cron kifejezés', - 'data_integration' => 'Adatintegráció', - 'database' => 'Adatbázis', - 'database_description' => 'Az erre a csatornára küldött értesítések a fejlécben lévő 🔔 ikon alatt jelennek meg.', - 'details' => 'Részletek', - 'dashboard' => 'Irányítópult', - 'discord' => 'Discord', - 'documentation' => 'Dokumentáció', - 'donate' => 'Adományozás', - 'download' => 'Letöltés', - 'download_latency' => 'Letöltési késleltetés', - 'download_latency_high' => 'Magas letöltési késleltetés', - 'download_latency_iqm' => 'Letöltési késleltetés IQM', - 'download_latency_jitter' => 'Letöltési késleltetés jitter', - 'download_latency_low' => 'Alacsony letöltési késleltetés', - 'download_mbps' => 'Letöltés (Mbps)', - 'download_ms' => 'Letöltés (ms)', - 'email' => 'Email', - 'email_address' => 'Email cím', - 'enable' => 'Engedélyezés', - 'enable_database_notifications' => 'Adatbázis értesítések engedélyezése', - 'enable_discord_webhook_notifications' => 'Discord webhook értesítések engedélyezése', - 'enable_mail_notifications' => 'Email értesítések engedélyezése', - 'enable_pushover_webhook_notifications' => 'Pushover webhook értesítések engedélyezése', - 'enable_telegram' => 'Telegram értesítések engedélyezése', - 'enable_webhook_notifications' => 'Webhook értesítések engedélyezése', - 'error_message' => 'Hibaüzenet', - 'expired_tokens' => 'Lejárt tokenek', - 'expires_at' => 'Lejárat dátuma', - 'expires_at_helper_text' => 'Hagyja üresen, ha nem akar lejárati dátumot', - 'export_all_results' => 'Összes eredmény exportálása', - 'export_all_results_description' => 'Minden oszlopot exportálni fog az összes eredményhez.', - 'export_completed' => 'Az exportálás befejeződött, :count :rows exportálva.', - 'export_current_results' => 'Jelenlegi eredmények exportálása', - 'Failed' => 'Sikertelen', - 'failed_export' => ':count :rows nem sikerült exportálni.', - 'Faker' => 'faker', - 'faster' => 'Gyorsabb', - 'general_settings' => [ - 'label' => 'Általános beállítások', - 'description' => 'Itt állíthatók be az alkalmazás általános beállításai.', - 'app_settings' => 'Alkalmazás beállítások', - 'speedtest_settings' => 'Sebességteszt beállítások', - 'api_settings' => 'Api beállítások', - 'app_name' => 'Alkalmazás neve', - 'asset_url' => 'Eszköz URL', - 'app_timezone' => 'Alkalmazás időzónája', - 'chart_begin_at_zero' => 'Diagram kezdése nullánál', - 'chart_datetime_format' => 'Diagram dátum-idő formátum', - 'datetime_format' => 'Dátum-idő formátum', - 'display_timezone' => 'Megjelenített időzóna', - 'public_dashboard' => 'Nyilvános irányítópult', - 'speedtest_skip_ips' => 'Speedtest kihagyott IP-k', - 'speedtest_schedule' => 'Speedtest ütemezés', - 'speedtest_schedule_description' => 'Adj meg érvényes cron kifejezéseket. Példa: * * * * * minden percben lefut.', - 'speedtest_servers' => 'Speedtest szerverek', - 'speedtest_blocked_servers' => 'Speedtest blokkolt szerverek', - 'speedtest_interface' => 'Speedtest interfész', - 'speedtest_checkinternet_url' => 'Speedtest internet ellenőrző URL', - 'threshold_enabled' => 'Küszöbérték engedélyezve', - 'threshold_download' => 'Küszöbérték letöltés', - 'threshold_upload' => 'Küszöbérték feltöltés', - 'threshold_ping' => 'Küszöbérték ping', - 'prune_results_older_than' => 'Eredmények törlése, ha régebbi mint', - 'api_rate_limit' => 'API sebességkorlát', - ], - 'gotify' => 'Gotify', - 'gotify_enabled' => 'Gotify webhook értesítések engedélyezése', - 'healthcheck_enabled' => 'healthcheck.io webhook értesítések engedélyezése', - 'healthcheck_io' => 'Healthcheck.io', - 'healthy' => 'Egészséges', - 'high' => 'Magas', - 'high_ms' => 'Magas (ms)', - 'id' => 'Azonosító', - 'infoluxdb' => 'InfluxDB v2', - 'infoluxdb_description' => 'Ha engedélyezve van, az új Speedtest eredmények el lesznek küldve az InfluxDB-be.', - 'ip_address' => 'IP cím', - 'iqm' => 'IQM', - 'isp' => 'Szolgáltató', - 'jitter' => 'Jitter', - 'last_24h' => 'Elmúlt 24 óra', - 'last_month' => 'Elmúlt hónap', - 'last_used_at' => 'Utoljára használva', - 'last_week' => 'Elmúlt hét', - 'latest_download' => 'Legutóbbi letöltés', - 'latest_ping' => 'Legutóbbi ping', - 'latest_upload' => 'Legutóbbi feltöltés', - 'links' => 'Hivatkozások', - 'list_servers' => 'Szerverek listázása', - 'list_servers_description' => 'A token jogosultságot kap a szerverek listázására.', - 'low' => 'Alacsony', - 'low_ms' => 'Alacsony (ms)', - 'mail' => 'E-mail', - 'message' => 'Üzenet', - 'ms' => 'ms', - 'name' => 'Név', - 'next_speedtest_at' => 'Következő speedtest: ', - 'no' => 'Nem', - 'no_speedtests_scheduled' => 'Nincs ütemezett speedtest.', - 'notifications' => [ - 'label' => 'Értesítések', - 'database' => [ - 'ping' => 'Azt mondom: ping', - 'pong' => 'Te mondod: pong', - 'received' => 'Teszt adatbázis értesítés megérkezett!', - 'sent' => 'Teszt adatbázis értesítés elküldve.', - ], - 'discord' => [ - 'add' => 'Adj hozzá Discord URL-eket!', - 'sent' => 'Teszt Discord értesítés elküldve.', - 'payload' => '👋 Teszteljük a Discord értesítési csatornát.', - ], - 'health_check' => [ - 'add' => 'Adj hozzá HealthCheck.io URL-eket!', - 'sent' => 'Teszt HealthCheck.io értesítés elküldve.', - 'payload' => '👋 Teszteljük a HealthCheck.io értesítési csatornát.', - ], - 'gotfy' => [ - 'add' => 'Adj hozzá Gotify URL-eket!', - 'sent' => 'Teszt Gotify értesítés elküldve.', - 'payload' => '👋 Teszteljük a Gotify értesítési csatornát.', - ], - 'mail' => [ - 'add' => 'Adj hozzá email címzetteket!', - 'sent' => 'Teszt email értesítés elküldve.', - ], - 'ntfy' => [ - 'add' => 'Adj hozzá ntfy URL-eket!', - 'sent' => 'Teszt ntfy értesítés elküldve.', - 'payload' => '👋 Teszteljük az ntfy értesítési csatornát.', - ], - 'pushover' => [ - 'add' => 'Adj hozzá Pushover URL-eket!', - 'sent' => 'Teszt Pushover értesítés elküldve.', - 'payload' => '👋 Teszteljük a Pushover értesítési csatornát.', - ], - 'slack' => [ - 'add' => 'Adj hozzá Slack URL-eket!', - 'sent' => 'Teszt Slack értesítés elküldve.', - 'payload' => '👋 Teszteljük a Slack értesítési csatornát.', - ], - 'telegram' => [ - 'add' => 'Adj hozzá Telegram címzetteket!', - 'sent' => 'Teszt Telegram értesítés elküldve.', - ], - 'webhook' => [ - 'add' => 'Adj hozzá webhook URL-eket!', - 'sent' => 'Teszt webhook értesítés elküldve.', - 'payload' => 'Webhook értesítés tesztelése', - ], - ], - 'notify_on_every_speedtest_run' => 'Értesítés minden speedtest után', - 'notify_on_threshold_failures' => 'Értesítés küszöbérték hibáknál', - 'ntfy' => 'ntfy', - 'ntfy_enabled' => 'ntfy webhook értesítések engedélyezése', - 'only_healthy_speedtests' => 'Csak egészséges speedtestek', - 'only_manual_speedtests' => 'Csak manuális speedtestek', - 'only_scheduled_speedtests' => 'Csak ütemezett speedtestek', - 'only_unhealthy_speedtests' => 'Csak hibás speedtestek', - 'Ookla' => 'Ookla', - 'ookla_error' => 'Hiba történt a speedtest szerverek listázása közben, nézd meg a naplókat.', - 'options' => 'Beállítások', - 'org' => 'Szervezet', - 'packet_loss' => 'Csomagvesztés', - 'password' => 'Jelszó', - 'password_confirmation' => 'Jelszó megerősítése', - 'password_placeholder' => 'Jelszó a Basic Auth-hoz (opcionális)', - 'ping' => 'Ping', - 'ping_details' => 'Ping részletek', - 'ping_high' => 'Magas ping', - 'ping_jitter' => 'Ping jitter', - 'ping_low' => 'Alacsony ping', - 'ping_ms' => 'Ping (ms)', - 'platform' => 'Platform', - 'pushover' => 'Pushover', - 'pushover_webhooks' => 'Pushover webhookok', - 'read_results' => 'Eredmények olvasása', - 'read_results_description' => 'A token jogosultságot kap eredmények és statisztikák olvasására.', - 'recipients' => 'Címzettek', - 'results' => 'Eredmények', - 'result_overview' => 'Eredmény áttekintés', - 'role' => 'Szerepkör', - 'row' => '{1} :count sor|[2,*] :count sor', - 'run_speedtest' => 'Speedtest futtatása', - 'run_speedtest_description' => 'A token jogosultságot kap speedtest futtatására.', - 'Running' => 'Fut', - 'scheduled' => 'Ütemezve', - 'sending_test_data_to_influxdb' => 'Tesztadatok küldése az InfluxDB-be', - 'server_&_metadata' => 'Szerver & Metaadatok', - 'server_host' => 'Szerver hoszt', - 'server_id' => 'Szerver azonosító', - 'server_location' => 'Szerver helyszín', - 'server_name' => 'Szerver neve', - 'service' => 'Szolgáltatás', - 'settings' => 'Beállítások', - 'Skipped' => 'Kihagyva', - 'slack' => 'Slack', - 'slack_enabled' => 'Slack webhook értesítések engedélyezése', - 'slower' => 'Lassabb', - 'speedtest_tracker' => 'speedtest-tracker', - 'Started' => 'Elindult', - 'starting_bulk_data_write_to_influxdb' => 'Tömeges adatok írásának indítása az InfluxDB-be', - 'status' => 'Állapot', - 'status_fix' => [ - 'confirm' => 'Folytatod?', - 'fail' => 'Parancs megszakítva.', - 'finished' => '✅ kész!', - 'info_1' => 'Minden eredményt ellenőriz és a státuszt kijavítja „befejezett” vagy „sikertelen” értékre az adatok alapján.', - 'info_2' => '📖 Olvasd el a dokumentációt: https://docs.speedtest-tracker.dev/other/commands', - ], - 'telegram' => 'Telegram', - 'telegram_chat_id' => 'Telegram chat ID', - 'telegram_disable_notification' => 'Üzenet csendes küldése', - 'test_connection' => 'Kapcsolat tesztelése', - 'test_database_channel' => 'Adatbázis csatorna tesztelése', - 'test_discord_webhook' => 'Discord webhook tesztelése', - 'test_gotify_webhook' => 'Gotify webhook tesztelése', - 'test_healthcheck_webhook' => 'healthcheck.io webhook tesztelése', - 'test_mail_channel' => 'Email csatorna tesztelése', - 'test_ntfy_webhook' => 'ntfy webhook tesztelése', - 'test_pushover_webhook' => 'Pushover webhook tesztelése', - 'test_slack_webhook' => 'Slack webhook tesztelése', - 'test_telegram_channel' => 'Telegram csatorna tesztelése', - 'test_webhook_channel' => 'Webhook csatorna tesztelése', - 'threshold_helper_text' => 'A küszöbértesítések a /fail útvonalra lesznek küldve az URL-ben.', - 'thresholds' => 'Küszöbértékek', - 'token' => 'Token', - 'token_created' => 'Token létrehozva', - 'token_status' => 'Token állapot', - 'topic' => 'Téma', - 'triggers' => 'Triggerek', - 'truncate' => 'Törlés', - 'truncate_results' => 'Eredmények törlése', - 'truncate_results_description' => 'Biztosan törölni szeretnéd az összes eredményt? Ez nem visszavonható.', - 'update_comments' => 'Megjegyzések frissítése', - 'updated_at' => 'Frissítve', - 'update_available' => 'Frissítés elérhető!', - 'upload' => 'Feltöltés', - 'upload_latency' => 'Feltöltési késleltetés', - 'upload_latency_high' => 'Magas feltöltési késleltetés', - 'upload_latency_jitter' => 'Feltöltési jitter', - 'upload_ms' => 'Feltöltés (ms)', - 'up_to_date' => 'Naprakész', - 'url' => 'URL', - 'users' => 'Felhasználók', - 'user_change' => [ - 'info' => 'Felhasználó szerepköre frissítve.', - 'password_updated_info' => ':email jelszava frissítve.', - 'what_is_password' => 'Mi az új jelszó?', - 'what_is_the_email_address' => 'Mi az email cím?', - 'what_role' => 'Milyen szerepköre legyen a felhasználónak?', - ], - 'user_key' => 'Felhasználói kulcs', - 'username' => 'Felhasználónév', - 'username_placeholder' => 'Felhasználónév a Basic Auth-hoz (opcionális)', - 'verify_ssl' => 'SSL ellenőrzése', - 'view_on_speedtest_net' => 'Megtekintés a Speedtest.net-en', - 'Waiting' => 'Várakozás', - 'webhook' => 'Webhook', - 'webhooks' => 'Webhookok', - 'yes' => 'Igen', - 'your_ntfy_server_url' => 'Az ntfy szervered URL-je', - 'your_ntfy_topic' => 'Az ntfy témád', - 'your_pushover_api_token' => 'A Pushover API tokened', - 'your_pushover_user_key' => 'A Pushover felhasználói kulcsod', - 'your_token' => 'A tokened', -]; diff --git a/lang/hu_HU/validation.php b/lang/hu_HU/validation.php deleted file mode 100644 index bc3f12f25..000000000 --- a/lang/hu_HU/validation.php +++ /dev/null @@ -1,230 +0,0 @@ - 'A(z) :attribute el kell legyen fogadva.', - 'accepted_if' => 'A(z) :attribute el kell legyen fogadva, ha :other értéke :value.', - 'active_url' => 'A(z) :attribute nem érvényes URL.', - 'after' => 'A(z) :attribute dátumának későbbinek kell lennie, mint :date.', - 'after_or_equal' => 'A(z) :attribute dátumának legalább :date-nek kell lennie.', - 'alpha' => 'A(z) :attribute csak betűket tartalmazhat.', - 'alpha_dash' => 'A(z) :attribute csak betűket, számokat, kötőjeleket és aláhúzásjeleket tartalmazhat.', - 'alpha_num' => 'A(z) :attribute csak betűket és számokat tartalmazhat.', - 'array' => 'A(z) :attribute egy tömbnek kell lennie.', - 'ascii' => 'A(z) :attribute csak ASCII karaktereket tartalmazhat.', - 'before' => 'A(z) :attribute dátumának korábbinak kell lennie, mint :date.', - 'before_or_equal' => 'A(z) :attribute dátumának legfeljebb :date-nek kell lennie.', - 'between' => [ - 'array' => 'A(z) :attribute :min és :max elem között kell legyen.', - 'file' => 'A(z) :attribute mérete :min és :max kilobájt között kell legyen.', - 'numeric' => 'A(z) :attribute értékének :min és :max között kell lennie.', - 'string' => 'A(z) :attribute :min és :max karakter között kell legyen.', - ], - 'boolean' => 'A(z) :attribute értéke igaz vagy hamis lehet.', - 'can' => 'A(z) :attribute érvénytelen értéket tartalmaz.', - 'confirmed' => 'A(z) :attribute megerősítése nem egyezik.', - 'current_password' => 'A megadott jelszó helytelen.', - 'date' => 'A(z) :attribute nem érvényes dátum.', - 'date_equals' => 'A(z) :attribute pontosan :date kell legyen.', - 'date_format' => 'A(z) :attribute nem felel meg a formátumnak: :format.', - 'decimal' => 'A(z) :attribute :decimal tizedesjegyet kell tartalmazzon.', - 'declined' => 'A(z) :attribute el kell legyen utasítva.', - 'declined_if' => 'A(z) :attribute el kell legyen utasítva, ha :other értéke ":value".', - 'different' => 'A(z) :attribute és :other nem lehet azonos.', - 'digits' => 'A(z) :attribute :digits számjegyből kell álljon.', - 'digits_between' => 'A(z) :attribute :min és :max számjegy közötti érték kell legyen.', - 'dimensions' => 'A(z) :attribute érvénytelen képméretet tartalmaz.', - 'distinct' => 'A(z) :attribute mező ismétlődő értéket tartalmaz.', - 'doesnt_end_with' => 'A(z) :attribute nem végződhet a következőkkel: :values.', - 'doesnt_start_with' => 'A(z) :attribute nem kezdődhet a következőkkel: :values.', - 'email' => 'A(z) :attribute érvényes e-mail cím kell legyen.', - 'ends_with' => 'A(z) :attribute a következő értékek egyikével kell végződjön: :values.', - 'enum' => 'A kiválasztott :attribute érvénytelen.', - 'exists' => 'A kiválasztott :attribute már létezik.', - 'file' => 'A(z) :attribute fájlnak kell lennie.', - 'filled' => 'A(z) :attribute mező nem lehet üres.', - 'gt' => [ - 'array' => 'A(z) :attribute több mint :value elemet kell tartalmazzon.', - 'file' => 'A(z) :attribute mérete nagyobb kell legyen, mint :value kilobájt.', - 'numeric' => 'A(z) :attribute nagyobb kell legyen, mint :value.', - 'string' => 'A(z) :attribute hosszabb kell legyen, mint :value karakter.', - ], - 'gte' => [ - 'array' => 'A(z) :attribute legalább :value elemet kell tartalmazzon.', - 'file' => 'A(z) :attribute mérete legalább :value kilobájt kell legyen.', - 'numeric' => 'A(z) :attribute legalább :value kell legyen.', - 'string' => 'A(z) :attribute legalább :value karakter hosszú kell legyen.', - ], - 'image' => 'A(z) :attribute képnek kell lennie.', - 'in' => 'A(z) :attribute értéke érvénytelen.', - 'in_array' => 'A(z) :attribute nem található meg a(z) :other mezőben.', - 'integer' => 'A(z) :attribute egész szám kell legyen.', - 'ip' => 'A(z) :attribute érvényes IP-cím kell legyen.', - 'ipv4' => 'A(z) :attribute érvényes IPv4-cím kell legyen.', - 'ipv6' => 'A(z) :attribute érvényes IPv6-cím kell legyen.', - 'json' => 'A(z) :attribute érvényes JSON kell legyen.', - 'lowercase' => 'A(z) :attribute csak kisbetűket tartalmazhat.', - 'lt' => [ - 'array' => 'A(z) :attribute legfeljebb :value elemet tartalmazhat.', - 'file' => 'A(z) :attribute kisebb kell legyen, mint :value kilobájt.', - 'numeric' => 'A(z) :attribute kisebb kell legyen, mint :value.', - 'string' => 'A(z) :attribute rövidebb kell legyen, mint :value karakter.', - ], - 'lte' => [ - 'array' => 'A(z) :attribute nem tartalmazhat több, mint :value elemet.', - 'file' => 'A(z) :attribute nem lehet nagyobb, mint :value kilobájt.', - 'numeric' => 'A(z) :attribute nem lehet nagyobb, mint :value.', - 'string' => 'A(z) :attribute nem lehet hosszabb, mint :value karakter.', - ], - 'mac_address' => 'A(z) :attribute érvényes MAC-cím kell legyen.', - 'max' => [ - 'array' => 'A(z) :attribute legfeljebb :max elemet tartalmazhat.', - 'file' => 'A(z) :attribute legfeljebb :max kilobájt lehet.', - 'numeric' => 'A(z) :attribute nem lehet nagyobb, mint :max.', - 'string' => 'A(z) :attribute nem lehet hosszabb, mint :max karakter.', - ], - 'max_digits' => 'A(z) :attribute legfeljebb :max számjegyet tartalmazhat.', - 'mimes' => 'A(z) :attribute típusának a következők egyikének kell lennie: :values.', - 'mimetypes' => 'A(z) :attribute formátuma a következők egyike kell legyen: :values.', - 'min' => [ - 'array' => 'A(z) :attribute legalább :min elemet kell tartalmazzon.', - 'file' => 'A(z) :attribute legalább :min kilobájt kell legyen.', - 'numeric' => 'A(z) :attribute legalább :min kell legyen.', - 'string' => 'A(z) :attribute legalább :min karakter hosszú kell legyen.', - ], - 'min_digits' => 'A(z) :attribute legalább :min számjegyet kell tartalmazzon.', - 'missing' => 'A(z) :attribute nem szerepelhet.', - 'missing_if' => 'A(z) :attribute nem lehet megadva, ha :other értéke ":value".', - 'missing_unless' => 'A(z) :attribute nem lehet megadva, kivéve ha :other értéke ":value".', - 'missing_with' => 'A(z) :attribute nem szerepelhet, ha :values meg van adva.', - 'missing_with_all' => 'A(z) :attribute nem szerepelhet, ha a(z) :values mezők mind meg vannak adva.', - 'multiple_of' => 'A(z) :attribute a(z) :value többszöröse kell legyen.', - 'not_in' => 'A kiválasztott :attribute érvénytelen.', - 'not_regex' => 'A(z) :attribute formátuma érvénytelen.', - 'numeric' => 'A(z) :attribute szám kell legyen.', - 'password' => [ - 'letters' => 'A(z) :attribute tartalmazzon legalább egy betűt.', - 'mixed' => 'A(z) :attribute tartalmazzon legalább egy kis- és egy nagybetűt.', - 'numbers' => 'A(z) :attribute tartalmazzon legalább egy számot.', - 'symbols' => 'A(z) :attribute tartalmazzon legalább egy speciális karaktert.', - 'uncompromised' => 'A(z) :attribute egy adatszivárgásban érintett. Kérjük, válasszon másik :attribute-t.', - ], - 'present' => 'A(z) :attribute mezőnek jelen kell lennie.', - 'prohibited' => 'A(z) :attribute megadása nem engedélyezett.', - 'prohibited_if' => 'A(z) :attribute nem adható meg, ha :other értéke ":value".', - 'prohibited_unless' => 'A(z) :attribute csak akkor adható meg, ha :other értéke ":values".', - 'prohibits' => 'A(z) :attribute kizárja a(z) :other megadását.', - 'regex' => 'A(z) :attribute formátuma érvénytelen.', - 'required' => 'A(z) :attribute mező kötelező.', - 'required_array_keys' => 'A(z) :attribute mezőnek tartalmaznia kell a következő kulcsokat: :values.', - 'required_if' => 'A(z) :attribute kötelező, ha :other értéke ":value".', - 'required_if_accepted' => 'A(z) :attribute kötelező, ha :other el van fogadva.', - 'required_unless' => 'A(z) :attribute kötelező, kivéve, ha :other értéke ":values".', - 'required_with' => 'A(z) :attribute kötelező, ha :values meg van adva.', - 'required_with_all' => 'A(z) :attribute kötelező, ha minden :values mező ki van töltve.', - 'required_without' => 'A(z) :attribute kötelező, ha :values nincs megadva.', - 'required_without_all' => 'A(z) :attribute kötelező, ha egyik :values mező sincs megadva.', - 'same' => 'A(z) :attribute és :other mezőknek egyezniük kell.', - 'size' => [ - 'array' => 'A(z) :attribute pontosan :size elemet kell tartalmazzon.', - 'file' => 'A(z) :attribute mérete :size kilobájt kell legyen.', - 'numeric' => 'A(z) :attribute értéke pontosan :size kell legyen.', - 'string' => 'A(z) :attribute pontosan :size karakter hosszú kell legyen.', - ], - 'starts_with' => 'A(z) :attribute a következők egyikével kell kezdődjön: :values.', - 'string' => 'A(z) :attribute szöveg kell legyen.', - 'timezone' => 'A(z) :attribute érvényes időzóna kell legyen.', - 'unique' => 'A(z) :attribute már foglalt.', - 'uploaded' => 'A(z) :attribute feltöltése sikertelen volt.', - 'uppercase' => 'A(z) :attribute csak nagybetűket tartalmazhat.', - 'url' => 'A(z) :attribute érvényes URL kell legyen.', - 'ulid' => 'A(z) :attribute érvényes ULID kell legyen.', - 'uuid' => 'A(z) :attribute érvényes UUID kell legyen.', - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => [ - 'attribute-name' => [ - 'rule-name' => 'custom-message', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap our attribute placeholder - | with something more reader friendly such as "E-Mail Address" instead - | of "email". This simply helps us make our message more expressive. - | - */ - - 'attributes' => [ - 'address' => 'Cím', - 'age' => 'Életkor', - 'body' => 'Tartalom', - 'cell' => 'Mobil', - 'city' => 'Város', - 'country' => 'Ország', - 'date' => 'Dátum', - 'day' => 'Nap', - 'excerpt' => 'Kivonat', - 'first_name' => 'Keresztnév', - 'gender' => 'Nem', - 'marital_status' => 'Családi állapot', - 'profession' => 'Foglalkozás', - 'nationality' => 'Állampolgárság', - 'hour' => 'Óra', - 'last_name' => 'Vezetéknév', - 'message' => 'Üzenet', - 'minute' => 'Perc', - 'mobile' => 'Mobiltelefonszám', - 'month' => 'Hónap', - 'name' => 'Név', - 'zipcode' => 'Irányítószám', - 'company_name' => 'Cégnév', - 'neighborhood' => 'Környék', - 'number' => 'Szám', - 'password' => 'Jelszó', - 'phone' => 'Telefonszám', - 'second' => 'Másodperc', - 'sex' => 'Nem', - 'state' => 'Megye / Tartomány', - 'street' => 'Utca', - 'subject' => 'Tárgy', - 'text' => 'Szöveg', - 'time' => 'Idő', - 'title' => 'Cím', - 'username' => 'Felhasználónév', - 'year' => 'Év', - 'description' => 'Leírás', - 'password_confirmation' => 'Jelszó megerősítése', - 'current_password' => 'Jelenlegi jelszó', - 'complement' => 'Kiegészítés', - 'modality' => 'Mód', - 'category' => 'Kategória', - 'blood_type' => 'Vércsoport', - 'birth_date' => 'Születési dátum', - ], -]; diff --git a/lang/it_IT/auth.php b/lang/it_IT/auth.php deleted file mode 100644 index a70a9a63b..000000000 --- a/lang/it_IT/auth.php +++ /dev/null @@ -1,20 +0,0 @@ - 'Queste credenziali non corrispondono ai nostri archivi.', - 'password' => 'La password fornita non è corretta.', - 'throttle' => 'Troppi tentativi di accesso. Riprova tra :seconds secondi.', - -]; diff --git a/lang/it_IT/pagination.php b/lang/it_IT/pagination.php deleted file mode 100644 index 9d6a2e2bc..000000000 --- a/lang/it_IT/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Precedente', - 'next' => 'Successivo »', - -]; diff --git a/lang/it_IT/passwords.php b/lang/it_IT/passwords.php deleted file mode 100644 index 4289493c6..000000000 --- a/lang/it_IT/passwords.php +++ /dev/null @@ -1,23 +0,0 @@ - 'La tua password è stata reimpostata!', - 'sent' => 'Ti abbiamo inviato via email il link per reimpostare la password!', - 'password' => 'La password e la conferma devono corrispondere e contenere almeno sei caratteri.', - 'throttled' => 'Per favore attendi prima di riprovare.', - 'token' => 'Questo token di reimpostazione della password non è valido.', - 'user' => "Non riusciamo a trovare un utente con quell'indirizzo email.", - -]; diff --git a/lang/it_IT/validation.php b/lang/it_IT/validation.php deleted file mode 100644 index 3ee37cddf..000000000 --- a/lang/it_IT/validation.php +++ /dev/null @@ -1,230 +0,0 @@ - 'Il campo :attribute deve essere accettato.', - 'accepted_if' => 'Il campo :attribute deve essere accettato quando :other è :value.', - 'active_url' => 'Il campo :attribute deve essere un URL valido.', - 'after' => 'Il campo :attribute deve essere una data successiva a :date.', - 'after_or_equal' => 'Il campo :attribute deve essere una data successiva o uguale a :date.', - 'alpha' => 'Il campo :attribute deve contenere solo lettere.', - 'alpha_dash' => 'Il campo :attribute deve contenere solo lettere, numeri, trattini e trattini bassi.', - 'alpha_num' => 'Il campo :attribute deve contenere solo lettere e numeri.', - 'array' => 'Il campo :attribute deve essere un array.', - 'ascii' => 'Il campo :attribute deve contenere solo caratteri alfanumerici e simboli a un byte.', - 'before' => 'Il campo :attribute deve essere una data antecedente a :date.', - 'before_or_equal' => 'Il campo :attribute deve essere una data precedente o uguale a :date.', - 'between' => [ - 'array' => 'Il campo :attribute deve contenere tra :min e :max elementi.', - 'file' => 'Il campo :attribute deve essere compreso tra :min e :max kilobyte.', - 'numeric' => 'Il campo :attribute deve essere compreso tra :min e :max.', - 'string' => 'Il campo :attribute deve contenere tra :min e :max caratteri.', - ], - 'boolean' => 'Il campo :attribute deve essere vero o falso.', - 'can' => 'Il campo :attribute contiene un valore non autorizzato.', - 'confirmed' => 'La conferma del campo :attribute non corrisponde.', - 'current_password' => 'La password non è corretta.', - 'date' => 'Il campo :attribute deve essere una data valida.', - 'date_equals' => 'Il campo :attribute deve essere una data uguale a :date.', - 'date_format' => 'Il campo :attribute deve corrispondere al formato :format.', - 'decimal' => 'Il campo :attribute deve avere :decimal cifre decimali.', - 'declined' => 'Il campo :attribute deve essere rifiutato.', - 'declined_if' => 'Il campo :attribute deve essere rifiutato quando :other è :value.', - 'different' => 'Il campo :attribute e :other devono essere diversi.', - 'digits' => 'Il campo :attribute deve essere composto da :digits cifre.', - 'digits_between' => 'Il campo :attribute deve essere compreso tra :min e :max cifre.', - 'dimensions' => 'Il campo :attribute presenta dimensioni della foto non valide.', - 'distinct' => 'Il campo :attribute ha un valore duplicato.', - 'doesnt_end_with' => 'Il campo :attribute non deve terminare con uno dei seguenti: :values.', - 'doesnt_start_with' => 'Il campo :attribute non deve iniziare con uno dei seguenti: :values.', - 'email' => 'Il campo :attribute deve essere un indirizzo email valido.', - 'ends_with' => 'Il campo :attribute deve terminare con uno dei seguenti: :values.', - 'enum' => 'Il :attribute selezionato non valido.', - 'exists' => 'Il :attribute selezionato non valido.', - 'file' => 'Il campo :attribute deve essere un file.', - 'filled' => 'Il campo :attribute deve avere un valore.', - 'gt' => [ - 'array' => 'Il campo :attribute deve contenere più di :value elementi.', - 'file' => 'Il campo :attribute deve essere maggiore di :value kilobyte.', - 'numeric' => 'Il campo :attribute deve essere maggiore di :value.', - 'string' => 'Il campo :attribute deve essere maggiore di :value caratteri.', - ], - 'gte' => [ - 'array' => 'Il campo :attribute deve contenere almeno :value elementi.', - 'file' => 'Il campo :attribute deve essere maggiore o uguale a :value kilobyte.', - 'numeric' => 'Il campo :attribute deve essere maggiore o uguale a :value.', - 'string' => 'Il campo :attribute deve essere maggiore o uguale a :value caratteri.', - ], - 'image' => 'Il campo :attribute deve essere una foto.', - 'in' => 'Il :attribute selezionato non è valido.', - 'in_array' => 'Il campo :attribute deve esistere in :other.', - 'integer' => 'Il campo :attribute deve essere un numero intero.', - 'ip' => 'Il campo :attribute deve essere un indirizzo IP valido.', - 'ipv4' => 'Il campo :attribute deve essere un indirizzo IPv4 valido.', - 'ipv6' => 'Il campo :attribute deve essere un indirizzo IPv6 valido.', - 'json' => 'Il campo :attribute deve essere una stringa JSON valida.', - 'lowercase' => 'Il campo :attribute deve essere in minuscolo.', - 'lt' => [ - 'array' => 'Il campo :attribute deve contenere meno di :value elementi.', - 'file' => 'Il campo :attribute deve essere meno di :value kilobyte.', - 'numeric' => 'Il campo :attribute deve essere minore di :value.', - 'string' => 'Il campo :attribute deve essere minore di :value caratteri.', - ], - 'lte' => [ - 'array' => 'Il campo :attribute non deve avere più di :value elementi.', - 'file' => 'Il campo :attribute deve essere minore di o uguale a :value kilobyte.', - 'numeric' => 'Il campo :attribute deve essere minore di o uguale a :value.', - 'string' => 'Il campo :attribute deve essere minore di o uguale a :value caratteri.', - ], - 'mac_address' => 'Il campo :attribute deve essere un indirizzo MAC valido.', - 'max' => [ - 'array' => 'Il campo :attribute non deve avere più di :max elementi.', - 'file' => 'Il campo :attribute non deve essere maggiore di :max kilobyte.', - 'numeric' => 'Il campo :attribute non deve essere maggiore di :max.', - 'string' => 'Il campo :attribute non deve essere maggiore di :max caratteri.', - ], - 'max_digits' => 'Il campo :attribute non deve avere più di :max cifre.', - 'mimes' => 'Il campo :attribute deve essere un file di tipo: :values.', - 'mimetypes' => 'Il campo :attribute deve essere un file di tipo: :values.', - 'min' => [ - 'array' => 'Il campo :attribute deve avere almeno :min elementi.', - 'file' => 'Il campo :attribute deve essere almeno :min kilobyte.', - 'numeric' => 'Il campo :attribute deve essere almeno :min.', - 'string' => 'Il campo :attribute deve essere almeno :min caratteri.', - ], - 'min_digits' => 'Il campo :attribute deve avere almeno :min cifre.', - 'missing' => 'Il campo :attribute deve essere mancante.', - 'missing_if' => 'Il campo :attribute deve essere mancante quando :other è :value.', - 'missing_unless' => 'Il campo :attribute deve essere mancante a meno che :other sia :value.', - 'missing_with' => 'Il campo :attribute deve essere mancante quando :values è presente.', - 'missing_with_all' => 'Il campo :attribute deve essere mancante quando :values sono presenti.', - 'multiple_of' => 'Il campo :attribute deve essere un multiplo di :value.', - 'not_in' => 'Il :attribute selezionato non è valido.', - 'not_regex' => 'Il formato del campo :attribute non è valido.', - 'numeric' => 'Il campo :attribute deve essere un numero.', - 'password' => [ - 'letters' => 'Il campo :attribute deve contenere almeno una lettera.', - 'mixed' => 'Il campo :attribute deve contenere almeno un carattere minuscolo e un carattere maiuscolo.', - 'numbers' => 'Il campo :attribute deve contenere almeno un numero.', - 'symbols' => 'Il campo :attribute deve contenere almeno un simbolo speciale.', - 'uncompromised' => 'Il :attribute dato è apparso in un data leak. Per favore scegliere un :attribute differente.', - ], - 'present' => 'Il campo :attribute deve essere presente.', - 'prohibited' => 'Il campo :attribute è proibito.', - 'prohibited_if' => 'Il campo :attribute è proibito quando :other è :value.', - 'prohibited_unless' => 'Il campo :attribute è proibito a meno che :other sia in :values.', - 'prohibits' => 'Il campo :attribute proibisce :other da essere presente.', - 'regex' => 'Il formato del campo :attribute non è valido.', - 'required' => 'Il campo :attribute è richiesto.', - 'required_array_keys' => 'Il campo :attribute deve contenere inserimenti per: :values.', - 'required_if' => 'Il campo :attribute è richiesto quando :other è :value.', - 'required_if_accepted' => 'Il campo :attribute è richiesto quando :other è accettato.', - 'required_unless' => 'Il campo :attribute è richiesto a meno che :other sia in :values.', - 'required_with' => 'Il campo :attribute è richiesto quando :values è presente.', - 'required_with_all' => 'Il campo :attribute è richiesto quando :values sono presenti.', - 'required_without' => 'Il campo :attribute è richiesto quando :values non è presente.', - 'required_without_all' => 'Il campo :attribute è richiesto quando nessuno dei :values sono presenti.', - 'same' => 'Il campo :attribute deve corrispondere a :other.', - 'size' => [ - 'array' => 'Il campo :attribute deve contenere :size elementi.', - 'file' => 'Il campo :attribute deve essere :size kilobyte.', - 'numeric' => 'Il campo :attribute deve essere :size.', - 'string' => 'Il campo :attribute deve essere :size caratteri.', - ], - 'starts_with' => 'Il campo :attribute deve iniziare con uno dei seguenti: :values.', - 'string' => 'Il campo :attribute deve essere una stringa.', - 'timezone' => 'Il campo :attribute deve essere un fuso orario valido.', - 'unique' => 'Il :attribute è già stato preso.', - 'uploaded' => 'Il :attribute non è riuscito ad essere caricato.', - 'uppercase' => 'Il campo :attribute deve essere maiuscolo.', - 'url' => 'Il campo :attribute deve essere un URL valido.', - 'ulid' => 'Il campo :attribute deve essere un ULID valido.', - 'uuid' => 'Il campo :attribute deve essere un UUID valido.', - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => [ - 'attribute-name' => [ - 'rule-name' => 'custom-message', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap our attribute placeholder - | with something more reader friendly such as "E-Mail Address" instead - | of "email". This simply helps us make our message more expressive. - | - */ - - 'attributes' => [ - 'address' => 'indirizzo', - 'age' => 'età', - 'body' => 'corpo del testo', - 'cell' => 'cella', - 'city' => 'città', - 'country' => 'nazione', - 'date' => 'data', - 'day' => 'giorno', - 'excerpt' => 'sommario', - 'first_name' => 'nome', - 'gender' => 'identità di genere', - 'marital_status' => 'stato civile', - 'profession' => 'professione', - 'nationality' => 'nazionalità', - 'hour' => 'ora', - 'last_name' => 'cognome', - 'message' => 'messaggio', - 'minute' => 'minuto', - 'mobile' => 'cellulare', - 'month' => 'mese', - 'name' => 'nome', - 'zipcode' => 'CAP', - 'company_name' => 'nome azienda', - 'neighborhood' => 'quartiere', - 'number' => 'numero', - 'password' => 'password', - 'phone' => 'telefono', - 'second' => 'secondo', - 'sex' => 'sesso', - 'state' => 'stato', - 'street' => 'strada', - 'subject' => 'oggetto', - 'text' => 'testo', - 'time' => 'ora', - 'title' => 'titolo', - 'username' => 'username', - 'year' => 'anno', - 'description' => 'descrizione', - 'password_confirmation' => 'conferma password', - 'current_password' => 'password corrente', - 'complement' => 'complemento', - 'modality' => 'modalità', - 'category' => 'categoria', - 'blood_type' => 'gruppo sanguigno', - 'birth_date' => 'data di nascita', - ], -]; diff --git a/lang/nl_NL/api_tokens.php b/lang/nl_NL/api_tokens.php new file mode 100644 index 000000000..fd9683948 --- /dev/null +++ b/lang/nl_NL/api_tokens.php @@ -0,0 +1,30 @@ + 'API Tokens', + 'label' => 'API Tokens', + + // Token management + 'api_token' => 'API token', + 'api_tokens' => 'API tokens', + 'create_api_token' => 'API-sleutel aanmaken', + 'your_token' => 'Jouw token', + 'token_status' => 'Token status', + + // Token lists + 'active_tokens' => 'Actieve tokens', + 'expired_tokens' => 'Verlopen tokens', + 'all_tokens' => 'Alle tokens', + + // Token properties + 'expires_at' => 'Verloopt op', + 'expires_at_helper_text' => 'Laat leeg als u geen vervaldatum wilt', + 'last_used_at' => 'Laatst gebruikt op', + + // Abilities/Permissions + 'abilities' => 'Vaardigheden', + 'read_results' => 'Lees resultaten', + 'read_results_description' => 'De token zal toestemming hebben om resultaten en statistieken te lezen.', + 'run_speedtest_description' => 'Het token zal toestemming hebben om snelheidstest uit te voeren.', + 'list_servers_description' => 'De token zal toestemming hebben om servers weer te geven.', +]; diff --git a/lang/nl_NL/auth.php b/lang/nl_NL/auth.php index 8d24acff7..2c9909f81 100644 --- a/lang/nl_NL/auth.php +++ b/lang/nl_NL/auth.php @@ -13,8 +13,9 @@ | */ - 'failed' => 'De ingevoerde inloggegevens komen niet overeen.', - 'password' => 'Het ingevoerde wachtwoord is onjuist.', - 'throttle' => 'Te veel inlogpogingen. Wacht :seconds seconden en probeer het opnieuw.', + 'sign_in' => 'Aanmelden', + 'failed' => 'Deze gegevens komen niet overeen met onze administratie.', + 'password' => 'Het opgegeven wachtwoord is onjuist.', + 'throttle' => 'Te veel inlogpogingen. Probeer het over :seconds seconden opnieuw.', ]; diff --git a/lang/nl_NL/dashboard.php b/lang/nl_NL/dashboard.php new file mode 100644 index 000000000..24ea8f7b9 --- /dev/null +++ b/lang/nl_NL/dashboard.php @@ -0,0 +1,14 @@ + 'Dashboard', + 'no_speedtests_scheduled' => 'Geen snelheidtests gepland.', + 'next_speedtest_at' => 'Volgende snelheidstest op', + + // Widgets + 'recent_results' => 'Recente resultaten', + 'statistics' => 'Statistieken', + 'latest_download' => 'Laatste download', + 'latest_upload' => 'Laatste upload', + 'latest_ping' => 'Laatste ping', +]; diff --git a/lang/nl_NL/enums.php b/lang/nl_NL/enums.php new file mode 100644 index 000000000..f66fcdbd3 --- /dev/null +++ b/lang/nl_NL/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Benchmarking', + 'checking' => 'Controleren', + 'completed' => 'Voltooid', + 'failed' => 'Mislukt', + 'running' => 'Lopend', + 'started' => 'Gestart', + 'skipped' => 'Overgeslagen', + 'waiting' => 'Wachten', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Zakker', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/nl_NL/errors.php b/lang/nl_NL/errors.php new file mode 100644 index 000000000..edf5f4304 --- /dev/null +++ b/lang/nl_NL/errors.php @@ -0,0 +1,23 @@ + 'Server fout', + 'oops_server_error' => 'Oeps, server fout!', + 'error_message' => 'Foutmelding', + 'error_fetching_servers' => 'Fout bij ophalen servers', + 'servers_refreshed_successfully' => 'Servers met succes vernieuwd', + 'copied_to_clipboard' => 'Gekopieerd naar klembord', + + // Speedtest specific errors + 'ookla_error' => 'Er is een fout opgetreden tijdens het weergeven van snelheidstest servers, controleer de logbestanden.', + 'cron_invalid' => 'Ongeldige cron expressie', + + // Status fix command + 'status_fix' => [ + 'confirm' => 'Wilt u doorgaan?', + 'fail' => 'Commando afgebroken.', + 'finished' => '✅ klaar!', + 'info_1' => 'Dit zal alle resultaten controleren en de status herstellen naar "voltooid" of "mislukt" op basis van de gegevens.', + 'info_2' => '📖 Lees de documentatie: https://docs.speedtest-tracker.dev/other/commando\'s', + ], +]; diff --git a/lang/nl_NL/general.php b/lang/nl_NL/general.php new file mode 100644 index 000000000..e17828565 --- /dev/null +++ b/lang/nl_NL/general.php @@ -0,0 +1,121 @@ + 'Huidige versie', + 'latest_version' => 'Laatste versie', + 'github' => 'GitHub', + 'repository' => 'Repository', + + // Common actions + 'save' => 'Opslaan', + 'cancel' => 'Annuleren', + 'delete' => 'Verwijderen', + 'edit' => 'Bewerken', + 'create' => 'Aanmaken', + 'search' => 'Zoeken', + 'filter' => 'Filteren', + 'export' => 'Exporteren', + 'actions' => 'Acties', + 'enable' => 'Inschakelen', + 'yes' => 'Ja', + 'no' => 'Neen', + 'options' => 'Instellingen', + 'details' => 'Beschrijving', + 'view' => 'Bekijk', + + // Common labels + 'name' => 'Naam', + 'email' => 'E-mailadres', + 'email_address' => 'E-mail adres', + 'password' => 'Wachtwoord', + 'password_confirmation' => 'Wachtwoord bevestiging', + 'id' => 'ID', + 'status' => 'Status', + 'message' => 'Bericht', + 'comment' => 'Opmerking', + 'comments' => 'Opmerkingen', + 'created_at' => 'Aangemaakt op', + 'updated_at' => 'Bijgewerkt op', + 'url' => 'URL', + 'server' => 'Server', + 'servers' => 'Servers', + 'stats' => 'Statistieken', + 'statistics' => 'Statistieken', + + // Navigation + 'dashboard' => 'Dashboard', + 'results' => 'Resultaten', + 'settings' => 'Instellingen', + 'users' => 'Gebruikers', + 'documentation' => 'Documentatie', + 'view_documentation' => 'Bekijk documentatie', + 'links' => 'Koppelingen', + 'donate' => 'Doneren', + 'donations' => 'Donaties', + + // Roles + 'admin' => 'Beheerder', + 'user' => 'Gebruiker', + 'role' => 'Functie', + + // Date ranges + 'last_24h' => 'Afgelopen 24 uur', + 'last_week' => 'Vorige week', + 'last_month' => 'Vorige maand', + + // Metrics + 'metrics' => 'Statistieken', + 'average' => 'Gemiddeld', + 'high' => 'Hoog', + 'low' => 'Laag', + 'faster' => 'sneller', + 'slower' => 'langzamer', + 'healthy' => 'Gezond', + 'not_measured' => 'Niet gemeten', + 'unhealthy' => 'Ongezond', + + // Units + 'ms' => 'ms', + 'mbps' => 'Mbps', + + // Speed test metrics + 'download' => 'Download', + 'upload' => 'Uploaden', + 'ping' => 'Ping', + 'jitter' => 'Jitter', + + // Metric labels with units + 'download_mbps' => 'Downloaden (Mbps)', + 'upload_mbps' => 'Upload (Mbps)', + 'ping_ms' => 'Ping (ms)', + 'download_ms' => 'Downloaden (ms)', + 'upload_ms' => 'Uploaden (ms)', + 'average_ms' => 'Gemiddelde (ms)', + 'high_ms' => 'Hoog (ms)', + 'low_ms' => 'Laag (ms)', + 'ping_ms_label' => 'Ping (ms)', + + // Latency + 'download_latency' => 'Download latentie', + 'upload_latency' => 'Upload latentie', + + // Actions + 'run_speedtest' => 'Snelheidstest uitvoeren', + 'list_servers' => 'Toon servers', + 'export_current_results' => 'Huidige resultaten exporteren', + 'test' => 'Test', + + // Common + 'token' => 'Token', + + // Application + 'speedtest_tracker' => 'Speedtest Tracker', + 'platform' => 'Platform', + + // Update status + 'update_available' => 'Update beschikbaar!', + 'up_to_date' => 'Bijgewerkt tot', + + // Notifications + 'token_created' => 'Token aangemaakt', +]; diff --git a/lang/nl_NL/pagination.php b/lang/nl_NL/pagination.php deleted file mode 100644 index 7382e2e8e..000000000 --- a/lang/nl_NL/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Vorige', - 'next' => 'Volgende »', - -]; diff --git a/lang/nl_NL/passwords.php b/lang/nl_NL/passwords.php index 2e165a9f3..8fdab288a 100644 --- a/lang/nl_NL/passwords.php +++ b/lang/nl_NL/passwords.php @@ -13,11 +13,8 @@ | */ - 'reset' => 'Je wachtwoord is succesvol opnieuw ingesteld!', - 'sent' => 'We hebben je een e-mail gestuurd met een link om je wachtwoord opnieuw in te stellen!', - 'password' => 'Het wachtwoord moet minstens 6 tekens lang zijn en overeenkomen met de bevestiging.', - 'throttled' => 'Wacht even voordat je het opnieuw probeert.', - 'token' => 'De link om het wachtwoord opnieuw in te stellen is ongeldig of verlopen.', - 'user' => 'Er bestaat geen gebruikersaccount met dit e-mailadres.', + 'reset' => 'Uw wachtwoord is gereset!', + 'sent' => 'We hebben uw wachtwoord reset link gemaild!', + 'password' => 'Het wachtwoord en de bevestiging moeten overeenkomen en minimaal zes tekens bevatten.', ]; diff --git a/lang/nl_NL/results.php b/lang/nl_NL/results.php new file mode 100644 index 000000000..4a17ed6c8 --- /dev/null +++ b/lang/nl_NL/results.php @@ -0,0 +1,78 @@ + 'Resultaten', + 'result_overview' => 'Overzicht van resultaten', + 'error_message_title' => 'Fout melding', + + // Metrics + 'download' => 'Download', + 'download_latency_high' => 'Download latency hoog', + 'download_latency_low' => 'Download latency laag', + 'download_latency_iqm' => 'Download latency IQM', + 'download_latency_jitter' => 'Download latency jitter', + + 'upload' => 'Upload', + 'upload_latency_high' => 'Upload latentie hoog', + 'upload_latency_low' => 'Upload latency laag', + 'upload_latency_iqm' => 'Upload latency IQM', + 'upload_latency_jitter' => 'Upload latency Jitter', + + 'ping' => 'Ping', + 'ping_details' => 'Ping details', + 'ping_jitter' => 'Ping jitter', + 'ping_high' => 'Ping hoog', + 'ping_low' => 'Ping laag', + + 'packet_loss' => 'Pakket verlies', + 'iqm' => 'IQM', + + // Server & metadata + 'server_&_metadata' => 'Server & Metadata', + 'server_id' => 'Server ID', + 'server_host' => 'Server host', + 'server_name' => 'Server naam', + 'server_location' => 'Server locatie', + 'service' => 'Diensten', + 'isp' => 'Internetprovider', + 'ip_address' => 'IP adres', + 'scheduled' => 'Gepland', + + // Filters + 'only_healthy_speedtests' => 'Alleen gezonde snelheidstesten', + 'only_unhealthy_speedtests' => 'Alleen ongezonde snelheidstesten', + 'only_manual_speedtests' => 'Alleen handmatige snelheden', + 'only_scheduled_speedtests' => 'Alleen geplande snelheden', + 'created_from' => 'Aangemaakt vanaf', + 'created_until' => 'Gemaakt tot', + + // Export + 'export_all_results' => 'Alle resultaten exporteren', + 'export_all_results_description' => 'Zal elke kolom exporteren voor alle resultaten.', + 'export_completed' => 'Exporteren voltooid, :count :rows geëxporteerd.', + 'failed_export' => ':count :rows kon niet exporteren.', + 'row' => '{1} :count rij [2,*] :count rijen', + + // Actions + 'update_comments' => 'Reacties bijwerken', + 'view_on_speedtest_net' => 'Bekijk op Speedtest.net', + + // Notifications + 'speedtest_benchmark_passed' => 'Snelheidstest benchmark geslaagd', + 'speedtest_benchmark_failed' => 'Snelheidstest benchmark mislukt', + 'speedtest_started' => 'Snelheidstest gestart', + 'speedtest_completed' => 'Snelheidstest voltooid', + 'speedtest_failed' => 'Speedtest mislukt', + 'download_threshold_breached' => 'Drempelwaarde voor downloaden doorbroken!', + 'upload_threshold_breached' => 'Upload drempelwaarde gelekt!', + 'ping_threshold_breached' => 'Ping drempelwaarde doorgedrukt!', + + // Run Speedtest Action + 'speedtest' => 'Snelheidstest', + 'select_server' => 'Selecteer Server', + 'select_server_helper' => 'Laat leeg om de snelheidstest uit te voeren zonder een server op te geven. Geblokkeerde servers zullen worden overgeslagen.', + 'manual_servers' => 'Handmatige servers', + 'closest_servers' => 'Dichtstbijzijnde servers', + 'run_speedtest' => 'Snelheidstest uitvoeren', + 'start' => 'Begin test', +]; diff --git a/lang/nl_NL/settings.php b/lang/nl_NL/settings.php new file mode 100644 index 000000000..148d2d036 --- /dev/null +++ b/lang/nl_NL/settings.php @@ -0,0 +1,13 @@ + 'Instellingen', + 'label' => 'Instellingen', + + // Common settings labels + 'triggers' => 'Trigger', + 'verify_ssl' => 'Controleer SSL', + 'username' => 'Gebruikersnaam', + 'username_placeholder' => 'Gebruikersnaam voor Basis Auth (optioneel)', + 'password_placeholder' => 'Wachtwoord voor basis authenticatie (optioneel)', +]; diff --git a/lang/nl_NL/settings/data_integration.php b/lang/nl_NL/settings/data_integration.php new file mode 100644 index 000000000..c95902fac --- /dev/null +++ b/lang/nl_NL/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Data integratie', + 'label' => 'Data integratie', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'Wanneer ingeschakeld, zullen alle nieuwe test resultaten ook worden verzonden naar de InfluxDB.', + 'influxdb_v2_enabled' => 'Inschakelen', + 'influxdb_v2_url' => 'URL', + 'influxdb_v2_url_placeholder' => 'http://your-influxdb-instance', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Emmer', + 'influxdb_v2_bucket_placeholder' => 'speedtest-tracker', + 'influxdb_v2_token' => 'Token', + 'influxdb_v2_verify_ssl' => 'Controleer SSL', + + // Actions + 'test_connection' => 'Verbindingstest testen', + 'starting_bulk_data_write_to_influxdb' => 'Alle resultaten naar InfluxDB sturen', + 'sending_test_data_to_influxdb' => 'Testgegevens verzenden naar InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Influxdb test mislukt', + 'influxdb_test_failed_body' => 'Bekijk de logs voor meer details.', + 'influxdb_test_success' => 'Test gegevens succesvol verzonden naar Influxdb', + 'influxdb_test_success_body' => 'Test gegevens zijn verzonden naar de InfluxDB, controleer of de gegevens zijn ontvangen.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Bulk schrijven naar Influxdb mislukt.', + 'influxdb_bulk_write_failed_body' => 'Bekijk de logs voor meer details.', + 'influxdb_bulk_write_success' => 'Alle resultaten naar InfluxDB sturen afgerond.', + 'influxdb_bulk_write_success_body' => 'Gegevens zijn verzonden naar InfluxDB, controleer of de gegevens zijn ontvangen.', + + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Inschakelen', + 'prometheus_enabled_helper_text' => 'Wanneer ingeschakeld, zullen statistieken voor elke nieuwe snelheidstest beschikbaar zijn vanaf het /Prometheus eindpunt.', + 'prometheus_allowed_ips' => 'Toegestane IP-adressen', + 'prometheus_allowed_ips_helper' => 'Lijst van IP-adressen of CIDR (bijv. 192.168.1.0/24) toegestaan om het eindpunt van de statistieken te bekijken. Laat leeg om alle IP-adressen toe te staan.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Emmer', +]; diff --git a/lang/nl_NL/settings/notifications.php b/lang/nl_NL/settings/notifications.php new file mode 100644 index 000000000..7a37c850a --- /dev/null +++ b/lang/nl_NL/settings/notifications.php @@ -0,0 +1,62 @@ + 'Notificaties', + 'label' => 'Notificaties', + + // Database notifications + 'database' => 'Database', + 'database_description' => 'Meldingen die naar dit kanaal worden verzonden worden weergegeven onder de 🔔 icoon in de header.', + 'test_database_channel' => 'Test database notificaties', + + // Mail notifications + 'mail' => 'E-mailen', + 'recipients' => 'Ontvangers', + 'test_mail_channel' => 'Test e-mailkanaal', + + // Apprise notifications + 'apprise' => 'Apprise', + 'enable_apprise_notifications' => 'Inschakelen Apprise meldingen', + 'apprise_server' => 'Apprise Server', + 'apprise_server_url' => 'Appprise Server-URL', + 'apprise_server_url_helper' => 'De URL van uw Apprise Server. De URL moet eindigen op /notify', + 'apprise_verify_ssl' => 'Controleer SSL', + 'apprise_channels' => 'Notificatie kanalen', + 'apprise_channel_url' => 'Service URL', + 'apprise_hint_description' => 'Met Apprise kan je meldingen verzenden naar meer dan 90 diensten. Je moet een Apprise server hebben draaien en onderstaande service URL\'s configureren.', + 'apprise_channel_url_helper' => 'Gebruik Apprise URL formaat. Bijvoorbeeld discord://WebhookID/Token, slack://TokenA/TokenB/TokenC', + 'test_apprise_channel' => 'Test Apprise', + 'apprise_channel_url_validation_error' => 'Ongeldige Apprise URL. De URL moet gebruik maken van Apprise formaat (bijv. discord://, slack://), niet http:// of https://. Zie de Apprise documentatie voor meer informatie', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Test webhook kanaal', + 'webhook_hint_description' => 'Dit zijn algemene webhooks. Voor payload voorbeelden en implementatiegegevens, bekijk de documentatie. Voor diensten zoals Discord, Ntfy etc. gebruik Apprise.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Notificatie bij elke geplande snelheidstest', + 'notify_on_threshold_failures' => 'Melding bij drempelfouten voor geplande snelheidstests', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'Ik zeg: ping', + 'pong' => 'Jij zegt: pong', + 'received' => 'Test database melding ontvangen!', + 'sent' => 'Test database melding verzonden.', + ], + 'mail' => [ + 'add' => 'Ontvangers toevoegen!', + 'sent' => 'Test mail melding verzonden.', + ], + 'webhook' => [ + 'add' => 'Voeg webhook URL\'s toe!', + 'sent' => 'Test webhook melding verzonden.', + 'payload' => 'Webhook melding', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Drempel meldingen worden verzonden naar de /fail route in de URL.', +]; diff --git a/lang/nl_NL/settings/thresholds.php b/lang/nl_NL/settings/thresholds.php new file mode 100644 index 000000000..b03b2974b --- /dev/null +++ b/lang/nl_NL/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Limieten', + 'label' => 'Drempels', + + // Absolute thresholds + 'absolute' => 'Absoluut', + 'absolute_description' => 'Limieten houden geen rekening met de voorgeschiedenis en kunnen bij elke test worden gebruikt.', + 'absolute_enabled' => 'Limieten inschakelen', + + // Metrics section + 'metrics' => 'Statistieken', + 'metrics_helper_text' => 'Ingesteld op nul om deze meting uit te schakelen.', + + // General threshold labels + 'thresholds' => 'Grenswaarden', + 'threshold_enabled' => 'Drempel ingeschakeld', + 'threshold_download' => 'Drempel downloaden', + 'threshold_upload' => 'Drempel uploaden', + 'threshold_ping' => 'Drempel ping', +]; diff --git a/lang/nl_NL/tools.php b/lang/nl_NL/tools.php new file mode 100644 index 000000000..f24f227c4 --- /dev/null +++ b/lang/nl_NL/tools.php @@ -0,0 +1,6 @@ + 'Ookla servers', +]; diff --git a/lang/nl_NL/users.php b/lang/nl_NL/users.php new file mode 100644 index 000000000..0d5c5d5df --- /dev/null +++ b/lang/nl_NL/users.php @@ -0,0 +1,15 @@ + 'Gebruikers', + 'label' => 'Gebruikers', + + // User prompts and messages + 'user_change' => [ + 'info' => 'Gebruikersrol bijgewerkt.', + 'password_updated_info' => ':email wachtwoord bijgewerkt.', + 'what_is_password' => 'Wat is het nieuwe wachtwoord?', + 'what_is_the_email_address' => 'Wat is het e-mailadres?', + 'what_role' => 'Welke rol moet de gebruiker hebben?', + ], +]; diff --git a/lang/nl_NL/validation.php b/lang/nl_NL/validation.php index 7f7a2f416..509fabd8e 100644 --- a/lang/nl_NL/validation.php +++ b/lang/nl_NL/validation.php @@ -13,145 +13,6 @@ | */ - 'accepted' => ':attribute moet worden geaccepteerd.', - 'accepted_if' => ':attribute moet worden geaccepteerd als :other :value is.', - 'active_url' => ':attribute moet een geldige URL zijn.', - 'after' => ':attribute moet een datum na :date zijn.', - 'after_or_equal' => ':attribute moet een datum zijn op of na :date.', - 'alpha' => ':attribute mag alleen letters bevatten.', - 'alpha_dash' => ':attribute mag alleen letters, cijfers, koppeltekens en underscores bevatten.', - 'alpha_num' => ':attribute mag alleen letters en cijfers bevatten.', - 'array' => ':attribute moet een lijst zijn.', - 'ascii' => ':attribute mag alleen standaardtekens bevatten.', - 'before' => ':attribute moet een datum voor :date zijn.', - 'before_or_equal' => ':attribute moet een datum zijn op of voor :date.', - 'between' => [ - 'array' => ':attribute moet tussen :min en :max items bevatten.', - 'file' => ':attribute moet tussen :min en :max kilobytes groot zijn.', - 'numeric' => ':attribute moet tussen :min en :max liggen.', - 'string' => ':attribute moet tussen :min en :max tekens lang zijn.', - ], - 'boolean' => ':attribute moet waar of onwaar zijn.', - 'can' => ':attribute bevat een ongeldige waarde.', - 'confirmed' => ':attribute komt niet overeen met de bevestiging.', - 'current_password' => 'Het ingevoerde wachtwoord is onjuist.', - 'date' => ':attribute is geen geldige datum.', - 'date_equals' => ':attribute moet exact :date zijn.', - 'date_format' => ':attribute komt niet overeen met het formaat :format.', - 'decimal' => ':attribute moet :decimal decimalen bevatten.', - 'declined' => ':attribute moet worden afgewezen.', - 'declined_if' => ':attribute moet worden afgewezen als :other :value is.', - 'different' => ':attribute en :other moeten verschillend zijn.', - 'digits' => ':attribute moet uit :digits cijfers bestaan.', - 'digits_between' => ':attribute moet tussen :min en :max cijfers bevatten.', - 'dimensions' => ':attribute heeft ongeldige afbeeldingsafmetingen.', - 'distinct' => ':attribute bevat een dubbele waarde.', - 'doesnt_end_with' => ':attribute mag niet eindigen met een van de volgende: :values.', - 'doesnt_start_with' => ':attribute mag niet beginnen met een van de volgende: :values.', - 'email' => ':attribute moet een geldig e-mailadres zijn.', - 'ends_with' => ':attribute moet eindigen met een van de volgende: :values.', - 'enum' => 'De geselecteerde waarde voor :attribute is ongeldig.', - 'exists' => ':attribute bestaat al.', - 'file' => ':attribute moet een bestand zijn.', - 'filled' => ':attribute mag niet leeg zijn.', - 'gt' => [ - 'array' => ':attribute moet meer dan :value items bevatten.', - 'file' => ':attribute moet groter zijn dan :value kilobytes.', - 'numeric' => ':attribute moet groter zijn dan :value.', - 'string' => ':attribute moet meer dan :value tekens bevatten.', - ], - 'gte' => [ - 'array' => ':attribute moet minimaal :value items bevatten.', - 'file' => ':attribute moet minimaal :value kilobytes zijn.', - 'numeric' => ':attribute moet minimaal :value zijn.', - 'string' => ':attribute moet minimaal :value tekens bevatten.', - ], - 'image' => ':attribute moet een afbeelding zijn.', - 'in' => 'De geselecteerde waarde voor :attribute is ongeldig.', - 'in_array' => ':attribute moet voorkomen in :other.', - 'integer' => ':attribute moet een geheel getal zijn.', - 'ip' => ':attribute moet een geldig IP-adres zijn.', - 'ipv4' => ':attribute moet een geldig IPv4-adres zijn.', - 'ipv6' => ':attribute moet een geldig IPv6-adres zijn.', - 'json' => ':attribute moet een geldige JSON-string zijn.', - 'lowercase' => ':attribute moet alleen kleine letters bevatten.', - 'lt' => [ - 'array' => ':attribute mag maximaal :value items bevatten.', - 'file' => ':attribute moet kleiner zijn dan :value kilobytes.', - 'numeric' => ':attribute moet kleiner zijn dan :value.', - 'string' => ':attribute moet minder dan :value tekens bevatten.', - ], - 'lte' => [ - 'array' => ':attribute mag niet meer dan :value items bevatten.', - 'file' => ':attribute mag maximaal :value kilobytes zijn.', - 'numeric' => ':attribute mag maximaal :value zijn.', - 'string' => ':attribute mag maximaal :value tekens bevatten.', - ], - 'mac_address' => ':attribute moet een geldig MAC-adres zijn.', - 'max' => [ - 'array' => ':attribute mag maximaal :max items bevatten.', - 'file' => ':attribute mag maximaal :max kilobytes zijn.', - 'numeric' => ':attribute mag niet groter zijn dan :max.', - 'string' => ':attribute mag niet langer zijn dan :max tekens.', - ], - 'max_digits' => ':attribute mag maximaal :max cijfers bevatten.', - 'mimes' => ':attribute moet een bestand zijn van het type: :values.', - 'mimetypes' => ':attribute moet een bestand zijn van het type: :values.', - 'min' => [ - 'array' => ':attribute moet minstens :min items bevatten.', - 'file' => ':attribute moet minstens :min kilobytes zijn.', - 'numeric' => ':attribute moet minstens :min zijn.', - 'string' => ':attribute moet minstens :min tekens bevatten.', - ], - 'min_digits' => ':attribute moet minstens :min cijfers bevatten.', - 'missing' => ':attribute moet ontbreken.', - 'missing_if' => ':attribute moet ontbreken als :other ":value" is.', - 'missing_unless' => ':attribute moet ontbreken tenzij :other :value is.', - 'missing_with' => ':attribute moet ontbreken als :values aanwezig is.', - 'missing_with_all' => ':attribute moet ontbreken als :values aanwezig zijn.', - 'multiple_of' => ':attribute moet een veelvoud van :value zijn.', - 'not_in' => 'De geselecteerde waarde voor :attribute is ongeldig.', - 'not_regex' => 'Het formaat van :attribute is ongeldig.', - 'numeric' => ':attribute moet een getal zijn.', - 'password' => [ - 'letters' => ':attribute moet minstens één letter bevatten.', - 'mixed' => ':attribute moet minstens één hoofdletter en één kleine letter bevatten.', - 'numbers' => ':attribute moet minstens één cijfer bevatten.', - 'symbols' => ':attribute moet minstens één speciaal teken bevatten.', - 'uncompromised' => 'Het opgegeven :attribute is aangetroffen in een datalek. Kies een andere :attribute.', - ], - 'present' => ':attribute moet aanwezig zijn.', - 'prohibited' => ':attribute mag niet worden opgegeven.', - 'prohibited_if' => ':attribute mag niet worden opgegeven als :other ":value" is.', - 'prohibited_unless' => ':attribute mag alleen worden opgegeven als :other één van de volgende waarden heeft: :values.', - 'prohibits' => ':attribute staat niet toe dat :other aanwezig is.', - 'regex' => 'Het formaat van :attribute is ongeldig.', - 'required' => ':attribute is verplicht.', - 'required_array_keys' => ':attribute moet waarden bevatten voor: :values.', - 'required_if' => ':attribute is verplicht als :other ":value" is.', - 'required_if_accepted' => ':attribute is verplicht als :other is geaccepteerd.', - 'required_unless' => ':attribute is verplicht tenzij :other één van de volgende waarden heeft: :values.', - 'required_with' => ':attribute is verplicht als :values aanwezig is.', - 'required_with_all' => ':attribute is verplicht als alle :values aanwezig zijn.', - 'required_without' => ':attribute is verplicht als :values niet aanwezig is.', - 'required_without_all' => ':attribute is verplicht als geen van :values aanwezig zijn.', - 'same' => ':attribute en :other moeten overeenkomen.', - 'size' => [ - 'array' => ':attribute moet precies :size items bevatten.', - 'file' => ':attribute moet :size kilobytes zijn.', - 'numeric' => ':attribute moet :size zijn.', - 'string' => ':attribute moet :size tekens lang zijn.', - ], - 'starts_with' => ':attribute moet beginnen met één van de volgende: :values.', - 'string' => ':attribute moet een tekst zijn.', - 'timezone' => ':attribute moet een geldige tijdzone zijn.', - 'unique' => ':attribute is al in gebruik.', - 'uploaded' => ':attribute kon niet worden geüpload.', - 'uppercase' => ':attribute moet alleen hoofdletters bevatten.', - 'url' => ':attribute moet een geldige URL zijn.', - 'ulid' => ':attribute moet een geldige ULID zijn.', - 'uuid' => ':attribute moet een geldige UUID zijn.', - /* |-------------------------------------------------------------------------- | Custom Validation Language Lines @@ -165,7 +26,7 @@ 'custom' => [ 'attribute-name' => [ - 'rule-name' => 'aangepaste-bericht', + ], ], @@ -181,50 +42,50 @@ */ 'attributes' => [ - 'address' => 'Adres', - 'age' => 'Leeftijd', - 'body' => 'Inhoud', - 'cell' => 'Mobiel', - 'city' => 'Stad', - 'country' => 'Land', - 'date' => 'Datum', - 'day' => 'Dag', - 'excerpt' => 'Samenvatting', - 'first_name' => 'Voornaam', - 'gender' => 'Geslacht', - 'marital_status' => 'Burgerlijke staat', - 'profession' => 'Beroep', - 'nationality' => 'Nationaliteit', - 'hour' => 'Uur', - 'last_name' => 'Achternaam', - 'message' => 'Bericht', - 'minute' => 'Minuut', - 'mobile' => 'Mobiele nummer', - 'month' => 'Maand', - 'name' => 'Naam', - 'zipcode' => 'Postcode', - 'company_name' => 'Bedrijfsnaam', - 'neighborhood' => 'Wijk', - 'number' => 'Nummer', - 'password' => 'Wachtwoord', - 'phone' => 'Telefoonnummer', - 'second' => 'Seconde', - 'sex' => 'Geslacht', - 'state' => 'Provincie', - 'street' => 'Straat', - 'subject' => 'Onderwerp', - 'text' => 'Tekst', - 'time' => 'Tijd', - 'title' => 'Titel', - 'username' => 'Gebruikersnaam', - 'year' => 'Jaar', - 'description' => 'Beschrijving', - 'password_confirmation' => 'Wachtwoordbevestiging', - 'current_password' => 'Huidig wachtwoord', - 'complement' => 'Aanvulling', - 'modality' => 'Modaliteit', - 'category' => 'Categorie', - 'blood_type' => 'Bloedgroep', - 'birth_date' => 'Geboortedatum', + 'address' => 'adres', + 'age' => 'leeftijd', + 'body' => 'inhoud', + 'cell' => 'cel', + 'city' => 'stad', + 'country' => 'land', + 'date' => 'datum', + 'day' => 'dag', + 'excerpt' => 'summary', + 'first_name' => 'voornaam', + 'gender' => 'geslacht', + 'marital_status' => 'burgerlijke staat', + 'profession' => 'beroep', + 'nationality' => 'nationaliteit', + 'hour' => 'uur', + 'last_name' => 'achternaam', + 'message' => 'bericht', + 'minute' => 'minuut', + 'mobile' => 'mobiel', + 'month' => 'maand', + 'name' => 'naam', + 'zipcode' => 'postcode', + 'company_name' => 'bedrijfsnaam', + 'neighborhood' => 'wijk', + 'number' => 'nummer', + 'password' => 'wachtwoord', + 'phone' => 'telefoon', + 'second' => 'seconde', + 'sex' => 'geslacht', + 'state' => 'staat', + 'street' => 'straat', + 'subject' => 'onderwerp', + 'text' => 'tekst', + 'time' => 'tijd', + 'title' => 'titel', + 'username' => 'gebruikersnaam', + 'year' => 'jaar', + 'description' => 'beschrijving', + 'password_confirmation' => 'wachtwoord bevestiging', + 'current_password' => 'huidig wachtwoord', + 'complement' => 'complementair', + 'modality' => 'modaliteit', + 'category' => 'categorie', + 'blood_type' => 'bloed type', + 'birth_date' => 'geboortedatum', ], ]; diff --git a/lang/pt_BR/api_tokens.php b/lang/pt_BR/api_tokens.php new file mode 100644 index 000000000..17ae5269c --- /dev/null +++ b/lang/pt_BR/api_tokens.php @@ -0,0 +1,30 @@ + 'Tokens de API', + 'label' => 'Tokens de API', + + // Token management + 'api_token' => 'Token de API', + 'api_tokens' => 'Tokens da API', + 'create_api_token' => 'Criar token de API', + 'your_token' => 'Seu token', + 'token_status' => 'Estado do token', + + // Token lists + 'active_tokens' => 'Tokens ativos', + 'expired_tokens' => 'Tokens expirados', + 'all_tokens' => 'Todos os tokens', + + // Token properties + 'expires_at' => 'Expira em', + 'expires_at_helper_text' => 'Deixe em branco se você não quiser uma data de validade', + 'last_used_at' => 'Última vez usado em', + + // Abilities/Permissions + 'abilities' => 'Habilidades', + 'read_results' => 'Ler resultados', + 'read_results_description' => 'O token terá permissão para ler resultados e estatísticas.', + 'run_speedtest_description' => 'O token terá permissão para executar o teste de velocidade.', + 'list_servers_description' => 'O token terá permissão para listar servidores.', +]; diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php index a9d4d26d8..13625436a 100644 --- a/lang/pt_BR/auth.php +++ b/lang/pt_BR/auth.php @@ -13,8 +13,9 @@ | */ - 'failed' => 'Essas credenciais não foram encontradas em nossos registros.', - 'password' => 'A senha informada está incorreta.', - 'throttle' => 'Muitas tentativas de login. Tente novamente em :seconds segundos.', + 'sign_in' => 'Entrar', + 'failed' => 'Credenciais não correspondem aos nossos registros.', + 'password' => 'A senha fornecida está incorreta.', + 'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.', ]; diff --git a/lang/pt_BR/dashboard.php b/lang/pt_BR/dashboard.php new file mode 100644 index 000000000..16510f3e8 --- /dev/null +++ b/lang/pt_BR/dashboard.php @@ -0,0 +1,14 @@ + 'Painel', + 'no_speedtests_scheduled' => 'Nenhum teste de velocidade agendado.', + 'next_speedtest_at' => 'Próximo teste de velocidade', + + // Widgets + 'recent_results' => 'Resultados Recentes', + 'statistics' => 'Estatísticas', + 'latest_download' => 'Último download', + 'latest_upload' => 'Último upload', + 'latest_ping' => 'Último ping', +]; diff --git a/lang/pt_BR/enums.php b/lang/pt_BR/enums.php new file mode 100644 index 000000000..2518bf4a7 --- /dev/null +++ b/lang/pt_BR/enums.php @@ -0,0 +1,21 @@ + [ + 'benchmarking' => 'Benchmarking', + 'checking' => 'Verificando', + 'completed' => 'Concluído', + 'failed' => 'Falhou', + 'running' => 'Executando', + 'started' => 'Iniciado', + 'skipped' => 'Ignorado', + 'waiting' => 'Esperando', + ], + + // Service enum values + 'service' => [ + 'faker' => 'Fake', + 'ookla' => 'Ookla', + ], +]; diff --git a/lang/pt_BR/errors.php b/lang/pt_BR/errors.php new file mode 100644 index 000000000..5e1993583 --- /dev/null +++ b/lang/pt_BR/errors.php @@ -0,0 +1,23 @@ + 'Erro no Servidor', + 'oops_server_error' => 'Opa, erro no servidor!', + 'error_message' => 'Mensagem de erro', + 'error_fetching_servers' => 'Erro ao buscar servidores', + 'servers_refreshed_successfully' => 'Servidores atualizados com sucesso', + 'copied_to_clipboard' => 'Copiado para o clipboard', + + // Speedtest specific errors + 'ookla_error' => 'Ocorreu um erro ao listar servidores de velocidade, verifique os logs.', + 'cron_invalid' => 'Expressão cron inválida', + + // Status fix command + 'status_fix' => [ + 'confirm' => 'Você deseja continuar?', + 'fail' => 'Comando abortado.', + 'finished' => '✅ concluído!', + 'info_1' => 'Isto irá verificar todos os resultados e corrigir o status para "concluído" ou "falhou" com base nos dados.', + 'info_2' => '📖 Leia a documentação: https://docs.speedtest-tracker.dev/other/commands', + ], +]; diff --git a/lang/pt_BR/general.php b/lang/pt_BR/general.php new file mode 100644 index 000000000..7a8b67af2 --- /dev/null +++ b/lang/pt_BR/general.php @@ -0,0 +1,121 @@ + 'Versão atual', + 'latest_version' => 'Versão mais recente', + 'github' => 'GitHub', + 'repository' => 'Repositório', + + // Common actions + 'save' => 'Salvar', + 'cancel' => 'Cancelar', + 'delete' => 'Excluir', + 'edit' => 'Alterar', + 'create' => 'Criar', + 'search' => 'Pesquisar', + 'filter' => 'Filtrar', + 'export' => 'Exportar', + 'actions' => 'Ações', + 'enable' => 'Habilitado', + 'yes' => 'Sim', + 'no' => 'Não', + 'options' => 'Opções', + 'details' => 'Detalhes', + 'view' => 'Visualizar', + + // Common labels + 'name' => 'Nome', + 'email' => 'E-mail', + 'email_address' => 'Endereço de e-mail', + 'password' => 'Senha', + 'password_confirmation' => 'Confirmação de senha', + 'id' => 'ID', + 'status' => 'Status', + 'message' => 'Mensagem', + 'comment' => 'Comentar', + 'comments' => 'Comentários', + 'created_at' => 'Criado em', + 'updated_at' => 'Atualizado em', + 'url' => 'URL', + 'server' => 'Servidor', + 'servers' => 'Servidores', + 'stats' => 'Estatísticas', + 'statistics' => 'Estatísticas', + + // Navigation + 'dashboard' => 'Painel', + 'results' => 'Resultados', + 'settings' => 'Confirgurações', + 'users' => 'Usuários', + 'documentation' => 'Documentação', + 'view_documentation' => 'Ver documentação', + 'links' => 'Links', + 'donate' => 'Doar', + 'donations' => 'Doações', + + // Roles + 'admin' => 'Admin', + 'user' => 'Usuário', + 'role' => 'Funções', + + // Date ranges + 'last_24h' => 'Últimas 24 horas', + 'last_week' => 'Semana passada', + 'last_month' => 'Mês anterior', + + // Metrics + 'metrics' => 'Métricas', + 'average' => 'Média', + 'high' => 'Alta', + 'low' => 'Baixa', + 'faster' => 'mais rápido', + 'slower' => 'lento', + 'healthy' => 'Saudável', + 'not_measured' => 'Não medido', + 'unhealthy' => 'Não saudável', + + // Units + 'ms' => 'ms', + 'mbps' => 'Mbps', + + // Speed test metrics + 'download' => 'Download', + 'upload' => 'Upload', + 'ping' => 'Latência', + 'jitter' => 'Jitter', + + // Metric labels with units + 'download_mbps' => 'Download (Mbps)', + 'upload_mbps' => 'Upload (Mbps)', + 'ping_ms' => 'Latência (ms)', + 'download_ms' => 'Download (ms)', + 'upload_ms' => 'Upload (ms)', + 'average_ms' => 'Média (ms)', + 'high_ms' => 'Alto (ms)', + 'low_ms' => 'Baixa (ms)', + 'ping_ms_label' => 'Latência (ms)', + + // Latency + 'download_latency' => 'Latência de Download', + 'upload_latency' => 'Latência de Upload', + + // Actions + 'run_speedtest' => 'Executar teste de velocidade', + 'list_servers' => 'Listar servidores', + 'export_current_results' => 'Exportar resultados atuais', + 'test' => 'Testar', + + // Common + 'token' => 'Token', + + // Application + 'speedtest_tracker' => 'Speedtest Tracker', + 'platform' => 'Plataforma', + + // Update status + 'update_available' => 'Atualização disponível!', + 'up_to_date' => 'Atualizado', + + // Notifications + 'token_created' => 'Token criado', +]; diff --git a/lang/pt_BR/pagination.php b/lang/pt_BR/pagination.php deleted file mode 100644 index 4deabd6fb..000000000 --- a/lang/pt_BR/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Anterior', - 'next' => 'Próximo »', - -]; diff --git a/lang/pt_BR/passwords.php b/lang/pt_BR/passwords.php index b49c439f8..8fa4d5f00 100644 --- a/lang/pt_BR/passwords.php +++ b/lang/pt_BR/passwords.php @@ -14,10 +14,7 @@ */ 'reset' => 'Sua senha foi redefinida!', - 'sent' => 'Enviamos seu link de redefinição de senha por e-mail!', - 'password' => 'A senha e a confirmação devem combinar e possuir pelo menos seis caracteres.', - 'throttled' => 'Aguarde antes de tentar novamente.', - 'token' => 'Este token de redefinição de senha é inválido.', - 'user' => 'Não encontramos um usuário com esse endereço de e-mail.', + 'sent' => 'Enviamos um e-mail com o link para redefinir sua senha!', + 'password' => 'A senha e a confirmação devem corresponder e conter pelo menos seis caracteres.', ]; diff --git a/lang/pt_BR/results.php b/lang/pt_BR/results.php new file mode 100644 index 000000000..5d3c62055 --- /dev/null +++ b/lang/pt_BR/results.php @@ -0,0 +1,78 @@ + 'Resultados', + 'result_overview' => 'Visão geral do resultado', + 'error_message_title' => 'Mensagem de erro', + + // Metrics + 'download' => 'Download', + 'download_latency_high' => 'Latência de Download alta', + 'download_latency_low' => 'Latência de Download baixa', + 'download_latency_iqm' => 'Latência de Download IQM', + 'download_latency_jitter' => 'Jitter de latência de Download', + + 'upload' => 'Upload', + 'upload_latency_high' => 'Latência de Upload alta', + 'upload_latency_low' => 'Latência de Upload baixa', + 'upload_latency_iqm' => 'Latência de Upload IQM', + 'upload_latency_jitter' => 'Jitter de latência de Upload', + + 'ping' => 'Latência', + 'ping_details' => 'Detalhes do Ping', + 'ping_jitter' => 'Jitter do Ping', + 'ping_high' => 'Latência de ping alta', + 'ping_low' => 'Latência de ping baixa', + + 'packet_loss' => 'Perda de pacote', + 'iqm' => 'IQM', + + // Server & metadata + 'server_&_metadata' => 'Servidor e Metadados', + 'server_id' => 'ID do Servidor', + 'server_host' => 'Host do servidor', + 'server_name' => 'Nome do servidor', + 'server_location' => 'Localização do servidor', + 'service' => 'Serviço', + 'isp' => 'Provedor', + 'ip_address' => 'Endereço IP', + 'scheduled' => 'Agendado', + + // Filters + 'only_healthy_speedtests' => 'Apenas testes de velocidade saudáveis', + 'only_unhealthy_speedtests' => 'Apenas testes de velocidade não saudáveis', + 'only_manual_speedtests' => 'Apenas testes de velocidade manuais', + 'only_scheduled_speedtests' => 'Apenas testes de velocidade agendados', + 'created_from' => 'Criado por', + 'created_until' => 'Criado até', + + // Export + 'export_all_results' => 'Exportar todos resultados', + 'export_all_results_description' => 'Irá exportar todas as colunas para todos os resultados.', + 'export_completed' => 'Exportação concluída, :count :rows exportados.', + 'failed_export' => ':count :rows falhou ao exportar.', + 'row' => '{1} :count fileira [2,*] :count linhas', + + // Actions + 'update_comments' => 'Atualizar comentários', + 'view_on_speedtest_net' => 'Ver em Speedtest.net', + + // Notifications + 'speedtest_benchmark_passed' => 'Referência do teste de velocidade aprovada', + 'speedtest_benchmark_failed' => 'Referência do teste de velocidade falhou', + 'speedtest_started' => 'Teste de velocidade iniciado', + 'speedtest_completed' => 'Teste de velocidade concluído', + 'speedtest_failed' => 'Teste de velocidade falhou', + 'download_threshold_breached' => 'Limite de Download violado!', + 'upload_threshold_breached' => 'Limite de Upload violado!', + 'ping_threshold_breached' => 'Limite de ping violado!', + + // Run Speedtest Action + 'speedtest' => 'Teste de velocidade', + 'select_server' => 'Selecionar servidor', + 'select_server_helper' => 'Deixe em branco para executar o acelerador sem especificar um servidor. Os servidores bloqueados serão ignorados.', + 'manual_servers' => 'Servidores manuais', + 'closest_servers' => 'Servidores mais próximos', + 'run_speedtest' => 'Executar teste de velocidade', + 'start' => 'Iniciar', +]; diff --git a/lang/pt_BR/settings.php b/lang/pt_BR/settings.php new file mode 100644 index 000000000..b557f3d08 --- /dev/null +++ b/lang/pt_BR/settings.php @@ -0,0 +1,13 @@ + 'Confirgurações', + 'label' => 'Confirgurações', + + // Common settings labels + 'triggers' => 'Gatilhos', + 'verify_ssl' => 'Verificar SSL', + 'username' => 'Usuário', + 'username_placeholder' => 'Nome de usuário para autenticação básica (opcional)', + 'password_placeholder' => 'Senha para Autenticação Básica (opcional)', +]; diff --git a/lang/pt_BR/settings/data_integration.php b/lang/pt_BR/settings/data_integration.php new file mode 100644 index 000000000..02cc2a6fd --- /dev/null +++ b/lang/pt_BR/settings/data_integration.php @@ -0,0 +1,46 @@ + 'Integração de dados', + 'label' => 'Integração de dados', + + // InfluxDB v2 + 'influxdb_v2' => 'InfluxDB v2', + 'influxdb_v2_description' => 'Quando ativado, todos os novos resultados de Speedtest também serão enviados para InfluxDB.', + 'influxdb_v2_enabled' => 'Habilitado', + 'influxdb_v2_url' => 'URL:', + 'influxdb_v2_url_placeholder' => 'http://sua-influxdb-instância', + 'influxdb_v2_org' => 'Org', + 'influxdb_v2_bucket' => 'Bucket', + 'influxdb_v2_bucket_placeholder' => 'speedtest-tracker', + 'influxdb_v2_token' => 'Token', + 'influxdb_v2_verify_ssl' => 'Verificar SSL', + + // Actions + 'test_connection' => 'Testar conexão', + 'starting_bulk_data_write_to_influxdb' => 'Iniciando dados em massa no InfluxDB', + 'sending_test_data_to_influxdb' => 'Enviando dados de teste para InfluxDB', + + // Test connection notifications + 'influxdb_test_failed' => 'Falha no teste Influxdb', + 'influxdb_test_failed_body' => 'Confira os logs para mais detalhes.', + 'influxdb_test_success' => 'Dados de teste enviados com sucesso para o Influxdb', + 'influxdb_test_success_body' => 'Dados de teste enviados para InfluxDB, verifique se os dados foram recebidos.', + + // Bulk write notifications + 'influxdb_bulk_write_failed' => 'Falha ao escrever em massa no Influxdb.', + 'influxdb_bulk_write_failed_body' => 'Confira os logs para mais detalhes.', + 'influxdb_bulk_write_success' => 'Carga massiva de dados concluída para o Influxdb.', + 'influxdb_bulk_write_success_body' => 'Os dados foram enviados para InfluxDB, verifique se os dados foram recebidos.', + + // Prometheus + 'prometheus' => 'Prometheus', + 'prometheus_enabled' => 'Habilitado', + 'prometheus_enabled_helper_text' => 'Quando ativado, as métricas para cada novo radar estarão disponíveis no ponto de extremidade do /prometheus.', + 'prometheus_allowed_ips' => 'Endereços de IP Permitidos', + 'prometheus_allowed_ips_helper' => 'Lista de endereços IP ou intervalos de CIDR (por exemplo, 192.168.1.0/24) permitidos de acessar o ponto de extremidade das métricas. Deixe em branco para permitir que todos os IPs.', + + // Common labels + 'org' => 'Org', + 'bucket' => 'Bucket', +]; diff --git a/lang/pt_BR/settings/notifications.php b/lang/pt_BR/settings/notifications.php new file mode 100644 index 000000000..827ce15f8 --- /dev/null +++ b/lang/pt_BR/settings/notifications.php @@ -0,0 +1,61 @@ + 'Notificações', + 'label' => 'Notificações', + + // Database notifications + 'database' => 'Banco de Dados', + 'database_description' => 'Notificações enviadas para este canal aparecerão sob o 🔔 ícone no cabeçalho.', + 'test_database_channel' => 'Testar canal do banco de dados', + + // Mail notifications + 'mail' => 'Correio', + 'recipients' => 'Destinatários', + 'test_mail_channel' => 'Testar canal de e-mail', + + // Apprise notifications + 'apprise' => 'Informar', + 'enable_apprise_notifications' => 'Habilitar notificações Apprise', + 'apprise_server' => 'Servidor Apprise', + 'apprise_server_url' => 'URL do Servidor Apprise', + 'apprise_verify_ssl' => 'Verificar SSL', + 'apprise_channels' => 'Canais Apprise', + 'apprise_channel_url' => 'URL do Canal', + 'apprise_hint_description' => 'Para obter mais informações sobre como configurar o Apprise, veja a documentação.', + 'apprise_channel_url_helper' => 'Forneça o URL do serviço endpoint para notificações.', + 'test_apprise_channel' => 'Testar Apprise', + 'apprise_channel_url_validation_error' => 'O URL do canal Apprise não deve começar com "http" ou "https". Por favor, forneça um esquema válido de URL Apprise.', + + // Webhook + 'webhook' => 'Webhook', + 'webhooks' => 'Webhooks', + 'test_webhook_channel' => 'Testar canal webhook', + 'webhook_hint_description' => 'Estes são webhooks genéricos. Para exemplos de payload e detalhes de implementação, consulte a documentação.', + + // Common notification messages + 'notify_on_every_speedtest_run' => 'Notificar a cada execução do teste de velocidade', + 'notify_on_threshold_failures' => 'Notificar sobre falhas nos limites de testes de velocidade agendados', + + // Test notification messages + 'test_notifications' => [ + 'database' => [ + 'ping' => 'Eu digo: ping', + 'pong' => 'Você diz: pong', + 'received' => 'Teste de notificação de banco de dados recebida!', + 'sent' => 'Teste de notificação do banco de dados enviada.', + ], + 'mail' => [ + 'add' => 'Adicione destinatários de email!', + 'sent' => 'Notificação de teste de email enviada.', + ], + 'webhook' => [ + 'add' => 'Adicionar URLs webhook!', + 'sent' => 'Notificação de teste webhook enviada.', + 'payload' => 'Testando notificação webhook', + ], + ], + + // Helper text + 'threshold_helper_text' => 'Notificações de limite serão enviadas para a rota /fail na URL.', +]; diff --git a/lang/pt_BR/settings/thresholds.php b/lang/pt_BR/settings/thresholds.php new file mode 100644 index 000000000..5c67c55be --- /dev/null +++ b/lang/pt_BR/settings/thresholds.php @@ -0,0 +1,22 @@ + 'Limites', + 'label' => 'Limites', + + // Absolute thresholds + 'absolute' => 'Absoluto', + 'absolute_description' => 'Os limites absolutos não levam em consideração o histórico anterior e podem ser acionados em cada teste.', + 'absolute_enabled' => 'Habilitar limites absolutos', + + // Metrics section + 'metrics' => 'Métricas', + 'metrics_helper_text' => 'Defina zero para desativar esta métrica.', + + // General threshold labels + 'thresholds' => 'Limites', + 'threshold_enabled' => 'Limite habilitado', + 'threshold_download' => 'Limite de download', + 'threshold_upload' => 'Limite de upload', + 'threshold_ping' => 'Limite de ping', +]; diff --git a/lang/pt_BR/tools.php b/lang/pt_BR/tools.php new file mode 100644 index 000000000..249d79aae --- /dev/null +++ b/lang/pt_BR/tools.php @@ -0,0 +1,6 @@ + 'Servidores Ookla', +]; diff --git a/lang/pt_BR/users.php b/lang/pt_BR/users.php new file mode 100644 index 000000000..ef4af9f81 --- /dev/null +++ b/lang/pt_BR/users.php @@ -0,0 +1,15 @@ + 'Usuários', + 'label' => 'Usuários', + + // User prompts and messages + 'user_change' => [ + 'info' => 'Função do usuário atualizada.', + 'password_updated_info' => ':email atualizado.', + 'what_is_password' => 'Qual é a nova senha?', + 'what_is_the_email_address' => 'Qual é o endereço de e-mail?', + 'what_role' => 'Qual papel o usuário deve ter?', + ], +]; diff --git a/lang/pt_BR/validation.php b/lang/pt_BR/validation.php index e11c1bee1..34d05b329 100644 --- a/lang/pt_BR/validation.php +++ b/lang/pt_BR/validation.php @@ -13,145 +13,6 @@ | */ - 'accepted' => 'O campo :attribute deve ser aceito.', - 'accepted_if' => 'O :attribute deve ser aceito quando :other for :value.', - 'active_url' => 'O campo :attribute não é uma URL válida.', - 'after' => 'O campo :attribute deve ser uma data posterior a :date.', - 'after_or_equal' => 'O campo :attribute deve ser uma data posterior ou igual a :date.', - 'alpha' => 'O campo :attribute só pode conter letras.', - 'alpha_dash' => 'O campo :attribute só pode conter letras, números e traços.', - 'alpha_num' => 'O campo :attribute só pode conter letras e números.', - 'array' => 'O campo :attribute deve ser uma matriz.', - 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', // TODO - 'before' => 'O campo :attribute deve ser uma data anterior :date.', - 'before_or_equal' => 'O campo :attribute deve ser uma data anterior ou igual a :date.', - 'between' => [ - 'numeric' => 'O campo :attribute deve ser entre :min e :max.', - 'file' => 'O campo :attribute deve ser entre :min e :max kilobytes.', - 'string' => 'O campo :attribute deve ser entre :min e :max caracteres.', - 'array' => 'O campo :attribute deve ter entre :min e :max itens.', - ], - 'boolean' => 'O campo :attribute deve ser verdadeiro ou falso.', - 'can' => 'The :attribute field contains an unauthorized value.', // TODO - 'confirmed' => 'O campo :attribute de confirmação não confere.', - 'current_password' => 'A senha está incorreta.', - 'date' => 'O campo :attribute não é uma data válida.', - 'date_equals' => 'O campo :attribute deve ser uma data igual a :date.', - 'date_format' => 'O campo :attribute não corresponde ao formato :format.', - 'decimal' => 'The :attribute field must have :decimal decimal places.', // TODO - 'declined' => 'O :attribute deve ser recusado.', - 'declined_if' => 'O :attribute deve ser recusado quando :other for :value.', - 'different' => 'Os campos :attribute e :other devem ser diferentes.', - 'digits' => 'O campo :attribute deve ter :digits dígitos.', - 'digits_between' => 'O campo :attribute deve ter entre :min e :max dígitos.', - 'dimensions' => 'O campo :attribute tem dimensões de imagem inválidas.', - 'distinct' => 'O campo :attribute campo tem um valor duplicado.', - 'doesnt_end_with' => 'O :attribute não pode terminar com um dos seguintes: :values.', - 'doesnt_start_with' => 'O :attribute não pode começar com um dos seguintes: :values.', - 'email' => 'O campo :attribute deve ser um endereço de e-mail válido.', - 'ends_with' => 'O campo :attribute deve terminar com um dos seguintes: :values', - 'enum' => 'O :attribute selecionado é inválido.', - 'exists' => 'O campo :attribute selecionado é inválido.', - 'file' => 'O campo :attribute deve ser um arquivo.', - 'filled' => 'O campo :attribute deve ter um valor.', - 'gt' => [ - 'numeric' => 'O campo :attribute deve ser maior que :value.', - 'file' => 'O campo :attribute deve ser maior que :value kilobytes.', - 'string' => 'O campo :attribute deve ser maior que :value caracteres.', - 'array' => 'O campo :attribute deve conter mais de :value itens.', - ], - 'gte' => [ - 'numeric' => 'O campo :attribute deve ser maior ou igual a :value.', - 'file' => 'O campo :attribute deve ser maior ou igual a :value kilobytes.', - 'string' => 'O campo :attribute deve ser maior ou igual a :value caracteres.', - 'array' => 'O campo :attribute deve conter :value itens ou mais.', - ], - 'image' => 'O campo :attribute deve ser uma imagem.', - 'in' => 'O campo :attribute selecionado é inválido.', - 'in_array' => 'O campo :attribute não existe em :other.', - 'integer' => 'O campo :attribute deve ser um número inteiro.', - 'ip' => 'O campo :attribute deve ser um endereço de IP válido.', - 'ipv4' => 'O campo :attribute deve ser um endereço IPv4 válido.', - 'ipv6' => 'O campo :attribute deve ser um endereço IPv6 válido.', - 'json' => 'O campo :attribute deve ser uma string JSON válida.', - 'lowercase' => 'The :attribute field must be lowercase.', // TODO - 'lt' => [ - 'numeric' => 'O campo :attribute deve ser menor que :value.', - 'file' => 'O campo :attribute deve ser menor que :value kilobytes.', - 'string' => 'O campo :attribute deve ser menor que :value caracteres.', - 'array' => 'O campo :attribute deve conter menos de :value itens.', - ], - 'lte' => [ - 'numeric' => 'O campo :attribute deve ser menor ou igual a :value.', - 'file' => 'O campo :attribute deve ser menor ou igual a :value kilobytes.', - 'string' => 'O campo :attribute deve ser menor ou igual a :value caracteres.', - 'array' => 'O campo :attribute não deve conter mais que :value itens.', - ], - 'mac_address' => 'The :attribute field must be a valid MAC address.', // TODO - 'max' => [ - 'numeric' => 'O campo :attribute não pode ser superior a :max.', - 'file' => 'O campo :attribute não pode ser superior a :max kilobytes.', - 'string' => 'O campo :attribute não pode ser superior a :max caracteres.', - 'array' => 'O campo :attribute não pode ter mais do que :max itens.', - ], - 'max_digits' => 'O campo :attribute não pode ser superior a :max dígitos', - 'mimes' => 'O campo :attribute deve ser um arquivo do tipo: :values.', - 'mimetypes' => 'O campo :attribute deve ser um arquivo do tipo: :values.', - 'min' => [ - 'numeric' => 'O campo :attribute deve ser pelo menos :min.', - 'file' => 'O campo :attribute deve ter pelo menos :min kilobytes.', - 'string' => 'O campo :attribute deve ter pelo menos :min caracteres.', - 'array' => 'O campo :attribute deve ter pelo menos :min itens.', - ], - 'min_digits' => 'O campo :attribute deve ter pelo menos :min dígitos', - 'missing' => 'The :attribute field must be missing.', // TODO - 'missing_if' => 'The :attribute field must be missing when :other is :value.', // TODO - 'missing_unless' => 'The :attribute field must be missing unless :other is :value.', // TODO - 'missing_with' => 'O campo :attribute não deve estar presente quando houver :values.', - 'missing_with_all' => 'The :attribute field must be missing when :values are present.', // TODO - 'multiple_of' => 'O campo :attribute deve ser um múltiplo de :value.', - 'not_in' => 'O campo :attribute selecionado é inválido.', - 'not_regex' => 'O campo :attribute possui um formato inválido.', - 'numeric' => 'O campo :attribute deve ser um número.', - 'password' => [ - 'letters' => 'O campo :attribute deve conter pelo menos uma letra.', - 'mixed' => 'O campo :attribute deve conter pelo menos uma letra maiúscula e uma letra minúscula.', - 'numbers' => 'O campo :attribute deve conter pelo menos um número.', - 'symbols' => 'O campo :attribute deve conter pelo menos um símbolo.', - 'uncompromised' => 'A senha que você inseriu em :attribute está em um vazamento de dados. Por favor escolha uma senha diferente.', - ], - 'present' => 'O campo :attribute deve estar presente.', - 'prohibited' => 'O campo :attribute é proibido.', - 'prohibited_if' => 'O campo :attribute é proibido quando :other for :value.', - 'prohibited_unless' => 'O campo :attribute é proibido exceto quando :other for :values.', - 'prohibits' => 'O campo :attribute proíbe :other de estar presente.', - 'regex' => 'O campo :attribute tem um formato inválido.', - 'required' => 'O campo :attribute é obrigatório.', - 'required_array_keys' => 'O campo :attribute deve conter entradas para: :values.', - 'required_if' => 'O campo :attribute é obrigatório quando :other for :value.', - 'required_if_accepted' => 'The :attribute field is required when :other is accepted.', // TODO - 'required_unless' => 'O campo :attribute é obrigatório exceto quando :other for :values.', - 'required_with' => 'O campo :attribute é obrigatório quando :values está presente.', - 'required_with_all' => 'O campo :attribute é obrigatório quando :values está presente.', - 'required_without' => 'O campo :attribute é obrigatório quando :values não está presente.', - 'required_without_all' => 'O campo :attribute é obrigatório quando nenhum dos :values estão presentes.', - 'same' => 'Os campos :attribute e :other devem corresponder.', - 'size' => [ - 'numeric' => 'O campo :attribute deve ser :size.', - 'file' => 'O campo :attribute deve ser :size kilobytes.', - 'string' => 'O campo :attribute deve ser :size caracteres.', - 'array' => 'O campo :attribute deve conter :size itens.', - ], - 'starts_with' => 'O campo :attribute deve começar com um dos seguintes valores: :values', - 'string' => 'O campo :attribute deve ser uma string.', - 'timezone' => 'O campo :attribute deve ser uma zona válida.', - 'unique' => 'O campo :attribute já está sendo utilizado.', - 'uploaded' => 'Ocorreu uma falha no upload do campo :attribute.', - 'uppercase' => 'The :attribute field must be uppercase.', // TODO - 'url' => 'O campo :attribute tem um formato inválido.', - 'ulid' => 'The :attribute field must be a valid ULID.', // TODO - 'uuid' => 'O campo :attribute deve ser um UUID válido.', - /* |-------------------------------------------------------------------------- | Custom Validation Language Lines @@ -165,7 +26,7 @@ 'custom' => [ 'attribute-name' => [ - 'rule-name' => 'custom-message', + ], ], @@ -184,26 +45,26 @@ 'address' => 'endereço', 'age' => 'idade', 'body' => 'conteúdo', - 'cell' => 'célula', + 'cell' => 'celular', 'city' => 'cidade', 'country' => 'país', 'date' => 'data', 'day' => 'dia', 'excerpt' => 'resumo', 'first_name' => 'primeiro nome', - 'gender' => 'gênero', + 'gender' => 'sexo', 'marital_status' => 'estado civil', 'profession' => 'profissão', 'nationality' => 'nacionalidade', 'hour' => 'hora', - 'last_name' => 'sobrenome', + 'last_name' => 'último nome', 'message' => 'mensagem', 'minute' => 'minuto', 'mobile' => 'celular', 'month' => 'mês', 'name' => 'nome', - 'zipcode' => 'cep', - 'company_name' => 'razão social', + 'zipcode' => 'CEP', + 'company_name' => 'nome da empresa', 'neighborhood' => 'bairro', 'number' => 'número', 'password' => 'senha', @@ -214,17 +75,17 @@ 'street' => 'rua', 'subject' => 'assunto', 'text' => 'texto', - 'time' => 'hora', + 'time' => 'horário', 'title' => 'título', 'username' => 'usuário', 'year' => 'ano', 'description' => 'descrição', - 'password_confirmation' => 'confirmação da senha', + 'password_confirmation' => 'confirmação de senha', 'current_password' => 'senha atual', 'complement' => 'complemento', 'modality' => 'modalidade', 'category' => 'categoria', - 'blood_type' => 'tipo sanguíneo', + 'blood_type' => 'tipo de sangue', 'birth_date' => 'data de nascimento', ], ]; diff --git a/lang/tr_TR/auth.php b/lang/tr_TR/auth.php deleted file mode 100644 index 273c94fee..000000000 --- a/lang/tr_TR/auth.php +++ /dev/null @@ -1,20 +0,0 @@ - 'Bu bilgiler kayıtlarımızla uyuşmuyor.', - 'password' => 'Sağlanan şifre yanlış.', - 'throttle' => 'Çok fazla giriş denemesi. Lütfen :seconds saniye içinde tekrar deneyin.', - -]; diff --git a/lang/tr_TR/pagination.php b/lang/tr_TR/pagination.php deleted file mode 100644 index 8c760579e..000000000 --- a/lang/tr_TR/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« Önceki', - 'next' => 'Sonraki »', - -]; diff --git a/lang/tr_TR/passwords.php b/lang/tr_TR/passwords.php deleted file mode 100644 index a6563d39c..000000000 --- a/lang/tr_TR/passwords.php +++ /dev/null @@ -1,23 +0,0 @@ - 'Parolanız sıfırlandı!', - 'sent' => 'Parola sıfırlama bağlantınızı e-postayla gönderdik!', - 'password' => 'Şifre ve onay aynı olmalı ve en az altı karakter uzunluğunda olmalıdır.', - 'throttled' => 'Tekrar denemeden önce lütfen bekleyin.', - 'token' => 'Bu parola sıfırlama anahtarı geçersiz.', - 'user' => 'Bu e-posta adresine sahip bir kullanıcı bulamadık.', - -]; diff --git a/lang/tr_TR/validation.php b/lang/tr_TR/validation.php deleted file mode 100644 index 92ff01c14..000000000 --- a/lang/tr_TR/validation.php +++ /dev/null @@ -1,230 +0,0 @@ - ':attribute alanı kabul edilmelidir.', - 'accepted_if' => ':attribute alanı, :other değeri :value olduğunda kabul edilmelidir.', - 'active_url' => ':attribute alanı geçerli bir URL olmalıdır.', - 'after' => ':attribute alanı :date değerinden sonraki bir tarih olmalıdır.', - 'after_or_equal' => ':attribute alanı :date tarihinden sonra veya ona eşit bir tarih olmalıdır.', - 'alpha' => ':attribute alanı yalnızca harf içermelidir.', - 'alpha_dash' => ':attribute alanı yalnızca harf, rakam, tire(-) ve alt çizgi(_) içermelidir.', - 'alpha_num' => ':attribute alanı yalnızca harf ve rakamlardan oluşmalıdır.', - 'array' => ':attribute alanı bir dizi olmalıdır.', - 'ascii' => ':attribute alanı yalnızca tek baytlık alfanümerik karakterler ve semboller içermelidir.', - 'before' => ':attribute alanı :date değerinden önceki bir tarih olmalıdır.', - 'before_or_equal' => ':attribute alanı :date tarihinden önce veya ona eşit bir tarih olmalıdır.', - 'between' => [ - 'array' => ':attribute :min ile :max öğe arasında olmalıdır.', - 'file' => ':attribute :min ile :max kilobayt arasında olmalıdır.', - 'numeric' => ':attribute :min ile :max arasında olmalıdır.', - 'string' => ':attribute :min ile :max karakter arasında olmalıdır.', - ], - 'boolean' => ':attribute alanı doğru veya yanlış olmalıdır.', - 'can' => ':attribute alanı yetkisiz bir değer içeriyor.', - 'confirmed' => ':attribute doğrulaması eşleşmiyor.', - 'current_password' => 'Şifre yanlış.', - 'date' => ':attribute geçerli bir tarih olmalıdır.', - 'date_equals' => ':attribute :date tarihine eşit bir tarih olmalıdır.', - 'date_format' => ':attribute :format formatıyla eşleşmelidir.', - 'decimal' => ':attribute :decimal ondalık basamak içermelidir.', - 'declined' => ':attribute reddedilmelidir.', - 'declined_if' => ':attribute, :other :value olduğunda reddedilmelidir.', - 'different' => ':attribute ve :other farklı olmalıdır.', - 'digits' => ':attribute :digits basamaklı olmalıdır.', - 'digits_between' => ':attribute :min ile :max basamak arasında olmalıdır.', - 'dimensions' => ':attribute geçersiz resim boyutlarına sahiptir.', - 'distinct' => ':attribute alanında yinelenen bir değer var.', - 'doesnt_end_with' => ':attribute şu değerlerden biriyle bitmemelidir: :values.', - 'doesnt_start_with' => ':attribute şu değerlerden biriyle başlamamalıdır: :values.', - 'email' => ':attribute geçerli bir e-posta adresi olmalıdır.', - 'ends_with' => ':attribute şu değerlerden biriyle bitmelidir: :values.', - 'enum' => 'Seçilen :attribute geçersiz.', - 'exists' => 'Seçilen :attribute geçersiz.', - 'file' => ':attribute bir dosya olmalıdır.', - 'filled' => ':attribute bir değer içermelidir.', - 'gt' => [ - 'array' => ':attribute :value öğeden fazla olmalıdır.', - 'file' => ':attribute :value kilobayttan büyük olmalıdır.', - 'numeric' => ':attribute :value değerinden büyük olmalıdır.', - 'string' => ':attribute :value karakterden uzun olmalıdır.', - ], - 'gte' => [ - 'array' => ':attribute :value veya daha fazla öğe içermelidir.', - 'file' => ':attribute :value kilobayt veya daha büyük olmalıdır.', - 'numeric' => ':attribute :value değerine eşit veya daha büyük olmalıdır.', - 'string' => ':attribute :value karakter veya daha fazla olmalıdır.', - ], - 'image' => ':attribute bir resim olmalıdır.', - 'in' => 'Seçilen :attribute geçersiz.', - 'in_array' => ':attribute, :other içinde mevcut olmalıdır.', - 'integer' => ':attribute bir tam sayı olmalıdır.', - 'ip' => ':attribute geçerli bir IP adresi olmalıdır.', - 'ipv4' => ':attribute geçerli bir IPv4 adresi olmalıdır.', - 'ipv6' => ':attribute geçerli bir IPv6 adresi olmalıdır.', - 'json' => ':attribute geçerli bir JSON metni olmalıdır.', - 'lowercase' => ':attribute küçük harflerden oluşmalıdır.', - 'lt' => [ - 'array' => ':attribute :value öğeden az olmalıdır.', - 'file' => ':attribute :value kilobayttan küçük olmalıdır.', - 'numeric' => ':attribute :value değerinden küçük olmalıdır.', - 'string' => ':attribute :value karakterden kısa olmalıdır.', - ], - 'lte' => [ - 'array' => ':attribute :value öğeden fazla olmamalıdır.', - 'file' => ':attribute :value kilobayt veya daha az olmalıdır.', - 'numeric' => ':attribute :value değerine eşit veya daha küçük olmalıdır.', - 'string' => ':attribute :value karakter veya daha az olmalıdır.', - ], - 'mac_address' => ':attribute geçerli bir MAC adresi olmalıdır.', - 'max' => [ - 'array' => ':attribute :max öğeden fazla olmamalıdır.', - 'file' => ':attribute :max kilobayttan büyük olmamalıdır.', - 'numeric' => ':attribute :max değerinden büyük olmamalıdır.', - 'string' => ':attribute :max karakterden uzun olmamalıdır.', - ], - 'max_digits' => ':attribute :max basamaktan fazla olmamalıdır.', - 'mimes' => ':attribute şu türde bir dosya olmalıdır: :values.', - 'mimetypes' => ':attribute şu türde bir dosya olmalıdır: :values.', - 'min' => [ - 'array' => ':attribute en az :min öğe içermelidir.', - 'file' => ':attribute en az :min kilobayt olmalıdır.', - 'numeric' => ':attribute en az :min olmalıdır.', - 'string' => ':attribute en az :min karakter olmalıdır.', - ], - 'min_digits' => ':attribute en az :min basamak içermelidir.', - 'missing' => ':attribute eksik olmalıdır.', - 'missing_if' => ':attribute, :other :value olduğunda eksik olmalıdır.', - 'missing_unless' => ':attribute, :other :value değilse eksik olmalıdır.', - 'missing_with' => ':attribute, :values mevcut olduğunda eksik olmalıdır.', - 'missing_with_all' => ':attribute, :values mevcut olduğunda eksik olmalıdır.', - 'multiple_of' => ':attribute :value katı olmalıdır.', - 'not_in' => 'Seçilen :attribute geçersiz.', - 'not_regex' => ':attribute formatı geçersiz.', - 'numeric' => ':attribute bir sayı olmalıdır.', - 'password' => [ - 'letters' => ':attribute en az bir harf içermelidir.', - 'mixed' => ':attribute en az bir büyük harf ve bir küçük harf içermelidir.', - 'numbers' => ':attribute en az bir rakam içermelidir.', - 'symbols' => ':attribute en az bir sembol içermelidir.', - 'uncompromised' => 'Verilen :attribute bir veri ihlalinde tespit edilmiştir. Lütfen farklı bir :attribute seçin.', - ], - 'present' => ':attribute mevcut olmalıdır.', - 'prohibited' => ':attribute yasaktır.', - 'prohibited_if' => ':attribute, :other :value olduğunda yasaktır.', - 'prohibited_unless' => ':attribute, :other :values içinde olmadıkça yasaktır.', - 'prohibits' => ':attribute, :other alanının mevcut olmasını yasaklar.', - 'regex' => ':attribute formatı geçersiz.', - 'required' => ':attribute alanı gereklidir.', - 'required_array_keys' => ':attribute şu anahtarları içermelidir: :values.', - 'required_if' => ':attribute, :other :value olduğunda gereklidir.', - 'required_if_accepted' => ':attribute, :other kabul edildiğinde gereklidir.', - 'required_unless' => ':attribute, :other :values içinde olmadıkça gereklidir.', - 'required_with' => ':attribute, :values mevcut olduğunda gereklidir.', - 'required_with_all' => ':attribute, :values mevcut olduğunda gereklidir.', - 'required_without' => ':attribute, :values mevcut değilse gereklidir.', - 'required_without_all' => ':attribute, :values hiçbirisi mevcut değilse gereklidir.', - 'same' => ':attribute, :other ile eşleşmelidir.', - 'size' => [ - 'array' => ':attribute :size öğe içermelidir.', - 'file' => ':attribute :size kilobayt olmalıdır.', - 'numeric' => ':attribute :size olmalıdır.', - 'string' => ':attribute :size karakter olmalıdır.', - ], - 'starts_with' => ':attribute şu değerlerden biriyle başlamalıdır: :values.', - 'string' => ':attribute bir metin olmalıdır.', - 'timezone' => ':attribute geçerli bir zaman dilimi olmalıdır.', - 'unique' => ':attribute zaten alınmış.', - 'uploaded' => ':attribute yüklenemedi.', - 'uppercase' => ':attribute büyük harflerden oluşmalıdır.', - 'url' => ':attribute geçerli bir URL olmalıdır.', - 'ulid' => ':attribute geçerli bir ULID olmalıdır.', - 'uuid' => ':attribute geçerli bir UUID olmalıdır.', - - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ - - 'custom' => [ - 'attribute-name' => [ - 'rule-name' => 'custom-message', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap our attribute placeholder - | with something more reader friendly such as "E-Mail Address" instead - | of "email". This simply helps us make our message more expressive. - | - */ - - 'attributes' => [ - 'address' => 'adres', - 'age' => 'yaş', - 'body' => 'içerik', - 'cell' => 'hücre', - 'city' => 'şehir', - 'country' => 'ülke', - 'date' => 'tarih', - 'day' => 'gün', - 'excerpt' => 'özet', - 'first_name' => 'ad', - 'gender' => 'cinsiyet', - 'marital_status' => 'medeni hal', - 'profession' => 'meslek', - 'nationality' => 'uyruk', - 'hour' => 'saat', - 'last_name' => 'soyad', - 'message' => 'mesaj', - 'minute' => 'dakika', - 'mobile' => 'cep telefonu', - 'month' => 'ay', - 'name' => 'isim', - 'zipcode' => 'posta kodu', - 'company_name' => 'şirket adı', - 'neighborhood' => 'mahalle', - 'number' => 'numara', - 'password' => 'şifre', - 'phone' => 'telefon', - 'second' => 'saniye', - 'sex' => 'cinsiyet', - 'state' => 'eyalet', - 'street' => 'sokak', - 'subject' => 'konu', - 'text' => 'metin', - 'time' => 'zaman', - 'title' => 'başlık', - 'username' => 'kullanıcı adı', - 'year' => 'yıl', - 'description' => 'açıklama', - 'password_confirmation' => 'şifre doğrulama', - 'current_password' => 'mevcut şifre', - 'complement' => 'ek bilgi', - 'modality' => 'mod', - 'category' => 'kategori', - 'blood_type' => 'kan grubu', - 'birth_date' => 'doğum tarihi', - ], -]; diff --git a/lang/zh_TW/pagination.php b/lang/zh_TW/pagination.php deleted file mode 100644 index cca0a8930..000000000 --- a/lang/zh_TW/pagination.php +++ /dev/null @@ -1,19 +0,0 @@ - '« 上一頁', - 'next' => '下一頁 »', - -]; diff --git a/lang/zh_TW/passwords.php b/lang/zh_TW/passwords.php deleted file mode 100644 index f3e0935c9..000000000 --- a/lang/zh_TW/passwords.php +++ /dev/null @@ -1,20 +0,0 @@ - '您的密碼已成功重設!', - 'sent' => '我們已將密碼重設連結寄送至您的電子郵件信箱!', - 'password' => '密碼必須至少包含六個字元,且與確認密碼相符。', - -]; diff --git a/lang/zh_TW/validation.php b/lang/zh_TW/validation.php deleted file mode 100644 index 2f4c46f4c..000000000 --- a/lang/zh_TW/validation.php +++ /dev/null @@ -1,91 +0,0 @@ - [ - 'attribute-name' => [ - - ], - ], - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap our attribute placeholder - | with something more reader friendly such as "E-Mail Address" instead - | of "email". This simply helps us make our message more expressive. - | - */ - - 'attributes' => [ - 'address' => '地址', - 'age' => '年齡', - 'body' => '內文', - 'cell' => '行動電話', - 'city' => '縣市', - 'country' => '國家', - 'date' => '日期', - 'day' => '日', - 'excerpt' => '摘要', - 'first_name' => '名字', - 'gender' => '性別', - 'marital_status' => '婚姻狀態', - 'profession' => '職業', - 'nationality' => '國籍', - 'hour' => '時', - 'last_name' => '姓氏', - 'message' => '訊息內容', - 'minute' => '分', - 'mobile' => '行動電話', - 'month' => '月', - 'name' => '名稱', - 'zipcode' => '郵遞區號', - 'company_name' => '公司名稱', - 'neighborhood' => '鄰里', - 'number' => '號碼', - 'password' => '密碼', - 'phone' => '電話號碼', - 'second' => '秒', - 'sex' => '性別', - 'state' => '縣市', - 'street' => '街道', - 'subject' => '主旨', - 'text' => '文字', - 'time' => '時間', - 'title' => '標題', - 'username' => '使用者帳號', - 'year' => '年', - 'description' => '說明', - 'password_confirmation' => '確認密碼', - 'current_password' => '目前密碼', - 'complement' => '補充說明', - 'modality' => '模式', - 'category' => '類別', - 'blood_type' => '血型', - 'birth_date' => '出生日期', - ], -]; diff --git a/openapi.json b/openapi.json index ffe15d82d..0c171dfba 100644 --- a/openapi.json +++ b/openapi.json @@ -21,7 +21,7 @@ "$ref": "#/components/parameters/AcceptHeader" }, { - "name": "per_page", + "name": "per.page", "in": "query", "description": "Number of results per page", "required": false, @@ -952,7 +952,7 @@ "path": { "type": "string" }, - "per_page": { + "per.page": { "type": "integer" }, "to": { diff --git a/package-lock.json b/package-lock.json index eb0c8bb10..6e8087046 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "": { "devDependencies": { "@tailwindcss/typography": "^0.5.10", - "@tailwindcss/vite": "^4.1.16", + "@tailwindcss/vite": "^4.1.17", "autoprefixer": "^10.4.15", "laravel-vite-plugin": "^1.0.0", - "tailwindcss": "^4.1.16", + "tailwindcss": "^4.1.17", "vite": "^6.4.1" } }, @@ -506,9 +506,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "cpu": [ "arm" ], @@ -520,9 +520,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "cpu": [ "arm64" ], @@ -534,9 +534,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "cpu": [ "arm64" ], @@ -548,9 +548,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "cpu": [ "x64" ], @@ -562,9 +562,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", "cpu": [ "arm64" ], @@ -576,9 +576,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "cpu": [ "x64" ], @@ -590,9 +590,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "cpu": [ "arm" ], @@ -604,9 +604,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "cpu": [ "arm" ], @@ -618,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "cpu": [ "arm64" ], @@ -632,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "cpu": [ "arm64" ], @@ -646,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", "cpu": [ "loong64" ], @@ -660,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "cpu": [ "ppc64" ], @@ -674,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", "cpu": [ "riscv64" ], @@ -688,9 +688,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "cpu": [ "riscv64" ], @@ -702,9 +702,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "cpu": [ "s390x" ], @@ -716,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" ], @@ -730,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "cpu": [ "x64" ], @@ -744,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "cpu": [ "arm64" ], @@ -758,9 +758,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "cpu": [ "arm64" ], @@ -772,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "cpu": [ "ia32" ], @@ -786,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "cpu": [ "x64" ], @@ -800,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" ], @@ -1144,9 +1144,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", - "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz", + "integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1154,9 +1154,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1173,13 +1173,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1189,9 +1188,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true, "funding": [ { @@ -1233,9 +1232,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.250", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz", - "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==", + "version": "1.5.264", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.264.tgz", + "integrity": "sha512-1tEf0nLgltC3iy9wtlYDlQDc5Rg9lEKVjEmIHJ21rI9OcqkvD45K1oyNIRA4rR1z3LgJ7KeGzEBojVcV6m4qjA==", "dev": true, "license": "ISC" }, @@ -1709,7 +1708,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1737,7 +1735,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1769,9 +1766,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1785,28 +1782,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" } }, @@ -1825,8 +1822,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -1860,9 +1856,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { @@ -1903,7 +1899,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/package.json b/package.json index cd7b12885..87e7212c3 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.10", - "@tailwindcss/vite": "^4.1.16", + "@tailwindcss/vite": "^4.1.17", "autoprefixer": "^10.4.15", "laravel-vite-plugin": "^1.0.0", - "tailwindcss": "^4.1.16", + "tailwindcss": "^4.1.17", "vite": "^6.4.1" } } diff --git a/public/vendor/livewire/livewire.esm.js b/public/vendor/livewire/livewire.esm.js index e45b0459a..516f2f907 100644 --- a/public/vendor/livewire/livewire.esm.js +++ b/public/vendor/livewire/livewire.esm.js @@ -17,9 +17,9 @@ var __copyProps = (to, from, except, desc) => { }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); -// ../alpine/packages/alpinejs/dist/module.cjs.js +// node_modules/alpinejs/dist/module.cjs.js var require_module_cjs = __commonJS({ - "../alpine/packages/alpinejs/dist/module.cjs.js"(exports, module) { + "node_modules/alpinejs/dist/module.cjs.js"(exports, module) { var __create2 = Object.create; var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; @@ -1290,8 +1290,8 @@ var require_module_cjs = __commonJS({ }); var module_exports = {}; __export(module_exports, { - Alpine: () => src_default, - default: () => module_default + Alpine: () => src_default2, + default: () => module_default2 }); module.exports = __toCommonJS(module_exports); var flushPending = false; @@ -1699,7 +1699,14 @@ var require_module_cjs = __commonJS({ handleError(e, el, expression); } } - function handleError(error2, el, expression = void 0) { + function handleError(...args) { + return errorHandler(...args); + } + var errorHandler = normalErrorHandler; + function setErrorHandler(handler4) { + errorHandler = handler4; + } + function normalErrorHandler(error2, el, expression = void 0) { error2 = Object.assign(error2 != null ? error2 : { message: "No error message given." }, { el, expression }); console.warn(`Alpine Expression Error: ${error2.message} @@ -1737,7 +1744,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } function generateEvaluatorFromFunction(dataStack, func) { return (receiver = () => { - }, { scope: scope2 = {}, params = [] } = {}) => { + }, { scope: scope2 = {}, params = [], context } = {}) => { let result = func.apply(mergeProxies([scope2, ...dataStack]), params); runIfTypeOfFunction(receiver, result); }; @@ -1769,12 +1776,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); function generateEvaluatorFromString(dataStack, expression, el) { let func = generateFunctionFromString(expression, el); return (receiver = () => { - }, { scope: scope2 = {}, params = [] } = {}) => { + }, { scope: scope2 = {}, params = [], context } = {}) => { func.result = void 0; func.finished = false; let completeScope = mergeProxies([scope2, ...dataStack]); if (typeof func === "function") { - let promise = func(func, completeScope).catch((error2) => handleError(error2, el, expression)); + let promise = func.call(context, func, completeScope).catch((error2) => handleError(error2, el, expression)); if (func.finished) { runIfTypeOfFunction(receiver, func.result, completeScope, params, el); func.result = void 0; @@ -2724,10 +2731,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); return el.type === "radio" || el.localName === "ui-radio"; } function debounce2(func, wait) { - var timeout; + let timeout; return function() { - var context = this, args = arguments; - var later = function() { + const context = this, args = arguments; + const later = function() { timeout = null; func.apply(context, args); }; @@ -2876,7 +2883,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); get raw() { return raw; }, - version: "3.14.9", + version: "3.15.2", flushAndStopDeferringMutations, dontAutoEvaluateFunctions, disableEffectScheduling, @@ -2890,6 +2897,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); onlyDuringClone, addRootSelector, addInitSelector, + setErrorHandler, interceptClone, addScopeToNode, deferMutations, @@ -3208,7 +3216,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) { let keyModifiers = modifiers.filter((i) => { - return !["window", "document", "prevent", "stop", "once", "capture", "self", "away", "outside", "passive"].includes(i); + return !["window", "document", "prevent", "stop", "once", "capture", "self", "away", "outside", "passive", "preserve-scroll"].includes(i); }); if (keyModifiers.includes("debounce")) { let debounceIndex = keyModifiers.indexOf("debounce"); @@ -3305,7 +3313,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); el.setAttribute("name", expression); }); } - var event = el.tagName.toLowerCase() === "select" || ["checkbox", "radio"].includes(el.type) || modifiers.includes("lazy") ? "change" : "input"; + let event = el.tagName.toLowerCase() === "select" || ["checkbox", "radio"].includes(el.type) || modifiers.includes("lazy") ? "change" : "input"; let removeListener = isCloning ? () => { } : on3(el, event, modifiers, (e) => { setValue(getInputValue(el, modifiers, e, getValue())); @@ -3826,14 +3834,14 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } alpine_default.setEvaluator(normalEvaluator); alpine_default.setReactivityEngine({ reactive: import_reactivity10.reactive, effect: import_reactivity10.effect, release: import_reactivity10.stop, raw: import_reactivity10.toRaw }); - var src_default = alpine_default; - var module_default = src_default; + var src_default2 = alpine_default; + var module_default2 = src_default2; } }); -// ../alpine/packages/collapse/dist/module.cjs.js +// node_modules/@alpinejs/collapse/dist/module.cjs.js var require_module_cjs2 = __commonJS({ - "../alpine/packages/collapse/dist/module.cjs.js"(exports, module) { + "node_modules/@alpinejs/collapse/dist/module.cjs.js"(exports, module) { var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropNames2 = Object.getOwnPropertyNames; @@ -3853,11 +3861,11 @@ var require_module_cjs2 = __commonJS({ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); var module_exports = {}; __export(module_exports, { - collapse: () => src_default, - default: () => module_default + collapse: () => src_default2, + default: () => module_default2 }); module.exports = __toCommonJS(module_exports); - function src_default(Alpine23) { + function src_default2(Alpine23) { Alpine23.directive("collapse", collapse3); collapse3.inline = (el, { modifiers }) => { if (!modifiers.includes("min")) @@ -3948,13 +3956,13 @@ var require_module_cjs2 = __commonJS({ } return rawValue; } - var module_default = src_default; + var module_default2 = src_default2; } }); -// ../alpine/packages/focus/dist/module.cjs.js +// node_modules/@alpinejs/focus/dist/module.cjs.js var require_module_cjs3 = __commonJS({ - "../alpine/packages/focus/dist/module.cjs.js"(exports, module) { + "node_modules/@alpinejs/focus/dist/module.cjs.js"(exports, module) { var __create2 = Object.create; var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; @@ -4745,13 +4753,13 @@ var require_module_cjs3 = __commonJS({ }); var module_exports = {}; __export(module_exports, { - default: () => module_default, - focus: () => src_default + default: () => module_default2, + focus: () => src_default2 }); module.exports = __toCommonJS(module_exports); var import_focus_trap = __toESM2(require_focus_trap()); var import_tabbable = __toESM2(require_dist()); - function src_default(Alpine23) { + function src_default2(Alpine23) { let lastFocused; let currentFocused; window.addEventListener("focusin", () => { @@ -4870,6 +4878,8 @@ var require_module_cjs3 = __commonJS({ allowOutsideClick: true, fallbackFocus: () => el }; + let undoInert = () => { + }; if (modifiers.includes("noautofocus")) { options.initialFocus = false; } else { @@ -4877,9 +4887,14 @@ var require_module_cjs3 = __commonJS({ if (autofocusEl) options.initialFocus = autofocusEl; } + if (modifiers.includes("inert")) { + options.onPostActivate = () => { + Alpine23.nextTick(() => { + undoInert = setInert(el); + }); + }; + } let trap = (0, import_focus_trap.createFocusTrap)(el, options); - let undoInert = () => { - }; let undoDisableScrolling = () => { }; const releaseFocus = () => { @@ -4899,8 +4914,6 @@ var require_module_cjs3 = __commonJS({ if (value && !oldValue) { if (modifiers.includes("noscroll")) undoDisableScrolling = disableScrolling(); - if (modifiers.includes("inert")) - undoInert = setInert(el); setTimeout(() => { trap.activate(); }, 15); @@ -4950,13 +4963,13 @@ var require_module_cjs3 = __commonJS({ document.documentElement.style.paddingRight = paddingRight; }; } - var module_default = src_default; + var module_default2 = src_default2; } }); -// ../alpine/packages/persist/dist/module.cjs.js +// node_modules/@alpinejs/intersect/dist/module.cjs.js var require_module_cjs4 = __commonJS({ - "../alpine/packages/persist/dist/module.cjs.js"(exports, module) { + "node_modules/@alpinejs/intersect/dist/module.cjs.js"(exports, module) { var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropNames2 = Object.getOwnPropertyNames; @@ -4976,100 +4989,11 @@ var require_module_cjs4 = __commonJS({ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); var module_exports = {}; __export(module_exports, { - default: () => module_default, - persist: () => src_default + default: () => module_default2, + intersect: () => src_default2 }); module.exports = __toCommonJS(module_exports); - function src_default(Alpine23) { - let persist3 = () => { - let alias; - let storage; - try { - storage = localStorage; - } catch (e) { - console.error(e); - console.warn("Alpine: $persist is using temporary storage since localStorage is unavailable."); - let dummy = /* @__PURE__ */ new Map(); - storage = { - getItem: dummy.get.bind(dummy), - setItem: dummy.set.bind(dummy) - }; - } - return Alpine23.interceptor((initialValue, getter, setter, path, key) => { - let lookup = alias || `_x_${path}`; - let initial = storageHas(lookup, storage) ? storageGet(lookup, storage) : initialValue; - setter(initial); - Alpine23.effect(() => { - let value = getter(); - storageSet(lookup, value, storage); - setter(value); - }); - return initial; - }, (func) => { - func.as = (key) => { - alias = key; - return func; - }, func.using = (target) => { - storage = target; - return func; - }; - }); - }; - Object.defineProperty(Alpine23, "$persist", { get: () => persist3() }); - Alpine23.magic("persist", persist3); - Alpine23.persist = (key, { get, set }, storage = localStorage) => { - let initial = storageHas(key, storage) ? storageGet(key, storage) : get(); - set(initial); - Alpine23.effect(() => { - let value = get(); - storageSet(key, value, storage); - set(value); - }); - }; - } - function storageHas(key, storage) { - return storage.getItem(key) !== null; - } - function storageGet(key, storage) { - let value = storage.getItem(key, storage); - if (value === void 0) - return; - return JSON.parse(value); - } - function storageSet(key, value, storage) { - storage.setItem(key, JSON.stringify(value)); - } - var module_default = src_default; - } -}); - -// ../alpine/packages/intersect/dist/module.cjs.js -var require_module_cjs5 = __commonJS({ - "../alpine/packages/intersect/dist/module.cjs.js"(exports, module) { - var __defProp2 = Object.defineProperty; - var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; - var __getOwnPropNames2 = Object.getOwnPropertyNames; - var __hasOwnProp2 = Object.prototype.hasOwnProperty; - var __export = (target, all2) => { - for (var name in all2) - __defProp2(target, name, { get: all2[name], enumerable: true }); - }; - var __copyProps2 = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames2(from)) - if (!__hasOwnProp2.call(to, key) && key !== except) - __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable }); - } - return to; - }; - var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); - var module_exports = {}; - __export(module_exports, { - default: () => module_default, - intersect: () => src_default - }); - module.exports = __toCommonJS(module_exports); - function src_default(Alpine23) { + function src_default2(Alpine23) { Alpine23.directive("intersect", Alpine23.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater, cleanup }) => { let evaluate = evaluateLater(expression); let options = { @@ -5121,12 +5045,12 @@ var require_module_cjs5 = __commonJS({ values = values.filter((v) => v !== void 0); return values.length ? values.join(" ").trim() : fallback2; } - var module_default = src_default; + var module_default2 = src_default2; } }); // node_modules/@alpinejs/resize/dist/module.cjs.js -var require_module_cjs6 = __commonJS({ +var require_module_cjs5 = __commonJS({ "node_modules/@alpinejs/resize/dist/module.cjs.js"(exports, module) { var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; @@ -5147,11 +5071,11 @@ var require_module_cjs6 = __commonJS({ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); var module_exports = {}; __export(module_exports, { - default: () => module_default, - resize: () => src_default + default: () => module_default2, + resize: () => src_default2 }); module.exports = __toCommonJS(module_exports); - function src_default(Alpine23) { + function src_default2(Alpine23) { Alpine23.directive("resize", Alpine23.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater, cleanup }) => { let evaluator = evaluateLater(expression); let evaluate = (width, height) => { @@ -5193,13 +5117,13 @@ var require_module_cjs6 = __commonJS({ } return [width, height]; } - var module_default = src_default; + var module_default2 = src_default2; } }); -// ../alpine/packages/anchor/dist/module.cjs.js -var require_module_cjs7 = __commonJS({ - "../alpine/packages/anchor/dist/module.cjs.js"(exports, module) { +// node_modules/@alpinejs/anchor/dist/module.cjs.js +var require_module_cjs6 = __commonJS({ + "node_modules/@alpinejs/anchor/dist/module.cjs.js"(exports, module) { var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropNames2 = Object.getOwnPropertyNames; @@ -5219,8 +5143,8 @@ var require_module_cjs7 = __commonJS({ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); var module_exports = {}; __export(module_exports, { - anchor: () => src_default, - default: () => module_default + anchor: () => src_default2, + default: () => module_default2 }); module.exports = __toCommonJS(module_exports); var min = Math.min; @@ -6396,7 +6320,7 @@ var require_module_cjs7 = __commonJS({ platform: platformWithCache }); }; - function src_default(Alpine23) { + function src_default2(Alpine23) { Alpine23.magic("anchor", (el) => { if (!el._x_anchor) throw "Alpine: No x-anchor directive found on element using $anchor..."; @@ -6454,7 +6378,7 @@ var require_module_cjs7 = __commonJS({ let unstyled = modifiers.includes("no-style"); return { placement, offsetValue, unstyled }; } - var module_default = src_default; + var module_default2 = src_default2; } }); @@ -6735,9 +6659,9 @@ var require_nprogress = __commonJS({ } }); -// ../alpine/packages/morph/dist/module.cjs.js -var require_module_cjs8 = __commonJS({ - "../alpine/packages/morph/dist/module.cjs.js"(exports, module) { +// node_modules/@alpinejs/morph/dist/module.cjs.js +var require_module_cjs7 = __commonJS({ + "node_modules/@alpinejs/morph/dist/module.cjs.js"(exports, module) { var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropNames2 = Object.getOwnPropertyNames; @@ -6757,119 +6681,161 @@ var require_module_cjs8 = __commonJS({ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); var module_exports = {}; __export(module_exports, { - default: () => module_default, - morph: () => src_default + default: () => module_default2, + morph: () => src_default2 }); module.exports = __toCommonJS(module_exports); function morph3(from, toHtml, options) { monkeyPatchDomSetAttributeToAllowAtSymbols(); - let fromEl; - let toEl; - let key, lookahead, updating, updated, removing, removed, adding, added; - function assignOptions(options2 = {}) { - let defaultGetKey = (el) => el.getAttribute("key"); - let noop = () => { - }; - updating = options2.updating || noop; - updated = options2.updated || noop; - removing = options2.removing || noop; - removed = options2.removed || noop; - adding = options2.adding || noop; - added = options2.added || noop; - key = options2.key || defaultGetKey; - lookahead = options2.lookahead || false; - } - function patch(from2, to) { - if (differentElementNamesTypesOrKeys(from2, to)) { - return swapElements(from2, to); + let context = createMorphContext(options); + let toEl = typeof toHtml === "string" ? createElement(toHtml) : toHtml; + if (window.Alpine && window.Alpine.closestDataStack && !from._x_dataStack) { + toEl._x_dataStack = window.Alpine.closestDataStack(from); + toEl._x_dataStack && window.Alpine.cloneNode(from, toEl); + } + context.patch(from, toEl); + return from; + } + function morphBetween(startMarker, endMarker, toHtml, options = {}) { + monkeyPatchDomSetAttributeToAllowAtSymbols(); + let context = createMorphContext(options); + let fromContainer = startMarker.parentNode; + let fromBlock = new Block(startMarker, endMarker); + let toContainer = typeof toHtml === "string" ? (() => { + let container = document.createElement("div"); + container.insertAdjacentHTML("beforeend", toHtml); + return container; + })() : toHtml; + let toStartMarker = document.createComment("[morph-start]"); + let toEndMarker = document.createComment("[morph-end]"); + toContainer.insertBefore(toStartMarker, toContainer.firstChild); + toContainer.appendChild(toEndMarker); + let toBlock = new Block(toStartMarker, toEndMarker); + if (window.Alpine && window.Alpine.closestDataStack) { + toContainer._x_dataStack = window.Alpine.closestDataStack(fromContainer); + toContainer._x_dataStack && window.Alpine.cloneNode(fromContainer, toContainer); + } + context.patchChildren(fromBlock, toBlock); + } + function createMorphContext(options = {}) { + let defaultGetKey = (el) => el.getAttribute("key"); + let noop = () => { + }; + let context = { + key: options.key || defaultGetKey, + lookahead: options.lookahead || false, + updating: options.updating || noop, + updated: options.updated || noop, + removing: options.removing || noop, + removed: options.removed || noop, + adding: options.adding || noop, + added: options.added || noop + }; + context.patch = function(from, to) { + if (context.differentElementNamesTypesOrKeys(from, to)) { + return context.swapElements(from, to); } let updateChildrenOnly = false; let skipChildren = false; - if (shouldSkipChildren(updating, () => skipChildren = true, from2, to, () => updateChildrenOnly = true)) + let skipUntil = (predicate) => context.skipUntilCondition = predicate; + if (shouldSkipChildren(context.updating, () => skipChildren = true, skipUntil, from, to, () => updateChildrenOnly = true)) return; - if (from2.nodeType === 1 && window.Alpine) { - window.Alpine.cloneNode(from2, to); - if (from2._x_teleport && to._x_teleport) { - patch(from2._x_teleport, to._x_teleport); + if (from.nodeType === 1 && window.Alpine) { + window.Alpine.cloneNode(from, to); + if (from._x_teleport && to._x_teleport) { + context.patch(from._x_teleport, to._x_teleport); } } if (textOrComment(to)) { - patchNodeValue(from2, to); - updated(from2, to); + context.patchNodeValue(from, to); + context.updated(from, to); return; } if (!updateChildrenOnly) { - patchAttributes(from2, to); + context.patchAttributes(from, to); } - updated(from2, to); + context.updated(from, to); if (!skipChildren) { - patchChildren(from2, to); + context.patchChildren(from, to); } - } - function differentElementNamesTypesOrKeys(from2, to) { - return from2.nodeType != to.nodeType || from2.nodeName != to.nodeName || getKey(from2) != getKey(to); - } - function swapElements(from2, to) { - if (shouldSkip(removing, from2)) + }; + context.differentElementNamesTypesOrKeys = function(from, to) { + return from.nodeType != to.nodeType || from.nodeName != to.nodeName || context.getKey(from) != context.getKey(to); + }; + context.swapElements = function(from, to) { + if (shouldSkip(context.removing, from)) return; let toCloned = to.cloneNode(true); - if (shouldSkip(adding, toCloned)) + if (shouldSkip(context.adding, toCloned)) return; - from2.replaceWith(toCloned); - removed(from2); - added(toCloned); - } - function patchNodeValue(from2, to) { + from.replaceWith(toCloned); + context.removed(from); + context.added(toCloned); + }; + context.patchNodeValue = function(from, to) { let value = to.nodeValue; - if (from2.nodeValue !== value) { - from2.nodeValue = value; + if (from.nodeValue !== value) { + from.nodeValue = value; } - } - function patchAttributes(from2, to) { - if (from2._x_transitioning) + }; + context.patchAttributes = function(from, to) { + if (from._x_transitioning) return; - if (from2._x_isShown && !to._x_isShown) { + if (from._x_isShown && !to._x_isShown) { return; } - if (!from2._x_isShown && to._x_isShown) { + if (!from._x_isShown && to._x_isShown) { return; } - let domAttributes = Array.from(from2.attributes); + let domAttributes = Array.from(from.attributes); let toAttributes = Array.from(to.attributes); for (let i = domAttributes.length - 1; i >= 0; i--) { let name = domAttributes[i].name; if (!to.hasAttribute(name)) { - from2.removeAttribute(name); + from.removeAttribute(name); } } for (let i = toAttributes.length - 1; i >= 0; i--) { let name = toAttributes[i].name; let value = toAttributes[i].value; - if (from2.getAttribute(name) !== value) { - from2.setAttribute(name, value); + if (from.getAttribute(name) !== value) { + from.setAttribute(name, value); } } - } - function patchChildren(from2, to) { - let fromKeys = keyToMap(from2.children); + }; + context.patchChildren = function(from, to) { + let fromKeys = context.keyToMap(from.children); let fromKeyHoldovers = {}; let currentTo = getFirstNode(to); - let currentFrom = getFirstNode(from2); + let currentFrom = getFirstNode(from); while (currentTo) { seedingMatchingId(currentTo, currentFrom); - let toKey = getKey(currentTo); - let fromKey = getKey(currentFrom); + let toKey = context.getKey(currentTo); + let fromKey = context.getKey(currentFrom); + if (context.skipUntilCondition) { + let fromDone = !currentFrom || context.skipUntilCondition(currentFrom); + let toDone = !currentTo || context.skipUntilCondition(currentTo); + if (fromDone && toDone) { + context.skipUntilCondition = null; + } else { + if (!fromDone) + currentFrom = currentFrom && getNextSibling(from, currentFrom); + if (!toDone) + currentTo = currentTo && getNextSibling(to, currentTo); + continue; + } + } if (!currentFrom) { if (toKey && fromKeyHoldovers[toKey]) { let holdover = fromKeyHoldovers[toKey]; - from2.appendChild(holdover); + from.appendChild(holdover); currentFrom = holdover; - fromKey = getKey(currentFrom); + fromKey = context.getKey(currentFrom); } else { - if (!shouldSkip(adding, currentTo)) { + if (!shouldSkip(context.adding, currentTo)) { let clone = currentTo.cloneNode(true); - from2.appendChild(clone); - added(clone); + from.appendChild(clone); + context.added(clone); } currentTo = getNextSibling(to, currentTo); continue; @@ -6881,7 +6847,7 @@ var require_module_cjs8 = __commonJS({ let nestedIfCount = 0; let fromBlockStart = currentFrom; while (currentFrom) { - let next = getNextSibling(from2, currentFrom); + let next = getNextSibling(from, currentFrom); if (isIf(next)) { nestedIfCount++; } else if (isEnd(next) && nestedIfCount > 0) { @@ -6910,17 +6876,17 @@ var require_module_cjs8 = __commonJS({ let toBlockEnd = currentTo; let fromBlock = new Block(fromBlockStart, fromBlockEnd); let toBlock = new Block(toBlockStart, toBlockEnd); - patchChildren(fromBlock, toBlock); + context.patchChildren(fromBlock, toBlock); continue; } - if (currentFrom.nodeType === 1 && lookahead && !currentFrom.isEqualNode(currentTo)) { + if (currentFrom.nodeType === 1 && context.lookahead && !currentFrom.isEqualNode(currentTo)) { let nextToElementSibling = getNextSibling(to, currentTo); let found = false; while (!found && nextToElementSibling) { if (nextToElementSibling.nodeType === 1 && currentFrom.isEqualNode(nextToElementSibling)) { found = true; - currentFrom = addNodeBefore(from2, currentTo, currentFrom); - fromKey = getKey(currentFrom); + currentFrom = context.addNodeBefore(from, currentTo, currentFrom); + fromKey = context.getKey(currentFrom); } nextToElementSibling = getNextSibling(to, nextToElementSibling); } @@ -6928,9 +6894,9 @@ var require_module_cjs8 = __commonJS({ if (toKey !== fromKey) { if (!toKey && fromKey) { fromKeyHoldovers[fromKey] = currentFrom; - currentFrom = addNodeBefore(from2, currentTo, currentFrom); + currentFrom = context.addNodeBefore(from, currentTo, currentFrom); fromKeyHoldovers[fromKey].remove(); - currentFrom = getNextSibling(from2, currentFrom); + currentFrom = getNextSibling(from, currentFrom); currentTo = getNextSibling(to, currentTo); continue; } @@ -6938,7 +6904,7 @@ var require_module_cjs8 = __commonJS({ if (fromKeys[toKey]) { currentFrom.replaceWith(fromKeys[toKey]); currentFrom = fromKeys[toKey]; - fromKey = getKey(currentFrom); + fromKey = context.getKey(currentFrom); } } if (toKey && fromKey) { @@ -6947,67 +6913,57 @@ var require_module_cjs8 = __commonJS({ fromKeyHoldovers[fromKey] = currentFrom; currentFrom.replaceWith(fromKeyNode); currentFrom = fromKeyNode; - fromKey = getKey(currentFrom); + fromKey = context.getKey(currentFrom); } else { fromKeyHoldovers[fromKey] = currentFrom; - currentFrom = addNodeBefore(from2, currentTo, currentFrom); + currentFrom = context.addNodeBefore(from, currentTo, currentFrom); fromKeyHoldovers[fromKey].remove(); - currentFrom = getNextSibling(from2, currentFrom); + currentFrom = getNextSibling(from, currentFrom); currentTo = getNextSibling(to, currentTo); continue; } } } - let currentFromNext = currentFrom && getNextSibling(from2, currentFrom); - patch(currentFrom, currentTo); + let currentFromNext = currentFrom && getNextSibling(from, currentFrom); + context.patch(currentFrom, currentTo); currentTo = currentTo && getNextSibling(to, currentTo); currentFrom = currentFromNext; } let removals = []; while (currentFrom) { - if (!shouldSkip(removing, currentFrom)) + if (!shouldSkip(context.removing, currentFrom)) removals.push(currentFrom); - currentFrom = getNextSibling(from2, currentFrom); + currentFrom = getNextSibling(from, currentFrom); } while (removals.length) { let domForRemoval = removals.shift(); domForRemoval.remove(); - removed(domForRemoval); + context.removed(domForRemoval); } - } - function getKey(el) { - return el && el.nodeType === 1 && key(el); - } - function keyToMap(els2) { + }; + context.getKey = function(el) { + return el && el.nodeType === 1 && context.key(el); + }; + context.keyToMap = function(els2) { let map = {}; for (let el of els2) { - let theKey = getKey(el); + let theKey = context.getKey(el); if (theKey) { map[theKey] = el; } } return map; - } - function addNodeBefore(parent, node, beforeMe) { - if (!shouldSkip(adding, node)) { + }; + context.addNodeBefore = function(parent, node, beforeMe) { + if (!shouldSkip(context.adding, node)) { let clone = node.cloneNode(true); parent.insertBefore(clone, beforeMe); - added(clone); + context.added(clone); return clone; } return node; - } - assignOptions(options); - fromEl = from; - toEl = typeof toHtml === "string" ? createElement(toHtml) : toHtml; - if (window.Alpine && window.Alpine.closestDataStack && !from._x_dataStack) { - toEl._x_dataStack = window.Alpine.closestDataStack(from); - toEl._x_dataStack && window.Alpine.cloneNode(from, toEl); - } - patch(from, toEl); - fromEl = void 0; - toEl = void 0; - return from; + }; + return context; } morph3.step = () => { }; @@ -7018,9 +6974,9 @@ var require_module_cjs8 = __commonJS({ hook(...args, () => skip = true); return skip; } - function shouldSkipChildren(hook, skipChildren, ...args) { + function shouldSkipChildren(hook, skipChildren, skipUntil, ...args) { let skip = false; - hook(...args, () => skip = true, skipChildren); + hook(...args, () => skip = true, skipChildren, skipUntil); return skip; } var patched = false; @@ -7103,16 +7059,17 @@ var require_module_cjs8 = __commonJS({ to.setAttribute("id", fromId); to.id = fromId; } - function src_default(Alpine23) { + function src_default2(Alpine23) { Alpine23.morph = morph3; + Alpine23.morphBetween = morphBetween; } - var module_default = src_default; + var module_default2 = src_default2; } }); -// ../alpine/packages/mask/dist/module.cjs.js -var require_module_cjs9 = __commonJS({ - "../alpine/packages/mask/dist/module.cjs.js"(exports, module) { +// node_modules/@alpinejs/mask/dist/module.cjs.js +var require_module_cjs8 = __commonJS({ + "node_modules/@alpinejs/mask/dist/module.cjs.js"(exports, module) { var __defProp2 = Object.defineProperty; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropNames2 = Object.getOwnPropertyNames; @@ -7132,12 +7089,12 @@ var require_module_cjs9 = __commonJS({ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod); var module_exports = {}; __export(module_exports, { - default: () => module_default, - mask: () => src_default, + default: () => module_default2, + mask: () => src_default2, stripDown: () => stripDown }); module.exports = __toCommonJS(module_exports); - function src_default(Alpine23) { + function src_default2(Alpine23) { Alpine23.directive("mask", (el, { value, expression }, { effect, evaluateLater, cleanup }) => { let templateFn = () => expression; let lastInputValue = ""; @@ -7304,7 +7261,7 @@ var require_module_cjs9 = __commonJS({ }); return template; } - var module_default = src_default; + var module_default2 = src_default2; } }); @@ -7498,7 +7455,8 @@ function handleFileUpload(el, property, component, cleanup) { return; start2(); if (e.target.multiple) { - manager.uploadMultiple(property, e.target.files, finish, error2, progress, cancel); + let append = ["ui-file-upload"].includes(e.target.tagName.toLowerCase()); + manager.uploadMultiple(property, e.target.files, finish, error2, progress, cancel, append); } else { manager.upload(property, e.target.files[0], finish, error2, progress, cancel); } @@ -7550,17 +7508,19 @@ var UploadManager = class { finishCallback, errorCallback, progressCallback, - cancelledCallback + cancelledCallback, + append: false }); } - uploadMultiple(name, files, finishCallback, errorCallback, progressCallback, cancelledCallback) { + uploadMultiple(name, files, finishCallback, errorCallback, progressCallback, cancelledCallback, append = false) { this.setUpload(name, { files: Array.from(files), multiple: true, finishCallback, errorCallback, progressCallback, - cancelledCallback + cancelledCallback, + append }); } removeUpload(name, tmpFilename, finishCallback) { @@ -7613,7 +7573,7 @@ var UploadManager = class { request.addEventListener("load", () => { if ((request.status + "")[0] === "2") { let paths = retrievePaths(request.response && JSON.parse(request.response)); - this.component.$wire.call("_finishUpload", name, paths, this.uploadBag.first(name).multiple); + this.component.$wire.call("_finishUpload", name, paths, this.uploadBag.first(name).multiple, this.uploadBag.first(name).append); return; } let errors = null; @@ -7710,9 +7670,9 @@ function uploadMultiple(component, name, files, finishCallback = () => { }, errorCallback = () => { }, progressCallback = () => { }, cancelledCallback = () => { -}) { +}, append = false) { let uploadManager = getUploadManager(component); - uploadManager.uploadMultiple(name, files, finishCallback, errorCallback, progressCallback, cancelledCallback); + uploadManager.uploadMultiple(name, files, finishCallback, errorCallback, progressCallback, cancelledCallback, append); } function removeUpload(component, name, tmpFilename, finishCallback = () => { }, errorCallback = () => { @@ -7828,14 +7788,13 @@ function showHtmlModal(html) { if (typeof modal != "undefined" && modal != null) { modal.innerHTML = ""; } else { - modal = document.createElement("div"); + modal = document.createElement("dialog"); modal.id = "livewire-error"; - modal.style.position = "fixed"; - modal.style.width = "100vw"; - modal.style.height = "100vh"; - modal.style.padding = "50px"; - modal.style.backgroundColor = "rgba(0, 0, 0, .6)"; - modal.style.zIndex = 2e5; + modal.style.margin = "50px"; + modal.style.width = "calc(100% - 100px)"; + modal.style.height = "calc(100% - 100px)"; + modal.style.borderRadius = "5px"; + modal.style.padding = "0px"; } let iframe = document.createElement("iframe"); iframe.style.backgroundColor = "#17161A"; @@ -7849,14 +7808,15 @@ function showHtmlModal(html) { iframe.contentWindow.document.write(page.outerHTML); iframe.contentWindow.document.close(); modal.addEventListener("click", () => hideHtmlModal(modal)); - modal.setAttribute("tabindex", 0); - modal.addEventListener("keydown", (e) => { - if (e.key === "Escape") - hideHtmlModal(modal); - }); + modal.addEventListener("close", () => cleanupModal(modal)); + modal.showModal(); modal.focus(); + modal.blur(); } function hideHtmlModal(modal) { + modal.close(); +} +function cleanupModal(modal) { modal.outerHTML = ""; document.body.style.overflow = "visible"; } @@ -7935,11 +7895,18 @@ var Commit = class { prepare() { trigger("commit.prepare", { component: this.component }); } + getEncodedSnapshotWithLatestChildrenMergedIn() { + let { snapshotEncoded, children, snapshot } = this.component; + let childIds = children.map((child) => child.id); + let filteredChildren = Object.fromEntries(Object.entries(snapshot.memo.children).filter(([key, value]) => childIds.includes(value[1]))); + return snapshotEncoded.replace(/"children":\{[^}]*\}/, `"children":${JSON.stringify(filteredChildren)}`); + } toRequestPayload() { let propertiesDiff = diff(this.component.canonical, this.component.ephemeral); let updates = this.component.mergeQueuedUpdates(propertiesDiff); + let snapshotEncoded = this.getEncodedSnapshotWithLatestChildrenMergedIn(); let payload = { - snapshot: this.component.snapshotEncoded, + snapshot: snapshotEncoded, updates, calls: this.calls.map((i) => ({ path: i.path, @@ -8028,7 +7995,6 @@ var CommitBus = class { createAndSendNewPool() { trigger("commit.pooling", { commits: this.commits }); let pools = this.corraleCommitsIntoPools(); - this.commits.clear(); trigger("commit.pooled", { pools }); pools.forEach((pool) => { if (pool.empty()) @@ -8036,13 +8002,17 @@ var CommitBus = class { this.pools.add(pool); pool.send().then(() => { this.pools.delete(pool); - this.sendAnyQueuedCommits(); + queueMicrotask(() => { + this.sendAnyQueuedCommits(); + }); }); }); } corraleCommitsIntoPools() { let pools = /* @__PURE__ */ new Set(); for (let [idx, commit] of this.commits.entries()) { + if (this.findPoolWithComponent(commit.component)) + continue; let hasFoundPool = false; pools.forEach((pool) => { if (pool.shouldHoldCommit(commit)) { @@ -8055,6 +8025,7 @@ var CommitBus = class { newPool.add(commit); pools.add(newPool); } + this.commits.delete(commit); } return pools; } @@ -8430,7 +8401,7 @@ var Component = class { get children() { let meta = this.snapshot.memo; let childIds = Object.values(meta.children).map((i) => i[1]); - return childIds.map((id) => findComponent(id)); + return childIds.filter((id) => hasComponent(id)).map((id) => findComponent(id)); } get parent() { return closestComponent(this.el.parentElement); @@ -8488,6 +8459,9 @@ function destroyComponent(id) { component.cleanup(); delete components[id]; } +function hasComponent(id) { + return !!components[id]; +} function findComponent(id) { let component = components[id]; if (!component) @@ -8555,6 +8529,9 @@ function on2(eventName, callback) { }; } function dispatchEvent(target, name, params, bubbles = true) { + if (typeof params === "string") { + params = [params]; + } let e = new CustomEvent(name, { bubbles, detail: params }); e.__livewire = { name, params, receivedBy: [] }; target.dispatchEvent(e); @@ -8632,38 +8609,172 @@ var Directive = class { this.expression = this.el.getAttribute(this.rawName); } get method() { - const { method } = this.parseOutMethodAndParams(this.expression); - return method; + const methods = this.parseOutMethodsAndParams(this.expression); + return methods[0].method; + } + get methods() { + return this.parseOutMethodsAndParams(this.expression); } get params() { - const { params } = this.parseOutMethodAndParams(this.expression); - return params; - } - parseOutMethodAndParams(rawMethod) { - let method = rawMethod; - let params = []; - const methodAndParamString = method.match(/(.*?)\((.*)\)/s); - if (methodAndParamString) { - method = methodAndParamString[1]; - let func = new Function("$event", `return (function () { - for (var l=arguments.length, p=new Array(l), k=0; k 0) { + let argumentsToArray = function() { + for (var l = arguments.length, p = new Array(l), k = 0; k < l; k++) { + p[k] = arguments[k]; + } + return [].concat(p); + }; + try { + params = Alpine.evaluate(document, "argumentsToArray(" + paramString + ")", { + scope: { argumentsToArray } + }); + } catch (error2) { + console.warn("Failed to parse parameters:", paramString, error2); + params = []; + } + } + methods.push({ method, params }); + } + return methods; + } + splitAndParseMethods(methodExpression) { + let methods = []; + let current = ""; + let parenCount = 0; + let inString = false; + let stringChar = null; + let trimmedExpression = methodExpression.trim(); + for (let i = 0; i < trimmedExpression.length; i++) { + let char = trimmedExpression[i]; + if (!inString) { + if (char === '"' || char === "'") { + inString = true; + stringChar = char; + current += char; + } else if (char === "(") { + parenCount++; + current += char; + } else if (char === ")") { + parenCount--; + current += char; + } else if (char === "," && parenCount === 0) { + methods.push(this.parseMethodCall(current.trim())); + current = ""; + } else { + current += char; + } + } else { + if (char === stringChar && trimmedExpression[i - 1] !== "\\") { + inString = false; + stringChar = null; + } + current += char; + } + } + if (current.trim().length > 0) { + methods.push(this.parseMethodCall(current.trim())); } - return { method, params }; + return methods; + } + parseMethodCall(methodString) { + let methodMatch = methodString.match(/^([^(]+)\(/); + if (!methodMatch) { + return { + method: methodString.trim(), + paramString: "" + }; + } + let method = methodMatch[1].trim(); + let paramStart = methodMatch[0].length - 1; + let lastParenIndex = methodString.lastIndexOf(")"); + if (lastParenIndex === -1) { + throw new Error(`Missing closing parenthesis for method "${method}"`); + } + let paramString = methodString.slice(paramStart + 1, lastParenIndex).trim(); + return { + method, + paramString + }; } }; // js/lifecycle.js var import_collapse = __toESM(require_module_cjs2()); var import_focus = __toESM(require_module_cjs3()); -var import_persist2 = __toESM(require_module_cjs4()); -var import_intersect = __toESM(require_module_cjs5()); -var import_resize = __toESM(require_module_cjs6()); -var import_anchor = __toESM(require_module_cjs7()); + +// node_modules/@alpinejs/persist/dist/module.esm.js +function src_default(Alpine23) { + let persist = () => { + let alias; + let storage; + try { + storage = localStorage; + } catch (e) { + console.error(e); + console.warn("Alpine: $persist is using temporary storage since localStorage is unavailable."); + let dummy = /* @__PURE__ */ new Map(); + storage = { + getItem: dummy.get.bind(dummy), + setItem: dummy.set.bind(dummy) + }; + } + return Alpine23.interceptor((initialValue, getter, setter, path, key) => { + let lookup = alias || `_x_${path}`; + let initial = storageHas(lookup, storage) ? storageGet(lookup, storage) : initialValue; + setter(initial); + Alpine23.effect(() => { + let value = getter(); + storageSet(lookup, value, storage); + setter(value); + }); + return initial; + }, (func) => { + func.as = (key) => { + alias = key; + return func; + }, func.using = (target) => { + storage = target; + return func; + }; + }); + }; + Object.defineProperty(Alpine23, "$persist", { get: () => persist() }); + Alpine23.magic("persist", persist); + Alpine23.persist = (key, { get, set }, storage = localStorage) => { + let initial = storageHas(key, storage) ? storageGet(key, storage) : get(); + set(initial); + Alpine23.effect(() => { + let value = get(); + storageSet(key, value, storage); + set(value); + }); + }; +} +function storageHas(key, storage) { + return storage.getItem(key) !== null; +} +function storageGet(key, storage) { + let value = storage.getItem(key); + if (value === void 0) + return; + return JSON.parse(value); +} +function storageSet(key, value, storage) { + storage.setItem(key, JSON.stringify(value)); +} +var module_default = src_default; + +// js/lifecycle.js +var import_intersect = __toESM(require_module_cjs4()); +var import_resize = __toESM(require_module_cjs5()); +var import_anchor = __toESM(require_module_cjs6()); // js/plugins/navigate/history.js var Snapshot = class { @@ -8852,12 +8963,12 @@ function performFetch(uri, callback) { // js/plugins/navigate/prefetch.js var prefetches = {}; +var cacheDuration = 3e4; function prefetchHtml(destination, callback) { let uri = getUriStringFromUrlObject(destination); if (prefetches[uri]) return; - prefetches[uri] = { finished: false, html: null, whenFinished: () => { - } }; + prefetches[uri] = { finished: false, html: null, whenFinished: () => setTimeout(() => delete prefetches[uri], cacheDuration) }; performFetch(uri, (html, routedUri) => { callback(html, routedUri); }); @@ -9137,6 +9248,7 @@ function isPopoverSupported() { var oldBodyScriptTagHashes = []; var attributesExemptFromScriptTagHashing = [ "data-csrf", + "nonce", "aria-hidden" ]; function swapCurrentPageWithNewHtml(html, andThen) { @@ -9288,7 +9400,8 @@ var showProgressBar = true; var restoreScroll = true; var autofocus = false; function navigate_default(Alpine23) { - Alpine23.navigate = (url) => { + Alpine23.navigate = (url, options = {}) => { + let { preserveScroll = false } = options; let destination = createUrlObjectFromString(url); let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", { url: destination, @@ -9297,7 +9410,7 @@ function navigate_default(Alpine23) { }); if (prevented) return; - navigateTo(destination); + navigateTo(destination, { preserveScroll }); }; Alpine23.navigate.disableProgressBar = () => { showProgressBar = false; @@ -9305,6 +9418,7 @@ function navigate_default(Alpine23) { Alpine23.addInitSelector(() => `[${Alpine23.prefixed("navigate")}]`); Alpine23.directive("navigate", (el, { modifiers }) => { let shouldPrefetchOnHover = modifiers.includes("hover"); + let preserveScroll = modifiers.includes("preserve-scroll"); shouldPrefetchOnHover && whenThisLinkIsHoveredFor(el, 60, () => { let destination = extractDestinationFromLink(el); if (!destination) @@ -9328,16 +9442,15 @@ function navigate_default(Alpine23) { }); if (prevented) return; - navigateTo(destination); + navigateTo(destination, { preserveScroll }); }); }); }); - function navigateTo(destination, shouldPushToHistoryState = true) { + function navigateTo(destination, { preserveScroll = false, shouldPushToHistoryState = true }) { showProgressBar && showAndStartProgressBar(); fetchHtmlOrUsePrefetchedHtml(destination, (html, finalDestination) => { fireEventForOtherLibrariesToHookInto("alpine:navigating"); restoreScroll && storeScrollInformationInHtmlBeforeNavigatingAway(); - showProgressBar && finishAndHideProgressBar(); cleanupAlpineElementsOnThePageThatArentInsideAPersistedElement(); updateCurrentPageHtmlInHistoryStateForLaterBackButtonClicks(); preventAlpineFromPickingUpDomChanges(Alpine23, (andAfterAllThis) => { @@ -9356,7 +9469,7 @@ function navigate_default(Alpine23) { unPackPersistedTeleports(persistedEl); unPackPersistedPopovers(persistedEl); }); - restoreScrollPositionOrScrollToTop(); + !preserveScroll && restoreScrollPositionOrScrollToTop(); afterNewScriptsAreDoneLoading(() => { andAfterAllThis(() => { setTimeout(() => { @@ -9364,6 +9477,7 @@ function navigate_default(Alpine23) { }); nowInitializeAlpineOnTheNewPage(Alpine23); fireEventForOtherLibrariesToHookInto("alpine:navigated"); + showProgressBar && finishAndHideProgressBar(); }); }); }); @@ -9380,8 +9494,7 @@ function navigate_default(Alpine23) { }); if (prevented) return; - let shouldPushToHistoryState = false; - navigateTo(destination, shouldPushToHistoryState); + navigateTo(destination, { shouldPushToHistoryState: false }); }); }, (html, url, currentPageUrl, currentPageKey) => { let destination = createUrlObjectFromString(url); @@ -9679,8 +9792,8 @@ function fromQueryString(search, queryKey) { } // js/lifecycle.js -var import_morph = __toESM(require_module_cjs8()); -var import_mask = __toESM(require_module_cjs9()); +var import_morph = __toESM(require_module_cjs7()); +var import_mask = __toESM(require_module_cjs8()); var import_alpinejs5 = __toESM(require_module_cjs()); function start() { setTimeout(() => ensureLivewireScriptIsntMisplaced()); @@ -9693,7 +9806,7 @@ function start() { import_alpinejs5.default.plugin(import_collapse.default); import_alpinejs5.default.plugin(import_anchor.default); import_alpinejs5.default.plugin(import_focus.default); - import_alpinejs5.default.plugin(import_persist2.default); + import_alpinejs5.default.plugin(module_default); import_alpinejs5.default.plugin(navigate_default); import_alpinejs5.default.plugin(import_mask.default); import_alpinejs5.default.addRootSelector(() => "[wire\\:id]"); @@ -9715,7 +9828,7 @@ function start() { import_alpinejs5.default.interceptInit(import_alpinejs5.default.skipDuringClone((el) => { if (!Array.from(el.attributes).some((attribute) => matchesForLivewireDirective(attribute.name))) return; - if (el.hasAttribute("wire:id")) { + if (el.hasAttribute("wire:id") && !el.__livewire && !hasComponent(el.getAttribute("wire:id"))) { let component2 = initComponent(el); import_alpinejs5.default.onAttributeRemoved(el, "wire:id", () => { destroyComponent(component2.id); @@ -9736,6 +9849,15 @@ function start() { } }); }); } + }, (el) => { + if (!Array.from(el.attributes).some((attribute) => matchesForLivewireDirective(attribute.name))) + return; + let directives = Array.from(el.getAttributeNames()).filter((name) => matchesForLivewireDirective(name)).map((name) => extractDirective(el, name)); + directives.forEach((directive2) => { + trigger("directive.global.init", { el, directive: directive2, cleanup: (callback) => { + import_alpinejs5.default.onAttributeRemoved(el, directive2.raw, callback); + } }); + }); })); import_alpinejs5.default.start(); setTimeout(() => window.Livewire.initialRenderIsFinished = true); @@ -9908,6 +10030,8 @@ on("effect", ({ component, effects }) => { var import_alpinejs8 = __toESM(require_module_cjs()); function morph2(component, el, html) { let wrapperTag = el.parentElement ? el.parentElement.tagName.toLowerCase() : "div"; + let customElement = customElements.get(wrapperTag); + wrapperTag = customElement ? customElement.name : wrapperTag; let wrapper = document.createElement(wrapperTag); wrapper.innerHTML = html; let parentComponent; @@ -9917,8 +10041,25 @@ function morph2(component, el, html) { } parentComponent && (wrapper.__livewire = parentComponent); let to = wrapper.firstElementChild; + to.setAttribute("wire:snapshot", component.snapshotEncoded); + let effects = { ...component.effects }; + delete effects.html; + to.setAttribute("wire:effects", JSON.stringify(effects)); to.__livewire = component; trigger("morph", { el, toEl: to, component }); + let existingComponentsMap = {}; + el.querySelectorAll("[wire\\:id]").forEach((component2) => { + existingComponentsMap[component2.getAttribute("wire:id")] = component2; + }); + to.querySelectorAll("[wire\\:id]").forEach((child) => { + if (child.hasAttribute("wire:snapshot")) + return; + let wireId = child.getAttribute("wire:id"); + let existingComponent = existingComponentsMap[wireId]; + if (existingComponent) { + child.replaceWith(existingComponent.cloneNode(true)); + } + }); import_alpinejs8.default.morph(el, to, { updating: (el2, toEl, childrenOnly, skip, skipChildren) => { if (isntElement(el2)) @@ -9995,7 +10136,13 @@ on("effect", ({ component, effects }) => { // js/features/supportDispatches.js on("effect", ({ component, effects }) => { - dispatchEvents(component, effects.dispatches || []); + queueMicrotask(() => { + queueMicrotask(() => { + queueMicrotask(() => { + dispatchEvents(component, effects.dispatches || []); + }); + }); + }); }); function dispatchEvents(component, dispatches) { dispatches.forEach(({ name, params = {}, self: self2 = false, to }) => { @@ -10451,11 +10598,20 @@ on("directive.init", ({ el, directive: directive2, cleanup, component }) => { var import_alpinejs13 = __toESM(require_module_cjs()); import_alpinejs13.default.addInitSelector(() => `[wire\\:navigate]`); import_alpinejs13.default.addInitSelector(() => `[wire\\:navigate\\.hover]`); +import_alpinejs13.default.addInitSelector(() => `[wire\\:navigate\\.preserve-scroll]`); +import_alpinejs13.default.addInitSelector(() => `[wire\\:navigate\\.preserve-scroll\\.hover]`); +import_alpinejs13.default.addInitSelector(() => `[wire\\:navigate\\.hover\\.preserve-scroll]`); import_alpinejs13.default.interceptInit(import_alpinejs13.default.skipDuringClone((el) => { if (el.hasAttribute("wire:navigate")) { import_alpinejs13.default.bind(el, { ["x-navigate"]: true }); } else if (el.hasAttribute("wire:navigate.hover")) { import_alpinejs13.default.bind(el, { ["x-navigate.hover"]: true }); + } else if (el.hasAttribute("wire:navigate.preserve-scroll")) { + import_alpinejs13.default.bind(el, { ["x-navigate.preserve-scroll"]: true }); + } else if (el.hasAttribute("wire:navigate.preserve-scroll.hover")) { + import_alpinejs13.default.bind(el, { ["x-navigate.preserve-scroll.hover"]: true }); + } else if (el.hasAttribute("wire:navigate.hover.preserve-scroll")) { + import_alpinejs13.default.bind(el, { ["x-navigate.hover.preserve-scroll"]: true }); } })); document.addEventListener("alpine:navigating", () => { @@ -10708,18 +10864,14 @@ function getTargets(el) { let inverted = false; if (directives.has("target")) { let directive2 = directives.get("target"); - let raw = directive2.expression; if (directive2.modifiers.includes("except")) inverted = true; - if (raw.includes("(") && raw.includes(")")) { - targets.push({ target: directive2.method, params: quickHash(JSON.stringify(directive2.params)) }); - } else if (raw.includes(",")) { - raw.split(",").map((i) => i.trim()).forEach((target) => { - targets.push({ target }); + directive2.methods.forEach(({ method, params }) => { + targets.push({ + target: method, + params: params && params.length > 0 ? quickHash(JSON.stringify(params)) : void 0 }); - } else { - targets.push({ target: raw }); - } + }); } else { let nonActionOrModelLivewireDirectives = ["init", "dirty", "offline", "target", "loading", "poll", "ignore", "key", "id"]; directives.all().filter((i) => !nonActionOrModelLivewireDirectives.includes(i.value)).map((i) => i.expression.split("(")[0]).forEach((target) => targets.push({ target })); @@ -10739,7 +10891,7 @@ directive("stream", ({ el, directive: directive2, cleanup }) => { if (modifiers.includes("replace") || replace2) { el.innerHTML = content; } else { - el.innerHTML = el.innerHTML + content; + el.insertAdjacentHTML("beforeend", content); } }); cleanup(off); @@ -10888,7 +11040,7 @@ directive("model", ({ el, directive: directive2, component, cleanup }) => { let onBlur = modifiers.includes("blur"); let isDebounced = modifiers.includes("debounce"); let update = expression.startsWith("$parent") ? () => component.$wire.$parent.$commit() : () => component.$wire.$commit(); - let debouncedUpdate = isTextInput(el) && !isDebounced && isLive ? debounce(update, 150) : update; + let debouncedUpdate = isRealtimeInput(el) && !isDebounced && isLive ? debounce(update, 150) : update; import_alpinejs16.default.bind(el, { ["@change"]() { isLazy && update(); @@ -10918,8 +11070,8 @@ function getModifierTail(modifiers) { return ""; return "." + modifiers.join("."); } -function isTextInput(el) { - return ["INPUT", "TEXTAREA"].includes(el.tagName.toUpperCase()) && !["checkbox", "radio"].includes(el.type); +function isRealtimeInput(el) { + return ["INPUT", "TEXTAREA"].includes(el.tagName.toUpperCase()) && !["checkbox", "radio"].includes(el.type) || el.tagName.toUpperCase() === "UI-SLIDER"; } function componentIsMissingProperty(component, property) { if (property.startsWith("$parent")) { diff --git a/public/vendor/livewire/livewire.esm.js.map b/public/vendor/livewire/livewire.esm.js.map index 939cd00df..ac70faba6 100644 --- a/public/vendor/livewire/livewire.esm.js.map +++ b/public/vendor/livewire/livewire.esm.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../../alpine/packages/alpinejs/dist/module.cjs.js", "../../alpine/packages/collapse/dist/module.cjs.js", "../../alpine/packages/focus/dist/module.cjs.js", "../../alpine/packages/persist/dist/module.cjs.js", "../../alpine/packages/intersect/dist/module.cjs.js", "../node_modules/@alpinejs/resize/dist/module.cjs.js", "../../alpine/packages/anchor/dist/module.cjs.js", "../node_modules/nprogress/nprogress.js", "../../alpine/packages/morph/dist/module.cjs.js", "../../alpine/packages/mask/dist/module.cjs.js", "../js/utils.js", "../js/features/supportFileUploads.js", "../js/features/supportEntangle.js", "../js/hooks.js", "../js/request/modal.js", "../js/request/pool.js", "../js/request/commit.js", "../js/request/bus.js", "../js/request/index.js", "../js/$wire.js", "../js/component.js", "../js/store.js", "../js/events.js", "../js/directives.js", "../js/lifecycle.js", "../js/plugins/navigate/history.js", "../js/plugins/navigate/links.js", "../js/plugins/navigate/fetch.js", "../js/plugins/navigate/prefetch.js", "../js/plugins/navigate/teleport.js", "../js/plugins/navigate/scroll.js", "../js/plugins/navigate/persist.js", "../js/plugins/navigate/bar.js", "../js/plugins/navigate/popover.js", "../js/plugins/navigate/page.js", "../js/plugins/navigate/index.js", "../js/plugins/history/index.js", "../js/index.js", "../js/features/supportListeners.js", "../js/features/supportScriptsAndAssets.js", "../js/features/supportJsEvaluation.js", "../js/morph.js", "../js/features/supportMorphDom.js", "../js/features/supportDispatches.js", "../js/features/supportDisablingFormsDuringRequest.js", "../js/features/supportPropsAndModelables.js", "../js/features/supportFileDownloads.js", "../js/features/supportLazyLoading.js", "../js/features/supportQueryString.js", "../js/features/supportLaravelEcho.js", "../js/features/supportIsolating.js", "../js/features/supportNavigate.js", "../js/features/supportRedirects.js", "../js/directives/wire-transition.js", "../js/debounce.js", "../js/directives/wire-wildcard.js", "../js/directives/wire-navigate.js", "../js/directives/wire-confirm.js", "../js/directives/wire-current.js", "../js/directives/shared.js", "../js/directives/wire-offline.js", "../js/directives/wire-loading.js", "../js/directives/wire-stream.js", "../js/directives/wire-replace.js", "../js/directives/wire-ignore.js", "../js/directives/wire-cloak.js", "../js/directives/wire-dirty.js", "../js/directives/wire-model.js", "../js/directives/wire-init.js", "../js/directives/wire-poll.js", "../js/directives/wire-show.js", "../js/directives/wire-text.js"], - "sourcesContent": ["var __create = Object.create;\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __getProtoOf = Object.getPrototypeOf;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __commonJS = (cb, mod) => function __require() {\n return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;\n};\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(\n // If the importer is in node compatibility mode or this is not an ESM\n // file that has been converted to a CommonJS file using a Babel-\n // compatible transform (i.e. \"__esModule\" has not been set), then set\n // \"default\" to the CommonJS \"module.exports\" for node compatibility.\n isNodeMode || !mod || !mod.__esModule ? __defProp(target, \"default\", { value: mod, enumerable: true }) : target,\n mod\n));\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// node_modules/@vue/shared/dist/shared.cjs.js\nvar require_shared_cjs = __commonJS({\n \"node_modules/@vue/shared/dist/shared.cjs.js\"(exports) {\n \"use strict\";\n Object.defineProperty(exports, \"__esModule\", { value: true });\n function makeMap(str, expectsLowerCase) {\n const map = /* @__PURE__ */ Object.create(null);\n const list = str.split(\",\");\n for (let i = 0; i < list.length; i++) {\n map[list[i]] = true;\n }\n return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val];\n }\n var PatchFlagNames = {\n [\n 1\n /* TEXT */\n ]: `TEXT`,\n [\n 2\n /* CLASS */\n ]: `CLASS`,\n [\n 4\n /* STYLE */\n ]: `STYLE`,\n [\n 8\n /* PROPS */\n ]: `PROPS`,\n [\n 16\n /* FULL_PROPS */\n ]: `FULL_PROPS`,\n [\n 32\n /* HYDRATE_EVENTS */\n ]: `HYDRATE_EVENTS`,\n [\n 64\n /* STABLE_FRAGMENT */\n ]: `STABLE_FRAGMENT`,\n [\n 128\n /* KEYED_FRAGMENT */\n ]: `KEYED_FRAGMENT`,\n [\n 256\n /* UNKEYED_FRAGMENT */\n ]: `UNKEYED_FRAGMENT`,\n [\n 512\n /* NEED_PATCH */\n ]: `NEED_PATCH`,\n [\n 1024\n /* DYNAMIC_SLOTS */\n ]: `DYNAMIC_SLOTS`,\n [\n 2048\n /* DEV_ROOT_FRAGMENT */\n ]: `DEV_ROOT_FRAGMENT`,\n [\n -1\n /* HOISTED */\n ]: `HOISTED`,\n [\n -2\n /* BAIL */\n ]: `BAIL`\n };\n var slotFlagsText = {\n [\n 1\n /* STABLE */\n ]: \"STABLE\",\n [\n 2\n /* DYNAMIC */\n ]: \"DYNAMIC\",\n [\n 3\n /* FORWARDED */\n ]: \"FORWARDED\"\n };\n var GLOBALS_WHITE_LISTED = \"Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt\";\n var isGloballyWhitelisted = /* @__PURE__ */ makeMap(GLOBALS_WHITE_LISTED);\n var range = 2;\n function generateCodeFrame(source, start2 = 0, end = source.length) {\n let lines = source.split(/(\\r?\\n)/);\n const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);\n lines = lines.filter((_, idx) => idx % 2 === 0);\n let count = 0;\n const res = [];\n for (let i = 0; i < lines.length; i++) {\n count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0);\n if (count >= start2) {\n for (let j = i - range; j <= i + range || end > count; j++) {\n if (j < 0 || j >= lines.length)\n continue;\n const line = j + 1;\n res.push(`${line}${\" \".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`);\n const lineLength = lines[j].length;\n const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0;\n if (j === i) {\n const pad = start2 - (count - (lineLength + newLineSeqLength));\n const length = Math.max(1, end > count ? lineLength - pad : end - start2);\n res.push(` | ` + \" \".repeat(pad) + \"^\".repeat(length));\n } else if (j > i) {\n if (end > count) {\n const length = Math.max(Math.min(end - count, lineLength), 1);\n res.push(` | ` + \"^\".repeat(length));\n }\n count += lineLength + newLineSeqLength;\n }\n }\n break;\n }\n }\n return res.join(\"\\n\");\n }\n var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;\n var isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs);\n var isBooleanAttr2 = /* @__PURE__ */ makeMap(specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`);\n var unsafeAttrCharRE = /[>/=\"'\\u0009\\u000a\\u000c\\u0020]/;\n var attrValidationCache = {};\n function isSSRSafeAttrName(name) {\n if (attrValidationCache.hasOwnProperty(name)) {\n return attrValidationCache[name];\n }\n const isUnsafe = unsafeAttrCharRE.test(name);\n if (isUnsafe) {\n console.error(`unsafe attribute name: ${name}`);\n }\n return attrValidationCache[name] = !isUnsafe;\n }\n var propsToAttrMap = {\n acceptCharset: \"accept-charset\",\n className: \"class\",\n htmlFor: \"for\",\n httpEquiv: \"http-equiv\"\n };\n var isNoUnitNumericStyleProp = /* @__PURE__ */ makeMap(`animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width`);\n var isKnownAttr = /* @__PURE__ */ makeMap(`accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`);\n function normalizeStyle(value) {\n if (isArray(value)) {\n const res = {};\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n const normalized = normalizeStyle(isString(item) ? parseStringStyle(item) : item);\n if (normalized) {\n for (const key in normalized) {\n res[key] = normalized[key];\n }\n }\n }\n return res;\n } else if (isObject(value)) {\n return value;\n }\n }\n var listDelimiterRE = /;(?![^(]*\\))/g;\n var propertyDelimiterRE = /:(.+)/;\n function parseStringStyle(cssText) {\n const ret = {};\n cssText.split(listDelimiterRE).forEach((item) => {\n if (item) {\n const tmp = item.split(propertyDelimiterRE);\n tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());\n }\n });\n return ret;\n }\n function stringifyStyle(styles) {\n let ret = \"\";\n if (!styles) {\n return ret;\n }\n for (const key in styles) {\n const value = styles[key];\n const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);\n if (isString(value) || typeof value === \"number\" && isNoUnitNumericStyleProp(normalizedKey)) {\n ret += `${normalizedKey}:${value};`;\n }\n }\n return ret;\n }\n function normalizeClass(value) {\n let res = \"\";\n if (isString(value)) {\n res = value;\n } else if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const normalized = normalizeClass(value[i]);\n if (normalized) {\n res += normalized + \" \";\n }\n }\n } else if (isObject(value)) {\n for (const name in value) {\n if (value[name]) {\n res += name + \" \";\n }\n }\n }\n return res.trim();\n }\n var HTML_TAGS = \"html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot\";\n var SVG_TAGS = \"svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view\";\n var VOID_TAGS = \"area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr\";\n var isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);\n var isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS);\n var isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS);\n var escapeRE = /[\"'&<>]/;\n function escapeHtml(string) {\n const str = \"\" + string;\n const match = escapeRE.exec(str);\n if (!match) {\n return str;\n }\n let html = \"\";\n let escaped;\n let index;\n let lastIndex = 0;\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34:\n escaped = \""\";\n break;\n case 38:\n escaped = \"&\";\n break;\n case 39:\n escaped = \"'\";\n break;\n case 60:\n escaped = \"<\";\n break;\n case 62:\n escaped = \">\";\n break;\n default:\n continue;\n }\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n lastIndex = index + 1;\n html += escaped;\n }\n return lastIndex !== index ? html + str.substring(lastIndex, index) : html;\n }\n var commentStripRE = /^-?>||--!>| looseEqual(item, val));\n }\n var toDisplayString = (val) => {\n return val == null ? \"\" : isObject(val) ? JSON.stringify(val, replacer, 2) : String(val);\n };\n var replacer = (_key, val) => {\n if (isMap(val)) {\n return {\n [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val2]) => {\n entries[`${key} =>`] = val2;\n return entries;\n }, {})\n };\n } else if (isSet(val)) {\n return {\n [`Set(${val.size})`]: [...val.values()]\n };\n } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {\n return String(val);\n }\n return val;\n };\n var babelParserDefaultPlugins = [\n \"bigInt\",\n \"optionalChaining\",\n \"nullishCoalescingOperator\"\n ];\n var EMPTY_OBJ = Object.freeze({});\n var EMPTY_ARR = Object.freeze([]);\n var NOOP = () => {\n };\n var NO = () => false;\n var onRE = /^on[^a-z]/;\n var isOn = (key) => onRE.test(key);\n var isModelListener = (key) => key.startsWith(\"onUpdate:\");\n var extend = Object.assign;\n var remove = (arr, el) => {\n const i = arr.indexOf(el);\n if (i > -1) {\n arr.splice(i, 1);\n }\n };\n var hasOwnProperty = Object.prototype.hasOwnProperty;\n var hasOwn = (val, key) => hasOwnProperty.call(val, key);\n var isArray = Array.isArray;\n var isMap = (val) => toTypeString(val) === \"[object Map]\";\n var isSet = (val) => toTypeString(val) === \"[object Set]\";\n var isDate = (val) => val instanceof Date;\n var isFunction = (val) => typeof val === \"function\";\n var isString = (val) => typeof val === \"string\";\n var isSymbol = (val) => typeof val === \"symbol\";\n var isObject = (val) => val !== null && typeof val === \"object\";\n var isPromise = (val) => {\n return isObject(val) && isFunction(val.then) && isFunction(val.catch);\n };\n var objectToString = Object.prototype.toString;\n var toTypeString = (value) => objectToString.call(value);\n var toRawType = (value) => {\n return toTypeString(value).slice(8, -1);\n };\n var isPlainObject = (val) => toTypeString(val) === \"[object Object]\";\n var isIntegerKey = (key) => isString(key) && key !== \"NaN\" && key[0] !== \"-\" && \"\" + parseInt(key, 10) === key;\n var isReservedProp = /* @__PURE__ */ makeMap(\n // the leading comma is intentional so empty string \"\" is also included\n \",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted\"\n );\n var cacheStringFunction = (fn) => {\n const cache = /* @__PURE__ */ Object.create(null);\n return (str) => {\n const hit = cache[str];\n return hit || (cache[str] = fn(str));\n };\n };\n var camelizeRE = /-(\\w)/g;\n var camelize = cacheStringFunction((str) => {\n return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : \"\");\n });\n var hyphenateRE = /\\B([A-Z])/g;\n var hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, \"-$1\").toLowerCase());\n var capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));\n var toHandlerKey = cacheStringFunction((str) => str ? `on${capitalize(str)}` : ``);\n var hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);\n var invokeArrayFns = (fns, arg) => {\n for (let i = 0; i < fns.length; i++) {\n fns[i](arg);\n }\n };\n var def = (obj, key, value) => {\n Object.defineProperty(obj, key, {\n configurable: true,\n enumerable: false,\n value\n });\n };\n var toNumber = (val) => {\n const n = parseFloat(val);\n return isNaN(n) ? val : n;\n };\n var _globalThis;\n var getGlobalThis = () => {\n return _globalThis || (_globalThis = typeof globalThis !== \"undefined\" ? globalThis : typeof self !== \"undefined\" ? self : typeof window !== \"undefined\" ? window : typeof global !== \"undefined\" ? global : {});\n };\n exports.EMPTY_ARR = EMPTY_ARR;\n exports.EMPTY_OBJ = EMPTY_OBJ;\n exports.NO = NO;\n exports.NOOP = NOOP;\n exports.PatchFlagNames = PatchFlagNames;\n exports.babelParserDefaultPlugins = babelParserDefaultPlugins;\n exports.camelize = camelize;\n exports.capitalize = capitalize;\n exports.def = def;\n exports.escapeHtml = escapeHtml;\n exports.escapeHtmlComment = escapeHtmlComment;\n exports.extend = extend;\n exports.generateCodeFrame = generateCodeFrame;\n exports.getGlobalThis = getGlobalThis;\n exports.hasChanged = hasChanged;\n exports.hasOwn = hasOwn;\n exports.hyphenate = hyphenate;\n exports.invokeArrayFns = invokeArrayFns;\n exports.isArray = isArray;\n exports.isBooleanAttr = isBooleanAttr2;\n exports.isDate = isDate;\n exports.isFunction = isFunction;\n exports.isGloballyWhitelisted = isGloballyWhitelisted;\n exports.isHTMLTag = isHTMLTag;\n exports.isIntegerKey = isIntegerKey;\n exports.isKnownAttr = isKnownAttr;\n exports.isMap = isMap;\n exports.isModelListener = isModelListener;\n exports.isNoUnitNumericStyleProp = isNoUnitNumericStyleProp;\n exports.isObject = isObject;\n exports.isOn = isOn;\n exports.isPlainObject = isPlainObject;\n exports.isPromise = isPromise;\n exports.isReservedProp = isReservedProp;\n exports.isSSRSafeAttrName = isSSRSafeAttrName;\n exports.isSVGTag = isSVGTag;\n exports.isSet = isSet;\n exports.isSpecialBooleanAttr = isSpecialBooleanAttr;\n exports.isString = isString;\n exports.isSymbol = isSymbol;\n exports.isVoidTag = isVoidTag;\n exports.looseEqual = looseEqual;\n exports.looseIndexOf = looseIndexOf;\n exports.makeMap = makeMap;\n exports.normalizeClass = normalizeClass;\n exports.normalizeStyle = normalizeStyle;\n exports.objectToString = objectToString;\n exports.parseStringStyle = parseStringStyle;\n exports.propsToAttrMap = propsToAttrMap;\n exports.remove = remove;\n exports.slotFlagsText = slotFlagsText;\n exports.stringifyStyle = stringifyStyle;\n exports.toDisplayString = toDisplayString;\n exports.toHandlerKey = toHandlerKey;\n exports.toNumber = toNumber;\n exports.toRawType = toRawType;\n exports.toTypeString = toTypeString;\n }\n});\n\n// node_modules/@vue/shared/index.js\nvar require_shared = __commonJS({\n \"node_modules/@vue/shared/index.js\"(exports, module2) {\n \"use strict\";\n if (false) {\n module2.exports = null;\n } else {\n module2.exports = require_shared_cjs();\n }\n }\n});\n\n// node_modules/@vue/reactivity/dist/reactivity.cjs.js\nvar require_reactivity_cjs = __commonJS({\n \"node_modules/@vue/reactivity/dist/reactivity.cjs.js\"(exports) {\n \"use strict\";\n Object.defineProperty(exports, \"__esModule\", { value: true });\n var shared = require_shared();\n var targetMap = /* @__PURE__ */ new WeakMap();\n var effectStack = [];\n var activeEffect;\n var ITERATE_KEY = Symbol(\"iterate\");\n var MAP_KEY_ITERATE_KEY = Symbol(\"Map key iterate\");\n function isEffect(fn) {\n return fn && fn._isEffect === true;\n }\n function effect3(fn, options = shared.EMPTY_OBJ) {\n if (isEffect(fn)) {\n fn = fn.raw;\n }\n const effect4 = createReactiveEffect(fn, options);\n if (!options.lazy) {\n effect4();\n }\n return effect4;\n }\n function stop2(effect4) {\n if (effect4.active) {\n cleanup(effect4);\n if (effect4.options.onStop) {\n effect4.options.onStop();\n }\n effect4.active = false;\n }\n }\n var uid = 0;\n function createReactiveEffect(fn, options) {\n const effect4 = function reactiveEffect() {\n if (!effect4.active) {\n return fn();\n }\n if (!effectStack.includes(effect4)) {\n cleanup(effect4);\n try {\n enableTracking();\n effectStack.push(effect4);\n activeEffect = effect4;\n return fn();\n } finally {\n effectStack.pop();\n resetTracking();\n activeEffect = effectStack[effectStack.length - 1];\n }\n }\n };\n effect4.id = uid++;\n effect4.allowRecurse = !!options.allowRecurse;\n effect4._isEffect = true;\n effect4.active = true;\n effect4.raw = fn;\n effect4.deps = [];\n effect4.options = options;\n return effect4;\n }\n function cleanup(effect4) {\n const { deps } = effect4;\n if (deps.length) {\n for (let i = 0; i < deps.length; i++) {\n deps[i].delete(effect4);\n }\n deps.length = 0;\n }\n }\n var shouldTrack = true;\n var trackStack = [];\n function pauseTracking() {\n trackStack.push(shouldTrack);\n shouldTrack = false;\n }\n function enableTracking() {\n trackStack.push(shouldTrack);\n shouldTrack = true;\n }\n function resetTracking() {\n const last = trackStack.pop();\n shouldTrack = last === void 0 ? true : last;\n }\n function track(target, type, key) {\n if (!shouldTrack || activeEffect === void 0) {\n return;\n }\n let depsMap = targetMap.get(target);\n if (!depsMap) {\n targetMap.set(target, depsMap = /* @__PURE__ */ new Map());\n }\n let dep = depsMap.get(key);\n if (!dep) {\n depsMap.set(key, dep = /* @__PURE__ */ new Set());\n }\n if (!dep.has(activeEffect)) {\n dep.add(activeEffect);\n activeEffect.deps.push(dep);\n if (activeEffect.options.onTrack) {\n activeEffect.options.onTrack({\n effect: activeEffect,\n target,\n type,\n key\n });\n }\n }\n }\n function trigger(target, type, key, newValue, oldValue, oldTarget) {\n const depsMap = targetMap.get(target);\n if (!depsMap) {\n return;\n }\n const effects = /* @__PURE__ */ new Set();\n const add2 = (effectsToAdd) => {\n if (effectsToAdd) {\n effectsToAdd.forEach((effect4) => {\n if (effect4 !== activeEffect || effect4.allowRecurse) {\n effects.add(effect4);\n }\n });\n }\n };\n if (type === \"clear\") {\n depsMap.forEach(add2);\n } else if (key === \"length\" && shared.isArray(target)) {\n depsMap.forEach((dep, key2) => {\n if (key2 === \"length\" || key2 >= newValue) {\n add2(dep);\n }\n });\n } else {\n if (key !== void 0) {\n add2(depsMap.get(key));\n }\n switch (type) {\n case \"add\":\n if (!shared.isArray(target)) {\n add2(depsMap.get(ITERATE_KEY));\n if (shared.isMap(target)) {\n add2(depsMap.get(MAP_KEY_ITERATE_KEY));\n }\n } else if (shared.isIntegerKey(key)) {\n add2(depsMap.get(\"length\"));\n }\n break;\n case \"delete\":\n if (!shared.isArray(target)) {\n add2(depsMap.get(ITERATE_KEY));\n if (shared.isMap(target)) {\n add2(depsMap.get(MAP_KEY_ITERATE_KEY));\n }\n }\n break;\n case \"set\":\n if (shared.isMap(target)) {\n add2(depsMap.get(ITERATE_KEY));\n }\n break;\n }\n }\n const run = (effect4) => {\n if (effect4.options.onTrigger) {\n effect4.options.onTrigger({\n effect: effect4,\n target,\n key,\n type,\n newValue,\n oldValue,\n oldTarget\n });\n }\n if (effect4.options.scheduler) {\n effect4.options.scheduler(effect4);\n } else {\n effect4();\n }\n };\n effects.forEach(run);\n }\n var isNonTrackableKeys = /* @__PURE__ */ shared.makeMap(`__proto__,__v_isRef,__isVue`);\n var builtInSymbols = new Set(Object.getOwnPropertyNames(Symbol).map((key) => Symbol[key]).filter(shared.isSymbol));\n var get2 = /* @__PURE__ */ createGetter();\n var shallowGet = /* @__PURE__ */ createGetter(false, true);\n var readonlyGet = /* @__PURE__ */ createGetter(true);\n var shallowReadonlyGet = /* @__PURE__ */ createGetter(true, true);\n var arrayInstrumentations = /* @__PURE__ */ createArrayInstrumentations();\n function createArrayInstrumentations() {\n const instrumentations = {};\n [\"includes\", \"indexOf\", \"lastIndexOf\"].forEach((key) => {\n instrumentations[key] = function(...args) {\n const arr = toRaw2(this);\n for (let i = 0, l = this.length; i < l; i++) {\n track(arr, \"get\", i + \"\");\n }\n const res = arr[key](...args);\n if (res === -1 || res === false) {\n return arr[key](...args.map(toRaw2));\n } else {\n return res;\n }\n };\n });\n [\"push\", \"pop\", \"shift\", \"unshift\", \"splice\"].forEach((key) => {\n instrumentations[key] = function(...args) {\n pauseTracking();\n const res = toRaw2(this)[key].apply(this, args);\n resetTracking();\n return res;\n };\n });\n return instrumentations;\n }\n function createGetter(isReadonly2 = false, shallow = false) {\n return function get3(target, key, receiver) {\n if (key === \"__v_isReactive\") {\n return !isReadonly2;\n } else if (key === \"__v_isReadonly\") {\n return isReadonly2;\n } else if (key === \"__v_raw\" && receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) {\n return target;\n }\n const targetIsArray = shared.isArray(target);\n if (!isReadonly2 && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {\n return Reflect.get(arrayInstrumentations, key, receiver);\n }\n const res = Reflect.get(target, key, receiver);\n if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {\n return res;\n }\n if (!isReadonly2) {\n track(target, \"get\", key);\n }\n if (shallow) {\n return res;\n }\n if (isRef(res)) {\n const shouldUnwrap = !targetIsArray || !shared.isIntegerKey(key);\n return shouldUnwrap ? res.value : res;\n }\n if (shared.isObject(res)) {\n return isReadonly2 ? readonly(res) : reactive3(res);\n }\n return res;\n };\n }\n var set2 = /* @__PURE__ */ createSetter();\n var shallowSet = /* @__PURE__ */ createSetter(true);\n function createSetter(shallow = false) {\n return function set3(target, key, value, receiver) {\n let oldValue = target[key];\n if (!shallow) {\n value = toRaw2(value);\n oldValue = toRaw2(oldValue);\n if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) {\n oldValue.value = value;\n return true;\n }\n }\n const hadKey = shared.isArray(target) && shared.isIntegerKey(key) ? Number(key) < target.length : shared.hasOwn(target, key);\n const result = Reflect.set(target, key, value, receiver);\n if (target === toRaw2(receiver)) {\n if (!hadKey) {\n trigger(target, \"add\", key, value);\n } else if (shared.hasChanged(value, oldValue)) {\n trigger(target, \"set\", key, value, oldValue);\n }\n }\n return result;\n };\n }\n function deleteProperty(target, key) {\n const hadKey = shared.hasOwn(target, key);\n const oldValue = target[key];\n const result = Reflect.deleteProperty(target, key);\n if (result && hadKey) {\n trigger(target, \"delete\", key, void 0, oldValue);\n }\n return result;\n }\n function has(target, key) {\n const result = Reflect.has(target, key);\n if (!shared.isSymbol(key) || !builtInSymbols.has(key)) {\n track(target, \"has\", key);\n }\n return result;\n }\n function ownKeys(target) {\n track(target, \"iterate\", shared.isArray(target) ? \"length\" : ITERATE_KEY);\n return Reflect.ownKeys(target);\n }\n var mutableHandlers = {\n get: get2,\n set: set2,\n deleteProperty,\n has,\n ownKeys\n };\n var readonlyHandlers = {\n get: readonlyGet,\n set(target, key) {\n {\n console.warn(`Set operation on key \"${String(key)}\" failed: target is readonly.`, target);\n }\n return true;\n },\n deleteProperty(target, key) {\n {\n console.warn(`Delete operation on key \"${String(key)}\" failed: target is readonly.`, target);\n }\n return true;\n }\n };\n var shallowReactiveHandlers = /* @__PURE__ */ shared.extend({}, mutableHandlers, {\n get: shallowGet,\n set: shallowSet\n });\n var shallowReadonlyHandlers = /* @__PURE__ */ shared.extend({}, readonlyHandlers, {\n get: shallowReadonlyGet\n });\n var toReactive = (value) => shared.isObject(value) ? reactive3(value) : value;\n var toReadonly = (value) => shared.isObject(value) ? readonly(value) : value;\n var toShallow = (value) => value;\n var getProto = (v) => Reflect.getPrototypeOf(v);\n function get$1(target, key, isReadonly2 = false, isShallow = false) {\n target = target[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const rawKey = toRaw2(key);\n if (key !== rawKey) {\n !isReadonly2 && track(rawTarget, \"get\", key);\n }\n !isReadonly2 && track(rawTarget, \"get\", rawKey);\n const { has: has2 } = getProto(rawTarget);\n const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;\n if (has2.call(rawTarget, key)) {\n return wrap(target.get(key));\n } else if (has2.call(rawTarget, rawKey)) {\n return wrap(target.get(rawKey));\n } else if (target !== rawTarget) {\n target.get(key);\n }\n }\n function has$1(key, isReadonly2 = false) {\n const target = this[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const rawKey = toRaw2(key);\n if (key !== rawKey) {\n !isReadonly2 && track(rawTarget, \"has\", key);\n }\n !isReadonly2 && track(rawTarget, \"has\", rawKey);\n return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);\n }\n function size(target, isReadonly2 = false) {\n target = target[\n \"__v_raw\"\n /* RAW */\n ];\n !isReadonly2 && track(toRaw2(target), \"iterate\", ITERATE_KEY);\n return Reflect.get(target, \"size\", target);\n }\n function add(value) {\n value = toRaw2(value);\n const target = toRaw2(this);\n const proto = getProto(target);\n const hadKey = proto.has.call(target, value);\n if (!hadKey) {\n target.add(value);\n trigger(target, \"add\", value, value);\n }\n return this;\n }\n function set$1(key, value) {\n value = toRaw2(value);\n const target = toRaw2(this);\n const { has: has2, get: get3 } = getProto(target);\n let hadKey = has2.call(target, key);\n if (!hadKey) {\n key = toRaw2(key);\n hadKey = has2.call(target, key);\n } else {\n checkIdentityKeys(target, has2, key);\n }\n const oldValue = get3.call(target, key);\n target.set(key, value);\n if (!hadKey) {\n trigger(target, \"add\", key, value);\n } else if (shared.hasChanged(value, oldValue)) {\n trigger(target, \"set\", key, value, oldValue);\n }\n return this;\n }\n function deleteEntry(key) {\n const target = toRaw2(this);\n const { has: has2, get: get3 } = getProto(target);\n let hadKey = has2.call(target, key);\n if (!hadKey) {\n key = toRaw2(key);\n hadKey = has2.call(target, key);\n } else {\n checkIdentityKeys(target, has2, key);\n }\n const oldValue = get3 ? get3.call(target, key) : void 0;\n const result = target.delete(key);\n if (hadKey) {\n trigger(target, \"delete\", key, void 0, oldValue);\n }\n return result;\n }\n function clear() {\n const target = toRaw2(this);\n const hadItems = target.size !== 0;\n const oldTarget = shared.isMap(target) ? new Map(target) : new Set(target);\n const result = target.clear();\n if (hadItems) {\n trigger(target, \"clear\", void 0, void 0, oldTarget);\n }\n return result;\n }\n function createForEach(isReadonly2, isShallow) {\n return function forEach(callback, thisArg) {\n const observed = this;\n const target = observed[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;\n !isReadonly2 && track(rawTarget, \"iterate\", ITERATE_KEY);\n return target.forEach((value, key) => {\n return callback.call(thisArg, wrap(value), wrap(key), observed);\n });\n };\n }\n function createIterableMethod(method, isReadonly2, isShallow) {\n return function(...args) {\n const target = this[\n \"__v_raw\"\n /* RAW */\n ];\n const rawTarget = toRaw2(target);\n const targetIsMap = shared.isMap(rawTarget);\n const isPair = method === \"entries\" || method === Symbol.iterator && targetIsMap;\n const isKeyOnly = method === \"keys\" && targetIsMap;\n const innerIterator = target[method](...args);\n const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;\n !isReadonly2 && track(rawTarget, \"iterate\", isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY);\n return {\n // iterator protocol\n next() {\n const { value, done } = innerIterator.next();\n return done ? { value, done } : {\n value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),\n done\n };\n },\n // iterable protocol\n [Symbol.iterator]() {\n return this;\n }\n };\n };\n }\n function createReadonlyMethod(type) {\n return function(...args) {\n {\n const key = args[0] ? `on key \"${args[0]}\" ` : ``;\n console.warn(`${shared.capitalize(type)} operation ${key}failed: target is readonly.`, toRaw2(this));\n }\n return type === \"delete\" ? false : this;\n };\n }\n function createInstrumentations() {\n const mutableInstrumentations2 = {\n get(key) {\n return get$1(this, key);\n },\n get size() {\n return size(this);\n },\n has: has$1,\n add,\n set: set$1,\n delete: deleteEntry,\n clear,\n forEach: createForEach(false, false)\n };\n const shallowInstrumentations2 = {\n get(key) {\n return get$1(this, key, false, true);\n },\n get size() {\n return size(this);\n },\n has: has$1,\n add,\n set: set$1,\n delete: deleteEntry,\n clear,\n forEach: createForEach(false, true)\n };\n const readonlyInstrumentations2 = {\n get(key) {\n return get$1(this, key, true);\n },\n get size() {\n return size(this, true);\n },\n has(key) {\n return has$1.call(this, key, true);\n },\n add: createReadonlyMethod(\n \"add\"\n /* ADD */\n ),\n set: createReadonlyMethod(\n \"set\"\n /* SET */\n ),\n delete: createReadonlyMethod(\n \"delete\"\n /* DELETE */\n ),\n clear: createReadonlyMethod(\n \"clear\"\n /* CLEAR */\n ),\n forEach: createForEach(true, false)\n };\n const shallowReadonlyInstrumentations2 = {\n get(key) {\n return get$1(this, key, true, true);\n },\n get size() {\n return size(this, true);\n },\n has(key) {\n return has$1.call(this, key, true);\n },\n add: createReadonlyMethod(\n \"add\"\n /* ADD */\n ),\n set: createReadonlyMethod(\n \"set\"\n /* SET */\n ),\n delete: createReadonlyMethod(\n \"delete\"\n /* DELETE */\n ),\n clear: createReadonlyMethod(\n \"clear\"\n /* CLEAR */\n ),\n forEach: createForEach(true, true)\n };\n const iteratorMethods = [\"keys\", \"values\", \"entries\", Symbol.iterator];\n iteratorMethods.forEach((method) => {\n mutableInstrumentations2[method] = createIterableMethod(method, false, false);\n readonlyInstrumentations2[method] = createIterableMethod(method, true, false);\n shallowInstrumentations2[method] = createIterableMethod(method, false, true);\n shallowReadonlyInstrumentations2[method] = createIterableMethod(method, true, true);\n });\n return [\n mutableInstrumentations2,\n readonlyInstrumentations2,\n shallowInstrumentations2,\n shallowReadonlyInstrumentations2\n ];\n }\n var [mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations] = /* @__PURE__ */ createInstrumentations();\n function createInstrumentationGetter(isReadonly2, shallow) {\n const instrumentations = shallow ? isReadonly2 ? shallowReadonlyInstrumentations : shallowInstrumentations : isReadonly2 ? readonlyInstrumentations : mutableInstrumentations;\n return (target, key, receiver) => {\n if (key === \"__v_isReactive\") {\n return !isReadonly2;\n } else if (key === \"__v_isReadonly\") {\n return isReadonly2;\n } else if (key === \"__v_raw\") {\n return target;\n }\n return Reflect.get(shared.hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver);\n };\n }\n var mutableCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(false, false)\n };\n var shallowCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(false, true)\n };\n var readonlyCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(true, false)\n };\n var shallowReadonlyCollectionHandlers = {\n get: /* @__PURE__ */ createInstrumentationGetter(true, true)\n };\n function checkIdentityKeys(target, has2, key) {\n const rawKey = toRaw2(key);\n if (rawKey !== key && has2.call(target, rawKey)) {\n const type = shared.toRawType(target);\n console.warn(`Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`);\n }\n }\n var reactiveMap = /* @__PURE__ */ new WeakMap();\n var shallowReactiveMap = /* @__PURE__ */ new WeakMap();\n var readonlyMap = /* @__PURE__ */ new WeakMap();\n var shallowReadonlyMap = /* @__PURE__ */ new WeakMap();\n function targetTypeMap(rawType) {\n switch (rawType) {\n case \"Object\":\n case \"Array\":\n return 1;\n case \"Map\":\n case \"Set\":\n case \"WeakMap\":\n case \"WeakSet\":\n return 2;\n default:\n return 0;\n }\n }\n function getTargetType(value) {\n return value[\n \"__v_skip\"\n /* SKIP */\n ] || !Object.isExtensible(value) ? 0 : targetTypeMap(shared.toRawType(value));\n }\n function reactive3(target) {\n if (target && target[\n \"__v_isReadonly\"\n /* IS_READONLY */\n ]) {\n return target;\n }\n return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);\n }\n function shallowReactive(target) {\n return createReactiveObject(target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap);\n }\n function readonly(target) {\n return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap);\n }\n function shallowReadonly(target) {\n return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);\n }\n function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {\n if (!shared.isObject(target)) {\n {\n console.warn(`value cannot be made reactive: ${String(target)}`);\n }\n return target;\n }\n if (target[\n \"__v_raw\"\n /* RAW */\n ] && !(isReadonly2 && target[\n \"__v_isReactive\"\n /* IS_REACTIVE */\n ])) {\n return target;\n }\n const existingProxy = proxyMap.get(target);\n if (existingProxy) {\n return existingProxy;\n }\n const targetType = getTargetType(target);\n if (targetType === 0) {\n return target;\n }\n const proxy = new Proxy(target, targetType === 2 ? collectionHandlers : baseHandlers);\n proxyMap.set(target, proxy);\n return proxy;\n }\n function isReactive2(value) {\n if (isReadonly(value)) {\n return isReactive2(value[\n \"__v_raw\"\n /* RAW */\n ]);\n }\n return !!(value && value[\n \"__v_isReactive\"\n /* IS_REACTIVE */\n ]);\n }\n function isReadonly(value) {\n return !!(value && value[\n \"__v_isReadonly\"\n /* IS_READONLY */\n ]);\n }\n function isProxy(value) {\n return isReactive2(value) || isReadonly(value);\n }\n function toRaw2(observed) {\n return observed && toRaw2(observed[\n \"__v_raw\"\n /* RAW */\n ]) || observed;\n }\n function markRaw(value) {\n shared.def(value, \"__v_skip\", true);\n return value;\n }\n var convert = (val) => shared.isObject(val) ? reactive3(val) : val;\n function isRef(r) {\n return Boolean(r && r.__v_isRef === true);\n }\n function ref(value) {\n return createRef(value);\n }\n function shallowRef(value) {\n return createRef(value, true);\n }\n var RefImpl = class {\n constructor(value, _shallow = false) {\n this._shallow = _shallow;\n this.__v_isRef = true;\n this._rawValue = _shallow ? value : toRaw2(value);\n this._value = _shallow ? value : convert(value);\n }\n get value() {\n track(toRaw2(this), \"get\", \"value\");\n return this._value;\n }\n set value(newVal) {\n newVal = this._shallow ? newVal : toRaw2(newVal);\n if (shared.hasChanged(newVal, this._rawValue)) {\n this._rawValue = newVal;\n this._value = this._shallow ? newVal : convert(newVal);\n trigger(toRaw2(this), \"set\", \"value\", newVal);\n }\n }\n };\n function createRef(rawValue, shallow = false) {\n if (isRef(rawValue)) {\n return rawValue;\n }\n return new RefImpl(rawValue, shallow);\n }\n function triggerRef(ref2) {\n trigger(toRaw2(ref2), \"set\", \"value\", ref2.value);\n }\n function unref(ref2) {\n return isRef(ref2) ? ref2.value : ref2;\n }\n var shallowUnwrapHandlers = {\n get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),\n set: (target, key, value, receiver) => {\n const oldValue = target[key];\n if (isRef(oldValue) && !isRef(value)) {\n oldValue.value = value;\n return true;\n } else {\n return Reflect.set(target, key, value, receiver);\n }\n }\n };\n function proxyRefs(objectWithRefs) {\n return isReactive2(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);\n }\n var CustomRefImpl = class {\n constructor(factory) {\n this.__v_isRef = true;\n const { get: get3, set: set3 } = factory(() => track(this, \"get\", \"value\"), () => trigger(this, \"set\", \"value\"));\n this._get = get3;\n this._set = set3;\n }\n get value() {\n return this._get();\n }\n set value(newVal) {\n this._set(newVal);\n }\n };\n function customRef(factory) {\n return new CustomRefImpl(factory);\n }\n function toRefs(object) {\n if (!isProxy(object)) {\n console.warn(`toRefs() expects a reactive object but received a plain one.`);\n }\n const ret = shared.isArray(object) ? new Array(object.length) : {};\n for (const key in object) {\n ret[key] = toRef(object, key);\n }\n return ret;\n }\n var ObjectRefImpl = class {\n constructor(_object, _key) {\n this._object = _object;\n this._key = _key;\n this.__v_isRef = true;\n }\n get value() {\n return this._object[this._key];\n }\n set value(newVal) {\n this._object[this._key] = newVal;\n }\n };\n function toRef(object, key) {\n return isRef(object[key]) ? object[key] : new ObjectRefImpl(object, key);\n }\n var ComputedRefImpl = class {\n constructor(getter, _setter, isReadonly2) {\n this._setter = _setter;\n this._dirty = true;\n this.__v_isRef = true;\n this.effect = effect3(getter, {\n lazy: true,\n scheduler: () => {\n if (!this._dirty) {\n this._dirty = true;\n trigger(toRaw2(this), \"set\", \"value\");\n }\n }\n });\n this[\n \"__v_isReadonly\"\n /* IS_READONLY */\n ] = isReadonly2;\n }\n get value() {\n const self2 = toRaw2(this);\n if (self2._dirty) {\n self2._value = this.effect();\n self2._dirty = false;\n }\n track(self2, \"get\", \"value\");\n return self2._value;\n }\n set value(newValue) {\n this._setter(newValue);\n }\n };\n function computed(getterOrOptions) {\n let getter;\n let setter;\n if (shared.isFunction(getterOrOptions)) {\n getter = getterOrOptions;\n setter = () => {\n console.warn(\"Write operation failed: computed value is readonly\");\n };\n } else {\n getter = getterOrOptions.get;\n setter = getterOrOptions.set;\n }\n return new ComputedRefImpl(getter, setter, shared.isFunction(getterOrOptions) || !getterOrOptions.set);\n }\n exports.ITERATE_KEY = ITERATE_KEY;\n exports.computed = computed;\n exports.customRef = customRef;\n exports.effect = effect3;\n exports.enableTracking = enableTracking;\n exports.isProxy = isProxy;\n exports.isReactive = isReactive2;\n exports.isReadonly = isReadonly;\n exports.isRef = isRef;\n exports.markRaw = markRaw;\n exports.pauseTracking = pauseTracking;\n exports.proxyRefs = proxyRefs;\n exports.reactive = reactive3;\n exports.readonly = readonly;\n exports.ref = ref;\n exports.resetTracking = resetTracking;\n exports.shallowReactive = shallowReactive;\n exports.shallowReadonly = shallowReadonly;\n exports.shallowRef = shallowRef;\n exports.stop = stop2;\n exports.toRaw = toRaw2;\n exports.toRef = toRef;\n exports.toRefs = toRefs;\n exports.track = track;\n exports.trigger = trigger;\n exports.triggerRef = triggerRef;\n exports.unref = unref;\n }\n});\n\n// node_modules/@vue/reactivity/index.js\nvar require_reactivity = __commonJS({\n \"node_modules/@vue/reactivity/index.js\"(exports, module2) {\n \"use strict\";\n if (false) {\n module2.exports = null;\n } else {\n module2.exports = require_reactivity_cjs();\n }\n }\n});\n\n// packages/alpinejs/builds/module.js\nvar module_exports = {};\n__export(module_exports, {\n Alpine: () => src_default,\n default: () => module_default\n});\nmodule.exports = __toCommonJS(module_exports);\n\n// packages/alpinejs/src/scheduler.js\nvar flushPending = false;\nvar flushing = false;\nvar queue = [];\nvar lastFlushedIndex = -1;\nfunction scheduler(callback) {\n queueJob(callback);\n}\nfunction queueJob(job) {\n if (!queue.includes(job))\n queue.push(job);\n queueFlush();\n}\nfunction dequeueJob(job) {\n let index = queue.indexOf(job);\n if (index !== -1 && index > lastFlushedIndex)\n queue.splice(index, 1);\n}\nfunction queueFlush() {\n if (!flushing && !flushPending) {\n flushPending = true;\n queueMicrotask(flushJobs);\n }\n}\nfunction flushJobs() {\n flushPending = false;\n flushing = true;\n for (let i = 0; i < queue.length; i++) {\n queue[i]();\n lastFlushedIndex = i;\n }\n queue.length = 0;\n lastFlushedIndex = -1;\n flushing = false;\n}\n\n// packages/alpinejs/src/reactivity.js\nvar reactive;\nvar effect;\nvar release;\nvar raw;\nvar shouldSchedule = true;\nfunction disableEffectScheduling(callback) {\n shouldSchedule = false;\n callback();\n shouldSchedule = true;\n}\nfunction setReactivityEngine(engine) {\n reactive = engine.reactive;\n release = engine.release;\n effect = (callback) => engine.effect(callback, { scheduler: (task) => {\n if (shouldSchedule) {\n scheduler(task);\n } else {\n task();\n }\n } });\n raw = engine.raw;\n}\nfunction overrideEffect(override) {\n effect = override;\n}\nfunction elementBoundEffect(el) {\n let cleanup = () => {\n };\n let wrappedEffect = (callback) => {\n let effectReference = effect(callback);\n if (!el._x_effects) {\n el._x_effects = /* @__PURE__ */ new Set();\n el._x_runEffects = () => {\n el._x_effects.forEach((i) => i());\n };\n }\n el._x_effects.add(effectReference);\n cleanup = () => {\n if (effectReference === void 0)\n return;\n el._x_effects.delete(effectReference);\n release(effectReference);\n };\n return effectReference;\n };\n return [wrappedEffect, () => {\n cleanup();\n }];\n}\nfunction watch(getter, callback) {\n let firstTime = true;\n let oldValue;\n let effectReference = effect(() => {\n let value = getter();\n JSON.stringify(value);\n if (!firstTime) {\n queueMicrotask(() => {\n callback(value, oldValue);\n oldValue = value;\n });\n } else {\n oldValue = value;\n }\n firstTime = false;\n });\n return () => release(effectReference);\n}\n\n// packages/alpinejs/src/mutation.js\nvar onAttributeAddeds = [];\nvar onElRemoveds = [];\nvar onElAddeds = [];\nfunction onElAdded(callback) {\n onElAddeds.push(callback);\n}\nfunction onElRemoved(el, callback) {\n if (typeof callback === \"function\") {\n if (!el._x_cleanups)\n el._x_cleanups = [];\n el._x_cleanups.push(callback);\n } else {\n callback = el;\n onElRemoveds.push(callback);\n }\n}\nfunction onAttributesAdded(callback) {\n onAttributeAddeds.push(callback);\n}\nfunction onAttributeRemoved(el, name, callback) {\n if (!el._x_attributeCleanups)\n el._x_attributeCleanups = {};\n if (!el._x_attributeCleanups[name])\n el._x_attributeCleanups[name] = [];\n el._x_attributeCleanups[name].push(callback);\n}\nfunction cleanupAttributes(el, names) {\n if (!el._x_attributeCleanups)\n return;\n Object.entries(el._x_attributeCleanups).forEach(([name, value]) => {\n if (names === void 0 || names.includes(name)) {\n value.forEach((i) => i());\n delete el._x_attributeCleanups[name];\n }\n });\n}\nfunction cleanupElement(el) {\n var _a, _b;\n (_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);\n while ((_b = el._x_cleanups) == null ? void 0 : _b.length)\n el._x_cleanups.pop()();\n}\nvar observer = new MutationObserver(onMutate);\nvar currentlyObserving = false;\nfunction startObservingMutations() {\n observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true });\n currentlyObserving = true;\n}\nfunction stopObservingMutations() {\n flushObserver();\n observer.disconnect();\n currentlyObserving = false;\n}\nvar queuedMutations = [];\nfunction flushObserver() {\n let records = observer.takeRecords();\n queuedMutations.push(() => records.length > 0 && onMutate(records));\n let queueLengthWhenTriggered = queuedMutations.length;\n queueMicrotask(() => {\n if (queuedMutations.length === queueLengthWhenTriggered) {\n while (queuedMutations.length > 0)\n queuedMutations.shift()();\n }\n });\n}\nfunction mutateDom(callback) {\n if (!currentlyObserving)\n return callback();\n stopObservingMutations();\n let result = callback();\n startObservingMutations();\n return result;\n}\nvar isCollecting = false;\nvar deferredMutations = [];\nfunction deferMutations() {\n isCollecting = true;\n}\nfunction flushAndStopDeferringMutations() {\n isCollecting = false;\n onMutate(deferredMutations);\n deferredMutations = [];\n}\nfunction onMutate(mutations) {\n if (isCollecting) {\n deferredMutations = deferredMutations.concat(mutations);\n return;\n }\n let addedNodes = [];\n let removedNodes = /* @__PURE__ */ new Set();\n let addedAttributes = /* @__PURE__ */ new Map();\n let removedAttributes = /* @__PURE__ */ new Map();\n for (let i = 0; i < mutations.length; i++) {\n if (mutations[i].target._x_ignoreMutationObserver)\n continue;\n if (mutations[i].type === \"childList\") {\n mutations[i].removedNodes.forEach((node) => {\n if (node.nodeType !== 1)\n return;\n if (!node._x_marker)\n return;\n removedNodes.add(node);\n });\n mutations[i].addedNodes.forEach((node) => {\n if (node.nodeType !== 1)\n return;\n if (removedNodes.has(node)) {\n removedNodes.delete(node);\n return;\n }\n if (node._x_marker)\n return;\n addedNodes.push(node);\n });\n }\n if (mutations[i].type === \"attributes\") {\n let el = mutations[i].target;\n let name = mutations[i].attributeName;\n let oldValue = mutations[i].oldValue;\n let add = () => {\n if (!addedAttributes.has(el))\n addedAttributes.set(el, []);\n addedAttributes.get(el).push({ name, value: el.getAttribute(name) });\n };\n let remove = () => {\n if (!removedAttributes.has(el))\n removedAttributes.set(el, []);\n removedAttributes.get(el).push(name);\n };\n if (el.hasAttribute(name) && oldValue === null) {\n add();\n } else if (el.hasAttribute(name)) {\n remove();\n add();\n } else {\n remove();\n }\n }\n }\n removedAttributes.forEach((attrs, el) => {\n cleanupAttributes(el, attrs);\n });\n addedAttributes.forEach((attrs, el) => {\n onAttributeAddeds.forEach((i) => i(el, attrs));\n });\n for (let node of removedNodes) {\n if (addedNodes.some((i) => i.contains(node)))\n continue;\n onElRemoveds.forEach((i) => i(node));\n }\n for (let node of addedNodes) {\n if (!node.isConnected)\n continue;\n onElAddeds.forEach((i) => i(node));\n }\n addedNodes = null;\n removedNodes = null;\n addedAttributes = null;\n removedAttributes = null;\n}\n\n// packages/alpinejs/src/scope.js\nfunction scope(node) {\n return mergeProxies(closestDataStack(node));\n}\nfunction addScopeToNode(node, data2, referenceNode) {\n node._x_dataStack = [data2, ...closestDataStack(referenceNode || node)];\n return () => {\n node._x_dataStack = node._x_dataStack.filter((i) => i !== data2);\n };\n}\nfunction closestDataStack(node) {\n if (node._x_dataStack)\n return node._x_dataStack;\n if (typeof ShadowRoot === \"function\" && node instanceof ShadowRoot) {\n return closestDataStack(node.host);\n }\n if (!node.parentNode) {\n return [];\n }\n return closestDataStack(node.parentNode);\n}\nfunction mergeProxies(objects) {\n return new Proxy({ objects }, mergeProxyTrap);\n}\nvar mergeProxyTrap = {\n ownKeys({ objects }) {\n return Array.from(\n new Set(objects.flatMap((i) => Object.keys(i)))\n );\n },\n has({ objects }, name) {\n if (name == Symbol.unscopables)\n return false;\n return objects.some(\n (obj) => Object.prototype.hasOwnProperty.call(obj, name) || Reflect.has(obj, name)\n );\n },\n get({ objects }, name, thisProxy) {\n if (name == \"toJSON\")\n return collapseProxies;\n return Reflect.get(\n objects.find(\n (obj) => Reflect.has(obj, name)\n ) || {},\n name,\n thisProxy\n );\n },\n set({ objects }, name, value, thisProxy) {\n const target = objects.find(\n (obj) => Object.prototype.hasOwnProperty.call(obj, name)\n ) || objects[objects.length - 1];\n const descriptor = Object.getOwnPropertyDescriptor(target, name);\n if ((descriptor == null ? void 0 : descriptor.set) && (descriptor == null ? void 0 : descriptor.get))\n return descriptor.set.call(thisProxy, value) || true;\n return Reflect.set(target, name, value);\n }\n};\nfunction collapseProxies() {\n let keys = Reflect.ownKeys(this);\n return keys.reduce((acc, key) => {\n acc[key] = Reflect.get(this, key);\n return acc;\n }, {});\n}\n\n// packages/alpinejs/src/interceptor.js\nfunction initInterceptors(data2) {\n let isObject = (val) => typeof val === \"object\" && !Array.isArray(val) && val !== null;\n let recurse = (obj, basePath = \"\") => {\n Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => {\n if (enumerable === false || value === void 0)\n return;\n if (typeof value === \"object\" && value !== null && value.__v_skip)\n return;\n let path = basePath === \"\" ? key : `${basePath}.${key}`;\n if (typeof value === \"object\" && value !== null && value._x_interceptor) {\n obj[key] = value.initialize(data2, path, key);\n } else {\n if (isObject(value) && value !== obj && !(value instanceof Element)) {\n recurse(value, path);\n }\n }\n });\n };\n return recurse(data2);\n}\nfunction interceptor(callback, mutateObj = () => {\n}) {\n let obj = {\n initialValue: void 0,\n _x_interceptor: true,\n initialize(data2, path, key) {\n return callback(this.initialValue, () => get(data2, path), (value) => set(data2, path, value), path, key);\n }\n };\n mutateObj(obj);\n return (initialValue) => {\n if (typeof initialValue === \"object\" && initialValue !== null && initialValue._x_interceptor) {\n let initialize = obj.initialize.bind(obj);\n obj.initialize = (data2, path, key) => {\n let innerValue = initialValue.initialize(data2, path, key);\n obj.initialValue = innerValue;\n return initialize(data2, path, key);\n };\n } else {\n obj.initialValue = initialValue;\n }\n return obj;\n };\n}\nfunction get(obj, path) {\n return path.split(\".\").reduce((carry, segment) => carry[segment], obj);\n}\nfunction set(obj, path, value) {\n if (typeof path === \"string\")\n path = path.split(\".\");\n if (path.length === 1)\n obj[path[0]] = value;\n else if (path.length === 0)\n throw error;\n else {\n if (obj[path[0]])\n return set(obj[path[0]], path.slice(1), value);\n else {\n obj[path[0]] = {};\n return set(obj[path[0]], path.slice(1), value);\n }\n }\n}\n\n// packages/alpinejs/src/magics.js\nvar magics = {};\nfunction magic(name, callback) {\n magics[name] = callback;\n}\nfunction injectMagics(obj, el) {\n let memoizedUtilities = getUtilities(el);\n Object.entries(magics).forEach(([name, callback]) => {\n Object.defineProperty(obj, `$${name}`, {\n get() {\n return callback(el, memoizedUtilities);\n },\n enumerable: false\n });\n });\n return obj;\n}\nfunction getUtilities(el) {\n let [utilities, cleanup] = getElementBoundUtilities(el);\n let utils = { interceptor, ...utilities };\n onElRemoved(el, cleanup);\n return utils;\n}\n\n// packages/alpinejs/src/utils/error.js\nfunction tryCatch(el, expression, callback, ...args) {\n try {\n return callback(...args);\n } catch (e) {\n handleError(e, el, expression);\n }\n}\nfunction handleError(error2, el, expression = void 0) {\n error2 = Object.assign(\n error2 != null ? error2 : { message: \"No error message given.\" },\n { el, expression }\n );\n console.warn(`Alpine Expression Error: ${error2.message}\n\n${expression ? 'Expression: \"' + expression + '\"\\n\\n' : \"\"}`, el);\n setTimeout(() => {\n throw error2;\n }, 0);\n}\n\n// packages/alpinejs/src/evaluator.js\nvar shouldAutoEvaluateFunctions = true;\nfunction dontAutoEvaluateFunctions(callback) {\n let cache = shouldAutoEvaluateFunctions;\n shouldAutoEvaluateFunctions = false;\n let result = callback();\n shouldAutoEvaluateFunctions = cache;\n return result;\n}\nfunction evaluate(el, expression, extras = {}) {\n let result;\n evaluateLater(el, expression)((value) => result = value, extras);\n return result;\n}\nfunction evaluateLater(...args) {\n return theEvaluatorFunction(...args);\n}\nvar theEvaluatorFunction = normalEvaluator;\nfunction setEvaluator(newEvaluator) {\n theEvaluatorFunction = newEvaluator;\n}\nfunction normalEvaluator(el, expression) {\n let overriddenMagics = {};\n injectMagics(overriddenMagics, el);\n let dataStack = [overriddenMagics, ...closestDataStack(el)];\n let evaluator = typeof expression === \"function\" ? generateEvaluatorFromFunction(dataStack, expression) : generateEvaluatorFromString(dataStack, expression, el);\n return tryCatch.bind(null, el, expression, evaluator);\n}\nfunction generateEvaluatorFromFunction(dataStack, func) {\n return (receiver = () => {\n }, { scope: scope2 = {}, params = [] } = {}) => {\n let result = func.apply(mergeProxies([scope2, ...dataStack]), params);\n runIfTypeOfFunction(receiver, result);\n };\n}\nvar evaluatorMemo = {};\nfunction generateFunctionFromString(expression, el) {\n if (evaluatorMemo[expression]) {\n return evaluatorMemo[expression];\n }\n let AsyncFunction = Object.getPrototypeOf(async function() {\n }).constructor;\n let rightSideSafeExpression = /^[\\n\\s]*if.*\\(.*\\)/.test(expression.trim()) || /^(let|const)\\s/.test(expression.trim()) ? `(async()=>{ ${expression} })()` : expression;\n const safeAsyncFunction = () => {\n try {\n let func2 = new AsyncFunction(\n [\"__self\", \"scope\"],\n `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`\n );\n Object.defineProperty(func2, \"name\", {\n value: `[Alpine] ${expression}`\n });\n return func2;\n } catch (error2) {\n handleError(error2, el, expression);\n return Promise.resolve();\n }\n };\n let func = safeAsyncFunction();\n evaluatorMemo[expression] = func;\n return func;\n}\nfunction generateEvaluatorFromString(dataStack, expression, el) {\n let func = generateFunctionFromString(expression, el);\n return (receiver = () => {\n }, { scope: scope2 = {}, params = [] } = {}) => {\n func.result = void 0;\n func.finished = false;\n let completeScope = mergeProxies([scope2, ...dataStack]);\n if (typeof func === \"function\") {\n let promise = func(func, completeScope).catch((error2) => handleError(error2, el, expression));\n if (func.finished) {\n runIfTypeOfFunction(receiver, func.result, completeScope, params, el);\n func.result = void 0;\n } else {\n promise.then((result) => {\n runIfTypeOfFunction(receiver, result, completeScope, params, el);\n }).catch((error2) => handleError(error2, el, expression)).finally(() => func.result = void 0);\n }\n }\n };\n}\nfunction runIfTypeOfFunction(receiver, value, scope2, params, el) {\n if (shouldAutoEvaluateFunctions && typeof value === \"function\") {\n let result = value.apply(scope2, params);\n if (result instanceof Promise) {\n result.then((i) => runIfTypeOfFunction(receiver, i, scope2, params)).catch((error2) => handleError(error2, el, value));\n } else {\n receiver(result);\n }\n } else if (typeof value === \"object\" && value instanceof Promise) {\n value.then((i) => receiver(i));\n } else {\n receiver(value);\n }\n}\n\n// packages/alpinejs/src/directives.js\nvar prefixAsString = \"x-\";\nfunction prefix(subject = \"\") {\n return prefixAsString + subject;\n}\nfunction setPrefix(newPrefix) {\n prefixAsString = newPrefix;\n}\nvar directiveHandlers = {};\nfunction directive(name, callback) {\n directiveHandlers[name] = callback;\n return {\n before(directive2) {\n if (!directiveHandlers[directive2]) {\n console.warn(String.raw`Cannot find directive \\`${directive2}\\`. \\`${name}\\` will use the default order of execution`);\n return;\n }\n const pos = directiveOrder.indexOf(directive2);\n directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf(\"DEFAULT\"), 0, name);\n }\n };\n}\nfunction directiveExists(name) {\n return Object.keys(directiveHandlers).includes(name);\n}\nfunction directives(el, attributes, originalAttributeOverride) {\n attributes = Array.from(attributes);\n if (el._x_virtualDirectives) {\n let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value }));\n let staticAttributes = attributesOnly(vAttributes);\n vAttributes = vAttributes.map((attribute) => {\n if (staticAttributes.find((attr) => attr.name === attribute.name)) {\n return {\n name: `x-bind:${attribute.name}`,\n value: `\"${attribute.value}\"`\n };\n }\n return attribute;\n });\n attributes = attributes.concat(vAttributes);\n }\n let transformedAttributeMap = {};\n let directives2 = attributes.map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName)).filter(outNonAlpineAttributes).map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride)).sort(byPriority);\n return directives2.map((directive2) => {\n return getDirectiveHandler(el, directive2);\n });\n}\nfunction attributesOnly(attributes) {\n return Array.from(attributes).map(toTransformedAttributes()).filter((attr) => !outNonAlpineAttributes(attr));\n}\nvar isDeferringHandlers = false;\nvar directiveHandlerStacks = /* @__PURE__ */ new Map();\nvar currentHandlerStackKey = Symbol();\nfunction deferHandlingDirectives(callback) {\n isDeferringHandlers = true;\n let key = Symbol();\n currentHandlerStackKey = key;\n directiveHandlerStacks.set(key, []);\n let flushHandlers = () => {\n while (directiveHandlerStacks.get(key).length)\n directiveHandlerStacks.get(key).shift()();\n directiveHandlerStacks.delete(key);\n };\n let stopDeferring = () => {\n isDeferringHandlers = false;\n flushHandlers();\n };\n callback(flushHandlers);\n stopDeferring();\n}\nfunction getElementBoundUtilities(el) {\n let cleanups = [];\n let cleanup = (callback) => cleanups.push(callback);\n let [effect3, cleanupEffect] = elementBoundEffect(el);\n cleanups.push(cleanupEffect);\n let utilities = {\n Alpine: alpine_default,\n effect: effect3,\n cleanup,\n evaluateLater: evaluateLater.bind(evaluateLater, el),\n evaluate: evaluate.bind(evaluate, el)\n };\n let doCleanup = () => cleanups.forEach((i) => i());\n return [utilities, doCleanup];\n}\nfunction getDirectiveHandler(el, directive2) {\n let noop = () => {\n };\n let handler4 = directiveHandlers[directive2.type] || noop;\n let [utilities, cleanup] = getElementBoundUtilities(el);\n onAttributeRemoved(el, directive2.original, cleanup);\n let fullHandler = () => {\n if (el._x_ignore || el._x_ignoreSelf)\n return;\n handler4.inline && handler4.inline(el, directive2, utilities);\n handler4 = handler4.bind(handler4, el, directive2, utilities);\n isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler4) : handler4();\n };\n fullHandler.runCleanups = cleanup;\n return fullHandler;\n}\nvar startingWith = (subject, replacement) => ({ name, value }) => {\n if (name.startsWith(subject))\n name = name.replace(subject, replacement);\n return { name, value };\n};\nvar into = (i) => i;\nfunction toTransformedAttributes(callback = () => {\n}) {\n return ({ name, value }) => {\n let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => {\n return transform(carry);\n }, { name, value });\n if (newName !== name)\n callback(newName, name);\n return { name: newName, value: newValue };\n };\n}\nvar attributeTransformers = [];\nfunction mapAttributes(callback) {\n attributeTransformers.push(callback);\n}\nfunction outNonAlpineAttributes({ name }) {\n return alpineAttributeRegex().test(name);\n}\nvar alpineAttributeRegex = () => new RegExp(`^${prefixAsString}([^:^.]+)\\\\b`);\nfunction toParsedDirectives(transformedAttributeMap, originalAttributeOverride) {\n return ({ name, value }) => {\n let typeMatch = name.match(alpineAttributeRegex());\n let valueMatch = name.match(/:([a-zA-Z0-9\\-_:]+)/);\n let modifiers = name.match(/\\.[^.\\]]+(?=[^\\]]*$)/g) || [];\n let original = originalAttributeOverride || transformedAttributeMap[name] || name;\n return {\n type: typeMatch ? typeMatch[1] : null,\n value: valueMatch ? valueMatch[1] : null,\n modifiers: modifiers.map((i) => i.replace(\".\", \"\")),\n expression: value,\n original\n };\n };\n}\nvar DEFAULT = \"DEFAULT\";\nvar directiveOrder = [\n \"ignore\",\n \"ref\",\n \"data\",\n \"id\",\n \"anchor\",\n \"bind\",\n \"init\",\n \"for\",\n \"model\",\n \"modelable\",\n \"transition\",\n \"show\",\n \"if\",\n DEFAULT,\n \"teleport\"\n];\nfunction byPriority(a, b) {\n let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type;\n let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type;\n return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB);\n}\n\n// packages/alpinejs/src/utils/dispatch.js\nfunction dispatch(el, name, detail = {}) {\n el.dispatchEvent(\n new CustomEvent(name, {\n detail,\n bubbles: true,\n // Allows events to pass the shadow DOM barrier.\n composed: true,\n cancelable: true\n })\n );\n}\n\n// packages/alpinejs/src/utils/walk.js\nfunction walk(el, callback) {\n if (typeof ShadowRoot === \"function\" && el instanceof ShadowRoot) {\n Array.from(el.children).forEach((el2) => walk(el2, callback));\n return;\n }\n let skip = false;\n callback(el, () => skip = true);\n if (skip)\n return;\n let node = el.firstElementChild;\n while (node) {\n walk(node, callback, false);\n node = node.nextElementSibling;\n }\n}\n\n// packages/alpinejs/src/utils/warn.js\nfunction warn(message, ...args) {\n console.warn(`Alpine Warning: ${message}`, ...args);\n}\n\n// packages/alpinejs/src/lifecycle.js\nvar started = false;\nfunction start() {\n if (started)\n warn(\"Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.\");\n started = true;\n if (!document.body)\n warn(\"Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `