diff --git a/.phpunit.result.cache b/.phpunit.result.cache index c8d15b8..5022604 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":8,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":8,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":8,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":8,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":8,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":8,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":7,"Tests\\Feature\\AiStatusTest::test_ai_status_endpoint_returns_correct_structure":8,"Tests\\Feature\\AiStatusTest::test_ai_status_update_disables_unavailable_providers":7,"Tests\\Feature\\AiStatusTest::test_comfyui_check_availability_method":8,"Tests\\Feature\\AiStatusTest::test_runwareai_check_availability_method":8,"Tests\\Feature\\GalleryAccessTest::test_allows_access_after_correct_password":8,"Tests\\Feature\\GalleryAccessTest::test_redirects_to_access_page_when_password_is_required":7,"Tests\\Feature\\GalleryAccessTest::test_denies_access_when_gallery_expired":8,"Tests\\Feature\\SparkboothUploadTest::test_json_upload_with_credentials_succeeds":7,"Tests\\Feature\\SparkboothUploadTest::test_base64_upload_with_token_succeeds":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":2.652,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.844,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.003,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.003,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.011,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.685,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.079,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.003,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.653,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":2.449,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":3.546,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.003,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.008,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.005,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.004,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.004,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":6.542,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":3.055,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":3.132,"Tests\\Feature\\AiStatusTest::test_ai_status_endpoint_returns_correct_structure":0.637,"Tests\\Feature\\AiStatusTest::test_ai_status_update_disables_unavailable_providers":1.569,"Tests\\Feature\\AiStatusTest::test_comfyui_check_availability_method":0.228,"Tests\\Feature\\AiStatusTest::test_runwareai_check_availability_method":0.233,"Tests\\Feature\\GalleryAccessTest::test_redirects_to_access_page_when_password_is_required":9.523,"Tests\\Feature\\GalleryAccessTest::test_allows_access_after_correct_password":3.424,"Tests\\Feature\\GalleryAccessTest::test_denies_access_when_gallery_expired":3.086,"Tests\\Feature\\Filament\\SparkboothConnectionsTest::test_admin_can_delete_connection_without_deleting_gallery":4.826,"Tests\\Feature\\SparkboothUploadTest::test_json_upload_with_credentials_succeeds":3.438,"Tests\\Feature\\SparkboothUploadTest::test_xml_response_for_invalid_credentials":2.015,"Tests\\Feature\\SparkboothUploadTest::test_base64_upload_with_token_succeeds":1.3}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":8,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":8,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":8,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":8,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":8,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":8,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":7,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":8,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":8,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":8,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":8,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":8,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":7,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":7,"Tests\\Feature\\AiStatusTest::test_ai_status_endpoint_returns_correct_structure":8,"Tests\\Feature\\AiStatusTest::test_ai_status_update_disables_unavailable_providers":7,"Tests\\Feature\\AiStatusTest::test_comfyui_check_availability_method":8,"Tests\\Feature\\AiStatusTest::test_runwareai_check_availability_method":8,"Tests\\Feature\\GalleryAccessTest::test_allows_access_after_correct_password":8,"Tests\\Feature\\GalleryAccessTest::test_redirects_to_access_page_when_password_is_required":7,"Tests\\Feature\\GalleryAccessTest::test_denies_access_when_gallery_expired":8,"Tests\\Feature\\SparkboothUploadTest::test_json_upload_with_credentials_succeeds":7,"Tests\\Feature\\SparkboothUploadTest::test_base64_upload_with_token_succeeds":7,"Tests\\Feature\\AdminPinLoginTest::test_admin_can_authenticate_with_valid_pin":7,"Tests\\Feature\\AdminPinLoginTest::test_admin_cannot_authenticate_with_invalid_pin":8,"Tests\\Feature\\Filament\\EditProfilePinTest::test_admin_can_set_pin_from_profile":7,"Tests\\Feature\\Filament\\EditProfilePinTest::test_admin_can_remove_pin_from_profile":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\Auth\\AuthenticationTest::test_login_screen_can_be_rendered":2.652,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_authenticate_using_the_login_screen":0.844,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_not_authenticate_with_invalid_password":0.003,"Tests\\Feature\\Auth\\AuthenticationTest::test_users_can_logout":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_verification_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_can_be_verified":0.003,"Tests\\Feature\\Auth\\EmailVerificationTest::test_email_is_not_verified_with_invalid_hash":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_confirm_password_screen_can_be_rendered":0.003,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_can_be_confirmed":0.004,"Tests\\Feature\\Auth\\PasswordConfirmationTest::test_password_is_not_confirmed_with_invalid_password":0.011,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_screen_can_be_rendered":0.685,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_link_can_be_requested":0.079,"Tests\\Feature\\Auth\\PasswordResetTest::test_reset_password_screen_can_be_rendered":0.004,"Tests\\Feature\\Auth\\PasswordResetTest::test_password_can_be_reset_with_valid_token":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_password_can_be_updated":0.003,"Tests\\Feature\\Auth\\PasswordUpdateTest::test_correct_password_must_be_provided_to_update_password":0.003,"Tests\\Feature\\Auth\\RegistrationTest::test_registration_screen_can_be_rendered":0.653,"Tests\\Feature\\Auth\\RegistrationTest::test_new_users_can_register":2.449,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":3.546,"Tests\\Feature\\ProfileTest::test_profile_page_is_displayed":0.003,"Tests\\Feature\\ProfileTest::test_profile_information_can_be_updated":0.008,"Tests\\Feature\\ProfileTest::test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged":0.005,"Tests\\Feature\\ProfileTest::test_user_can_delete_their_account":0.004,"Tests\\Feature\\ProfileTest::test_correct_password_must_be_provided_to_delete_account":0.004,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_index_pages_render":6.542,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_create_pages_render":3.055,"Tests\\Feature\\Filament\\ResourcePagesTest::test_admin_resource_edit_pages_render":3.132,"Tests\\Feature\\AiStatusTest::test_ai_status_endpoint_returns_correct_structure":0.637,"Tests\\Feature\\AiStatusTest::test_ai_status_update_disables_unavailable_providers":1.569,"Tests\\Feature\\AiStatusTest::test_comfyui_check_availability_method":0.228,"Tests\\Feature\\AiStatusTest::test_runwareai_check_availability_method":0.233,"Tests\\Feature\\GalleryAccessTest::test_redirects_to_access_page_when_password_is_required":9.523,"Tests\\Feature\\GalleryAccessTest::test_allows_access_after_correct_password":3.424,"Tests\\Feature\\GalleryAccessTest::test_denies_access_when_gallery_expired":3.086,"Tests\\Feature\\Filament\\SparkboothConnectionsTest::test_admin_can_delete_connection_without_deleting_gallery":4.826,"Tests\\Feature\\SparkboothUploadTest::test_json_upload_with_credentials_succeeds":3.438,"Tests\\Feature\\SparkboothUploadTest::test_xml_response_for_invalid_credentials":2.015,"Tests\\Feature\\SparkboothUploadTest::test_base64_upload_with_token_succeeds":1.3,"Tests\\Feature\\AdminPinLoginTest::test_admin_can_authenticate_with_valid_pin":5.401,"Tests\\Feature\\AdminPinLoginTest::test_admin_cannot_authenticate_with_invalid_pin":1.964,"Tests\\Feature\\Filament\\EditProfilePinTest::test_admin_can_set_pin_from_profile":6.513,"Tests\\Feature\\Filament\\EditProfilePinTest::test_admin_can_remove_pin_from_profile":1.986}} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 4ccb39a..2e964a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,8 +8,8 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## 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.3.24 -- filament/filament (FILAMENT) - v3 +- php - 8.3.29 +- filament/filament (FILAMENT) - v4 - inertiajs/inertia-laravel (INERTIA) - v1 - laravel/breeze (BREEZE) - v2 - laravel/framework (LARAVEL) - v12 @@ -23,11 +23,11 @@ This application is a Laravel application and its main Laravel ecosystems packag - phpunit/phpunit (PHPUNIT) - v12 - laravel-echo (ECHO) - v2 - @inertiajs/vue3 (INERTIA) - v1 -- tailwindcss (TAILWINDCSS) - v3 +- tailwindcss (TAILWINDCSS) - v4 - vue (VUE) - v3 ## 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. +- 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, and naming. - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. - Check for existing components to reuse before writing a new one. @@ -35,7 +35,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - 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. +- Stick to existing directory structure; don't create new base folders without approval. - Do not change the application's dependencies without approval. ## Frontend Bundling @@ -47,17 +47,16 @@ This application is a Laravel application and its main Laravel ecosystems packag ## 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. +- 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. +- 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. @@ -68,22 +67,21 @@ This application is a Laravel application and its main Laravel ecosystems packag - 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. +- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation 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`. +- 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 - +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 === @@ -94,7 +92,7 @@ This application is a Laravel application and its main Laravel ecosystems packag ### Constructors - Use PHP 8 constructor property promotion in `__construct()`. - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. ### Type Declarations - Always use explicit return type declarations for methods and functions. @@ -108,7 +106,7 @@ 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. +- Prefer PHPDoc blocks over inline 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. @@ -116,16 +114,29 @@ protected function isAccessible(User $user, ?string $path = null): bool ## Enums - Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. +=== herd rules === + +## Laravel Herd + +- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. +- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd. + +=== 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 --compact` with a specific filename or filter. === inertia-laravel/core rules === -## Inertia Core +## Inertia -- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js). +- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (`vite.config.js`). - Use `Inertia::render()` for server-side routing instead of traditional Blade views. -- Use `search-docs` for accurate guidance on all things Inertia. +- Use the `search-docs` tool for accurate guidance on all things Inertia. - + // routes/web.php example Route::get('/users', function () { return Inertia::render('Users/Index', [ @@ -134,30 +145,28 @@ Route::get('/users', function () { }); - === inertia-laravel/v1 rules === ## Inertia v1 -- Inertia v1 does _not_ come with these features. Do not recommend using these Inertia v2 features directly. - - Polling - - Prefetching - - Deferred props - - Infinite scrolling using merging props and `WhenVisible` - - Lazy loading data on scroll - +- Inertia v1 does not come with these features. Do not recommend using these Inertia v2 features directly: + - Deferred props. + - Infinite scrolling using merging props and `WhenVisible`. + - Lazy loading data on scroll. + - Polling. + - Prefetching. === 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`. +- If you're creating a generic PHP class, use `php 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 +- 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. @@ -187,41 +196,41 @@ Route::get('/users', function () { ### 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. +- When creating tests, make use of `php artisan make:test [options] {name}` 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. +- 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/`. +- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`. +- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`. - `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. +- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration. +- Console commands 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);`. +- Laravel 12 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 +## Livewire + +- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and 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. +- 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. @@ -238,15 +247,14 @@ Route::get('/users', function () { - 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') @@ -255,19 +263,17 @@ Route::get('/users', function () { ->assertStatus(200); - - - $this->get('/posts/create') - ->assertSeeLivewire(CreatePost::class); - - + + $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. +- These things changed in Livewire 3, 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`). @@ -277,13 +283,13 @@ Route::get('/users', function () { - `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. +- 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) { @@ -297,7 +303,6 @@ document.addEventListener('livewire:init', function () { }); - === pint/core rules === ## Laravel Pint Code Formatter @@ -305,24 +310,22 @@ document.addEventListener('livewire:init', function () { - 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. - === phpunit/core rules === -## PHPUnit Core +## PHPUnit -- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit ` to create a new test. +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. - If you see a test using "Pest", convert it to PHPUnit. - Every time a test has been updated, run that singular test. - When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. - Tests should test all of the happy paths, failure paths, and weird paths. -- 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. +- 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. ### Running Tests - Run the minimal number of tests, using an appropriate filter, before finalizing. -- 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). - +- To run all tests: `php artisan test --compact`. +- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). === inertia-vue/core rules === @@ -338,15 +341,13 @@ document.addEventListener('livewire:init', function () { - === inertia-vue/v1/forms rules === -## Inertia + Vue Forms +## Inertia v1 + Vue Forms - For form handling in Inertia pages, use `router.post` and related methods. Do not use regular forms. - - + diff --git a/resources/views/filament/pages/auth/kiosk-login.blade.php b/resources/views/filament/pages/auth/kiosk-login.blade.php new file mode 100644 index 0000000..7411300 --- /dev/null +++ b/resources/views/filament/pages/auth/kiosk-login.blade.php @@ -0,0 +1,137 @@ +@php + $selectedEmail = $get('email'); + $pin = (string) ($get('pin') ?? ''); + $pinLength = strlen($pin); +@endphp + +
+ @if (! $hasPinUsers) + + + + Password login enabled + + + + @else + +
+ @foreach ($users as $user) +
+ +
+
{{ $user->name }}
+
{{ $user->email }}
+
+
+
+ @endforeach +
+
+ + + + + Clear + + + + + {{ $pinLength ? str_repeat('*', $pinLength) : '----' }} + + + + + @foreach ([[1, 2, 3], [4, 5, 6], [7, 8, 9]] as $row) + + @foreach ($row as $digit) + + @endforeach + + @endforeach + + + + + + +
+ + {{ $digit }} + +
+ + Clear + + + + 0 + + + + Del + +
+
+ + @error('data.email') + + {{ $message }} + + @enderror + @endif +
diff --git a/tests/Feature/AdminPinLoginTest.php b/tests/Feature/AdminPinLoginTest.php new file mode 100644 index 0000000..f0abbb6 --- /dev/null +++ b/tests/Feature/AdminPinLoginTest.php @@ -0,0 +1,67 @@ +createAdminUser(); + $this->assertTrue(Hash::check('1234', $user->admin_pin_hash)); + + Livewire::test(Login::class) + ->call('selectUser', $user->id) + ->assertSet('data.email', $user->email) + ->call('appendPinDigit', 1) + ->call('appendPinDigit', 2) + ->call('appendPinDigit', 3) + ->call('appendPinDigit', 4) + ->assertSet('data.pin', '1234') + ->call('authenticate') + ->assertHasNoErrors() + ->assertRedirect(Filament::getUrl()); + } + + public function test_admin_cannot_authenticate_with_invalid_pin(): void + { + $user = $this->createAdminUser(); + + Livewire::test(Login::class) + ->call('selectUser', $user->id) + ->assertSet('data.email', $user->email) + ->call('appendPinDigit', 0) + ->call('appendPinDigit', 0) + ->call('appendPinDigit', 0) + ->call('appendPinDigit', 0) + ->assertSet('data.pin', '0000') + ->call('authenticate') + ->assertHasErrors(['data.email']); + + $this->assertGuest(); + } + + protected function createAdminUser(): User + { + $role = Role::firstOrCreate(['name' => 'Admin']); + + return User::factory()->create([ + 'role_id' => $role->id, + 'admin_pin_hash' => Hash::make('1234'), + ]); + } +} diff --git a/tests/Feature/Filament/EditProfilePinTest.php b/tests/Feature/Filament/EditProfilePinTest.php new file mode 100644 index 0000000..d5899e3 --- /dev/null +++ b/tests/Feature/Filament/EditProfilePinTest.php @@ -0,0 +1,62 @@ +createAdminUser(); + $this->actingAs($user); + + Livewire::test(EditProfile::class) + ->set('data.admin_pin', '4321') + ->call('save') + ->assertHasNoErrors(); + + $user->refresh(); + + $this->assertTrue(Hash::check('4321', $user->admin_pin_hash)); + } + + public function test_admin_can_remove_pin_from_profile(): void + { + $user = $this->createAdminUser([ + 'admin_pin_hash' => Hash::make('5678'), + ]); + $this->actingAs($user); + + Livewire::test(EditProfile::class) + ->set('data.remove_admin_pin', true) + ->call('save') + ->assertHasNoErrors(); + + $user->refresh(); + + $this->assertNull($user->admin_pin_hash); + } + + protected function createAdminUser(array $overrides = []): User + { + $role = Role::firstOrCreate(['name' => 'Admin']); + + return User::factory()->create(array_merge([ + 'role_id' => $role->id, + ], $overrides)); + } +} diff --git a/update-prod.bat b/update-prod.bat new file mode 100644 index 0000000..d50851f --- /dev/null +++ b/update-prod.bat @@ -0,0 +1,44 @@ +@echo off +setlocal + +set "RUN_NODE=auto" + +call :run "git pull --ff-only" "Git pull" +call :run "composer install --no-interaction --no-dev --optimize-autoloader" "Composer install" +call :run "php artisan migrate --force" "Migrations" +call :run "php artisan config:cache" "Config cache" +call :run "php artisan route:cache" "Route cache" +call :run "php artisan view:cache" "View cache" +call :run "php artisan optimize" "Optimize" + +if /i "%RUN_NODE%"=="1" goto :run_node +if /i "%RUN_NODE%"=="auto" goto :maybe_node +goto :done + +:maybe_node +where npm >nul 2>nul +if errorlevel 1 goto :done + +:run_node +if exist package-lock.json ( + call :run "npm ci" "NPM install" +) else ( + call :run "npm install" "NPM install" +) +call :run "npm run build" "NPM build" +goto :done + +:run +set "CMD=%~1" +set "LABEL=%~2" +if "%LABEL%"=="" set "LABEL=%CMD%" +echo == %LABEL% +%CMD% +if errorlevel 1 ( + echo Command failed: %CMD% + exit /b 1 +) +exit /b 0 + +:done +echo Update complete. diff --git a/vite.config.mjs b/vite.config.mjs index f4f7e3a..77264e4 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -8,7 +8,10 @@ export default defineConfig({ }, plugins: [ laravel({ - input: 'resources/js/app.js', + input: [ + 'resources/js/app.js', + 'resources/css/filament/admin/theme.css', + ], refresh: true, }), vue({