Files
ppeasy_vg12/CLAUDE.md
T
2026-10-09 08:21:06 +02:00

262 lines
21 KiB
Markdown

<laravel-boost-guidelines>
=== 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 ensure the best experience when building Laravel applications.
## Foundational Context
This application is a Laravel application running on PHP 8.4. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version.
Before relying on a package's API, confirm its installed version:
- PHP packages: run `composer show --direct` to list direct dependencies with versions, or `composer show <vendor/package>` for a single package.
- JS packages: check `package.json` for the installed versions.
## Skills Activation
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
## 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, 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.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. 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.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
# Laravel Boost
## Tools
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
- Use `database-schema` to inspect table structure before writing migrations or models.
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
## Searching Documentation (IMPORTANT)
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
- Pass a `packages` array to scope results when you know which packages are relevant.
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
### Search Syntax
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
## Project Rules
- This project contains committed, area-grouped rules in `.ai/rules` when that directory exists (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under `.ai/rules/boost` — this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and run `grep -rin 'keyword' .ai/rules` to catch what a path match alone misses. Do not write code until you have read and are following every matching rule. If `.ai/rules` does not exist, continue without it.
- Record durable rules with `record-rule` so the next agent or teammate inherits them instead of working them out again. Pass a `glob` (e.g. `app/Http/Controllers/**`), a short `title`, and a few-line `note`. Always use `record-rule`, never your native memory or notes tool — native memory is personal and session-scoped; only `.ai/rules` is shared with the team and persists in the repo.
## Artisan
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
## Tinker
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
=== php rules ===
# PHP
- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
- Follow existing application Enum naming conventions.
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.
=== deployments rules ===
# Deployment
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
=== herd rules ===
# Laravel Herd
- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available.
- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands.
=== 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
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
- Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
# Inertia v3
- Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach.
- New v3 features: standalone HTTP requests (`useHttp` hook), optimistic updates with automatic rollback, layout props (`useLayoutProps` hook), instant visits, simplified SSR via `@inertiajs/vite` plugin, custom exception handling for error pages.
- Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data.
- When using deferred props, add an empty state with a pulsing or animated skeleton.
- Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed.
- `Inertia::lazy()` / `LazyProp` has been removed. Use `Inertia::optional()` instead.
- Prop types (`Inertia::optional()`, `Inertia::defer()`, `Inertia::merge()`) work inside nested arrays with dot-notation paths.
- SSR works automatically in Vite dev mode with `@inertiajs/vite` - no separate Node.js server needed during development.
- Event renames: `invalid` is now `httpException`, `exception` is now `networkError`.
- `router.cancel()` replaced by `router.cancelAll()`.
- The `future` configuration namespace has been removed - all v2 future options are now always enabled.
=== 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 `php artisan list` and check their parameters with `php artisan [command] --help`.
- 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.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
## 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.
## URL Generation
- When generating links to other pages, prefer named routes and the `route()` 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] {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`.
=== pint/core rules ===
# Laravel Pint Code Formatter
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
=== pest/core rules ===
## Pest
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
=== inertia-vue/core rules ===
# Inertia + Vue
Vue components must have a single root element.
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
</laravel-boost-guidelines>
# Vanguard
## Frontend
- The UI is Inertia + Vue 3 with TypeScript. There is no Blade UI: the only Blade left is `resources/views/app.blade.php` (the Inertia root), the installation wizard, and the mail templates.
- Components live in `resources/js/components/ui/` and are ours. They wrap Reka UI primitives, which supply focus management, keyboard interaction and ARIA; everything visual is plain Tailwind in those files. Reach for one before writing a new component.
- Server data reaches pages as Inertia props. Shared props are defined in `App\Http\Middleware\HandleInertiaRequests`; per-page shapes live in `App\Support\*Payload` classes rather than being assembled inline in controllers.
- `route()` comes from Ziggy, whose route list is emitted once per full page load by `@routes`. Do not re-add it as a shared prop.
- `__()` in Vue comes from `useTranslations()`. It falls back to the key on an empty string, matching Laravel's `$line ?: $key`.
## Plugins
- Plugins are separate composer packages that ship their own compiled Vue bundle to `public/vendor/plugins/{slug}`. Installing one never requires rebuilding the host.
- A bundle registers its pages, slots and widgets against `window.Vanguard` and externalises Vue, Inertia and the UI kit onto that runtime, so it shares the host's single Vue instance.
- `resources/js/plugin-api.ts` is what plugins import as `@vanguard/ui`. Removing an export from it is a breaking change for third party plugins.
- Plugins contribute UI through slots (`Vanguard::slot()`), never by reaching into host views.
## Demo mode
- Turned on with `DEMO_MODE` in `.env`, read once by `config/demo.php`. Never move it into the settings store: blocking settings writes is demo mode's main job, so a toggle living there would be either unreachable to the maintainer or reachable to any visitor on the shared account.
- The whole policy is data in `config/demo.php`. `App\Http\Middleware\DemoMode` is the only enforcement point and no controller knows demo mode exists — the opposite of the `demo_v8_1` branch, which pasted a guard into fifteen actions.
- `App\Support\Demo::isProtectedUser()` is the single definition of "the demo account". Never compare a username to `admin` at a call site.
- Rules match the route **name** and the request **path**, because most of `routes/api.php` is unnamed and a name-only matcher leaves the API half guarded. Dots are names, slashes are paths.
- `blocked` is consulted after the safe-verb pass, so `settings.*` refuses saving without hiding the settings pages. GET routes that mutate (`install.installation.start`) go in `unsafe_reads`, which is checked before it.
- Impersonation is deliberately allowed in the demo — it is a feature the demo exists to show off, and it grants nothing, because the rules follow the impersonated identity. Taking over the demo account still cannot modify it.
- The middleware is appended to the `web` and `api` groups, both of which run `SubstituteBindings` first — the rules read the route-bound `{user}`, `{role}` and `{permission}` models, so it must stay after it. Its position relative to `HandleInertiaRequests` does not matter: Inertia's service provider pushes a *global* `EnsureGetOnRedirect` that rewrites the refusal's 302 into a 303 for PUT/PATCH/DELETE, which is what stops the browser repeating the verb. Do not "fix" that by moving the middleware; the 303 does not come from the group.
- Refusals use `back()->with('error', ...)`, not `withErrors()`. Only `AuthLayout` renders an `errors[0]` banner, so on an authenticated page `withErrors()` shows nothing.
- `phpunit.xml` pins `DEMO_MODE` off, so the suite keeps its meaning when run on the demo box, whose `.env` turns it on. Spelled `(false)` and not `false`: PHPUnit parses a bare `false` into a boolean and then puts the *empty string* in the environment, which `env()` returns verbatim.
- Being a group middleware, it runs before `auth` and `permission:`. A visitor without the permission sees the demo message rather than a 403. That is deliberate — `abort(403)` renders the stock error page, which an Inertia visit cannot parse.
## Testing
- Feature tests assert against the Inertia payload, not rendered markup. `$response->viewData('page')` gives the whole page object; `assertInertia()` covers the common cases.
- Plugin suites live in their own repositories and run from there against a Vanguard checkout. See each plugin's readme.
- Every test in this project is Pest, in Vanguard and in both official plugins. Only the base classes (`tests/TestCase.php`, `tests/Feature/ApiTestCase.php`) are still PHP classes, bound to their directories by `tests/Pest.php`. This overrides the "convert Pest to PHPUnit" rule in the Boost block above, which is generated and cannot be edited.
- Assertions stayed as `$this->assert*()` rather than becoming `expect()`. Match that in existing files; new files may use either.
- Browser tests are the ones in the `browser` group, under `tests/Browser`. They are excluded from `php artisan test` because they need Playwright and a built frontend. Run them with `composer test:browser`, which builds first.
### Browser test gotchas
- `assertNoJavaScriptErrors()` does not catch a Vue render failure — deleting `@routes` leaves `route()` undefined and renders an empty page while that assertion still passes. Always assert on something the page must actually render.
- `click()` on an element that does not exist **hangs forever** rather than failing. Verify every label against the component source before using it.
- Text locators match any element, not just controls: `click('Register')` finds the `<h5>` heading. Use `button[type="submit"]` where a heading shares the button's label.
- Repeated controls (a table's row actions) are a strict-mode violation. Scope them: `tr:has-text("doomed") [aria-label="Delete User"]`.
- A selector with no CSS special character is treated as **text**, not CSS (`GuessLocator` falls back to `getByText`). `assertMissing('nav ol')` therefore passes on any page, because the text never matches. Give the element an `id`, a `data-testid` or an `aria-label` and select on that.
- `assertPathIs` does not wait for navigation but `assertSee` does, so put a content assertion first after any click that navigates.
- After a navigation, `$page->url()`, `script()` and `content()` still report the pre-navigation page. The `assert*` methods do see the current page; use them rather than those readers.
- `browserLogin()` must be called inside the test, never from `beforeEach` — Pest connects the browser client as the test starts.
- The Reka-based `Checkbox` and `Switch` render a button with `aria-checked`, not a native input, so assert with `assertAttribute(...)` and toggle with `click()`.
- A stale `vendor/pestphp/pest-plugin-browser/.temp/playwright-server.json` wedges the next run with "WebSocket client is not connected". `composer test:browser` deletes it first; if you run `pest --group=browser` directly, delete it yourself.
### Visual tests
- `tests/Visual/` holds two things, both in the `visual` group and both excluded from `php artisan test` and `composer test:browser`. Run them with `composer test:visual`, which builds the frontend and drives Chrome, Firefox and Safari in turn.
- `GalleryTest.php` captures every page at three viewports and files the images under `tests/Browser/Gallery/{browser}`, with a browsable `index.html`. It asserts nothing about appearance, but every capture still asserts on content first — a silent screenshot will happily photograph a blank page.
- `RegressionTest.php` pixel diffs a handful of static pages, **Chrome desktop only**. Pest keys a snapshot on test file, test name and dataset with nothing for the browser or device, so a second engine would compare against Chrome's baseline and always fail.
- `assertScreenshotMatches()` is byte exact. Its `maxDiffPixels`/`threshold` options only decide whether you get a diff image; they never make the assertion pass. Treat any rendering jitter as a failure and keep diffed pages free of dates, faker data and canvas.
- Baselines (`tests/.pest`) and captures (`tests/Browser/Gallery`) are gitignored. Font rasterisation differs between operating systems, so a baseline captured here would fail on a buyer's Linux or Windows machine. Regenerate with `--update-snapshots`, inspect a failure with `--diff`.
- The gallery and the diffing run as separate `pest` invocations because global state leaks between them: the settings store and `session.driver` are process wide, and the gallery changes both.
- Selection is by file path, not by group. A `--group=gallery` on the command line does not override the `<exclude><group>visual</group></exclude>` in `phpunit.xml`, so extra sub-groups are silently filtered out.
- Log in with `visualLogin()`, not `browserLogin()`. The latter mints a faker user whose random name lands in the navbar and the users table.
- A test is marked as a browser test by finding `visit()` **in the test itself**. Wrapping every `visit()` in a helper leaves the test unmarked and it fails with "WebSocket client is not connected" — the same error as calling `visit()` from `beforeEach`.
- The navbar avatar is hidden before each pixel diff. It is a raster image scaled into a 44px box and Chrome picks its scaling path by decode timing, so the same 149 pixels flip between two renders on whichever authenticated page loses the race. `networkidle` and a settle delay both failed to pin it.