Compare commits
1 Commits
dev
...
179-provid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e54c66321c |
@ -1,9 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
apps/platform/node_modules/
|
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
vendor/
|
vendor/
|
||||||
apps/platform/vendor/
|
|
||||||
coverage/
|
coverage/
|
||||||
.git/
|
.git/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -20,19 +18,12 @@ Dockerfile*
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.swp
|
*.swp
|
||||||
public/build/
|
public/build/
|
||||||
apps/platform/public/build/
|
|
||||||
public/hot/
|
public/hot/
|
||||||
apps/platform/public/hot/
|
|
||||||
public/storage/
|
public/storage/
|
||||||
apps/platform/public/storage/
|
|
||||||
storage/framework/
|
storage/framework/
|
||||||
apps/platform/storage/framework/
|
|
||||||
storage/logs/
|
storage/logs/
|
||||||
apps/platform/storage/logs/
|
|
||||||
storage/debugbar/
|
storage/debugbar/
|
||||||
apps/platform/storage/debugbar/
|
|
||||||
storage/*.key
|
storage/*.key
|
||||||
apps/platform/storage/*.key
|
|
||||||
/references/
|
/references/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@ -3,8 +3,6 @@ APP_ENV=local
|
|||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
SAIL_FILES=../../docker-compose.yml
|
|
||||||
TENANTATLAS_REPO_ROOT=../..
|
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
@ -23,12 +21,11 @@ LOG_DEPRECATIONS_CHANNEL=null
|
|||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
DB_HOST=pgsql
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
FORWARD_DB_PORT=55432
|
|
||||||
DB_DATABASE=tenantatlas
|
DB_DATABASE=tenantatlas
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
@ -46,7 +43,7 @@ CACHE_STORE=database
|
|||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
REDIS_CLIENT=phpredis
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PASSWORD=null
|
REDIS_PASSWORD=null
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
24
.github/agents/copilot-instructions.md
vendored
24
.github/agents/copilot-instructions.md
vendored
@ -2,12 +2,6 @@ # TenantAtlas Development Guidelines
|
|||||||
|
|
||||||
Auto-generated from all feature plans. Last updated: 2025-12-22
|
Auto-generated from all feature plans. Last updated: 2025-12-22
|
||||||
|
|
||||||
## Relocation override
|
|
||||||
- The authoritative Laravel application root is `apps/platform`.
|
|
||||||
- Human-facing commands should use `cd apps/platform && ...`.
|
|
||||||
- Repo-root tooling may delegate via `./scripts/platform-sail` when it cannot set a nested working directory.
|
|
||||||
- If any generated technology note below conflicts with the current repo, trust `apps/platform/composer.json`, `apps/platform/package.json`, and the live Laravel application metadata over stale generated entries.
|
|
||||||
|
|
||||||
## Active Technologies
|
## Active Technologies
|
||||||
- PHP 8.4.15 + Laravel 12, Filament v4, Livewire v3 (feat/005-bulk-operations)
|
- PHP 8.4.15 + Laravel 12, Filament v4, Livewire v3 (feat/005-bulk-operations)
|
||||||
- PostgreSQL (app), SQLite in-memory (tests) (feat/005-bulk-operations)
|
- PostgreSQL (app), SQLite in-memory (tests) (feat/005-bulk-operations)
|
||||||
@ -137,18 +131,6 @@ ## Active Technologies
|
|||||||
- PostgreSQL unchanged; no new persistence, cache table, or materialized aggregate is introduced (175-workspace-governance-attention)
|
- PostgreSQL unchanged; no new persistence, cache table, or materialized aggregate is introduced (175-workspace-governance-attention)
|
||||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `TenantResource`, `ProviderConnectionResource`, `TenantVerificationReport`, `BadgeCatalog`, `BadgeRenderer`, `TenantOperabilityService`, `ProviderConsentStatus`, `ProviderVerificationStatus`, and shared provider-state Blade partials (179-provider-truth-cleanup)
|
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `TenantResource`, `ProviderConnectionResource`, `TenantVerificationReport`, `BadgeCatalog`, `BadgeRenderer`, `TenantOperabilityService`, `ProviderConsentStatus`, `ProviderVerificationStatus`, and shared provider-state Blade partials (179-provider-truth-cleanup)
|
||||||
- PostgreSQL unchanged; no new table, column, or persisted artifact is introduced (179-provider-truth-cleanup)
|
- PostgreSQL unchanged; no new table, column, or persisted artifact is introduced (179-provider-truth-cleanup)
|
||||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `InventoryItem`, `OperationRun`, `InventoryCoverage`, `InventoryPolicyTypeMeta`, `CoverageCapabilitiesResolver`, `InventoryKpiHeader`, `InventoryCoverage` page, and `OperationRunResource` enterprise-detail stack (177-inventory-coverage-truth)
|
|
||||||
- PostgreSQL; existing `inventory_items` rows and `operation_runs.context` / `operation_runs.summary_counts` JSONB are reused with no schema change (177-inventory-coverage-truth)
|
|
||||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `OperationRun`, `OperationLifecyclePolicy`, `OperationRunFreshnessState`, `OperationUxPresenter`, `OperationRunLinks`, `ActiveRuns`, `StuckRunClassifier`, `WorkspaceOverviewBuilder`, dashboard widgets, workspace widgets, and system ops pages (178-ops-truth-alignment)
|
|
||||||
- PostgreSQL unchanged; existing `operation_runs` JSONB-backed `context`, `summary_counts`, and `failure_summary`; no schema change (178-ops-truth-alignment)
|
|
||||||
- PHP 8.4, Laravel 12, Blade, Filament v5, Livewire v4 + Filament v5, Livewire v4, Pest v4, Laravel Sail, existing `RestoreRunResource`, `RestoreService`, `RestoreRiskChecker`, `RestoreDiffGenerator`, `OperationRunResource`, `TenantlessOperationRunViewer`, shared badge infrastructure, and existing RBAC or write-gate helpers (181-restore-safety-integrity)
|
|
||||||
- PostgreSQL with existing `restore_runs` and `operation_runs` records plus JSON or array-backed `metadata`, `preview`, `results`, and `context`; no schema change planned (181-restore-safety-integrity)
|
|
||||||
- PHP 8.4, Laravel 12, Blade, Filament v5, Livewire v4 + Filament v5, Livewire v4, Pest v4, Laravel Sail, existing `BackupSetResource`, `BackupItemsRelationManager`, `PolicyVersionResource`, `RestoreRunResource`, `CreateRestoreRun`, `AssignmentBackupService`, `VersionService`, `PolicySnapshotService`, `RestoreRiskChecker`, `BadgeRenderer`, `PolicySnapshotModeBadge`, `EnterpriseDetailBuilder`, and existing RBAC helpers (176-backup-quality-truth)
|
|
||||||
- PostgreSQL with existing tenant-owned `backup_sets`, `backup_items`, `policy_versions`, and restore wizard input state; JSON-backed `metadata`, `snapshot`, `assignments`, and `scope_tags`; no schema change planned (176-backup-quality-truth)
|
|
||||||
- PHP 8.4, Laravel 12, Blade, Filament v5, Livewire v4 + Filament v5, Livewire v4, Pest v4, Laravel Sail, existing `DashboardKpis`, `NeedsAttention`, `BackupSetResource`, `BackupScheduleResource`, `BackupQualityResolver`, `BackupQualitySummary`, `ScheduleTimeService`, shared badge infrastructure, and existing RBAC helpers (180-tenant-backup-health)
|
|
||||||
- PostgreSQL with existing tenant-owned `backup_sets`, `backup_items`, and `backup_schedules` records plus existing JSON-backed backup metadata; no schema change planned (180-tenant-backup-health)
|
|
||||||
- PHP 8.4.15, Laravel 12, Blade, Livewire v4, Filament v5.2.x, Tailwind CSS v4, Vite 7 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `laravel/sail`, `laravel-vite-plugin`, `tailwindcss`, `vite`, `pestphp/pest`, `drizzle-kit`, PostgreSQL, Redis, Docker Compose (182-platform-relocation)
|
|
||||||
- PostgreSQL, Redis, filesystem storage under the Laravel app `storage/` tree, plus existing Vite build artifacts in `public/build`; no new database persistence planned (182-platform-relocation)
|
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -168,8 +150,8 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
- 182-platform-relocation: Added PHP 8.4.15, Laravel 12, Blade, Livewire v4, Filament v5.2.x, Tailwind CSS v4, Vite 7 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `laravel/sail`, `laravel-vite-plugin`, `tailwindcss`, `vite`, `pestphp/pest`, `drizzle-kit`, PostgreSQL, Redis, Docker Compose
|
- 179-provider-truth-cleanup: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `TenantResource`, `ProviderConnectionResource`, `TenantVerificationReport`, `BadgeCatalog`, `BadgeRenderer`, `TenantOperabilityService`, `ProviderConsentStatus`, `ProviderVerificationStatus`, and shared provider-state Blade partials
|
||||||
- 180-tenant-backup-health: Added PHP 8.4, Laravel 12, Blade, Filament v5, Livewire v4 + Filament v5, Livewire v4, Pest v4, Laravel Sail, existing `DashboardKpis`, `NeedsAttention`, `BackupSetResource`, `BackupScheduleResource`, `BackupQualityResolver`, `BackupQualitySummary`, `ScheduleTimeService`, shared badge infrastructure, and existing RBAC helpers
|
- 175-workspace-governance-attention: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `WorkspaceOverviewBuilder`, `TenantGovernanceAggregateResolver`, `BaselineCompareStats`, `BaselineCompareSummaryAssessor`, `WorkspaceSummaryStats`, `WorkspaceNeedsAttention`, `WorkspaceRecentOperations`, `FindingResource`, `BaselineCompareLanding`, `EvidenceSnapshotResource`, `TenantReviewResource`, and canonical admin Operations routes
|
||||||
- 176-backup-quality-truth: Added PHP 8.4, Laravel 12, Blade, Filament v5, Livewire v4 + Filament v5, Livewire v4, Pest v4, Laravel Sail, existing `BackupSetResource`, `BackupItemsRelationManager`, `PolicyVersionResource`, `RestoreRunResource`, `CreateRestoreRun`, `AssignmentBackupService`, `VersionService`, `PolicySnapshotService`, `RestoreRiskChecker`, `BadgeRenderer`, `PolicySnapshotModeBadge`, `EnterpriseDetailBuilder`, and existing RBAC helpers
|
- 174-evidence-freshness-publication-trust: Added PHP 8.4, Laravel 12, Filament v5, Livewire v4, Blade + Filament v5, Livewire v4, Pest v4, Laravel Sail, existing `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, `TenantReviewReadinessGate`, `EvidenceSnapshotService`, `TenantReviewRegisterService`, and current evidence/review/review-pack resources and pages
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
<!-- MANUAL ADDITIONS END -->
|
<!-- MANUAL ADDITIONS END -->
|
||||||
|
|||||||
49
.github/copilot-instructions.md
vendored
49
.github/copilot-instructions.md
vendored
@ -40,7 +40,7 @@ ## 3) Panel setup defaults
|
|||||||
- Assets policy:
|
- Assets policy:
|
||||||
- Panel-only assets: register via panel config.
|
- Panel-only assets: register via panel config.
|
||||||
- Shared/plugin assets: register via `FilamentAsset::register()`.
|
- Shared/plugin assets: register via `FilamentAsset::register()`.
|
||||||
- Deployment must include `cd apps/platform && php artisan filament:assets`.
|
- Deployment must include `php artisan filament:assets`.
|
||||||
|
|
||||||
Sources:
|
Sources:
|
||||||
- https://filamentphp.com/docs/5.x/panel-configuration
|
- https://filamentphp.com/docs/5.x/panel-configuration
|
||||||
@ -254,7 +254,7 @@ ## Testing
|
|||||||
- Source: https://filamentphp.com/docs/5.x/testing/testing-actions — “Testing actions”
|
- Source: https://filamentphp.com/docs/5.x/testing/testing-actions — “Testing actions”
|
||||||
|
|
||||||
## Deployment / Ops
|
## Deployment / Ops
|
||||||
- [ ] `cd apps/platform && php artisan filament:assets` is included in the deployment process when using registered assets.
|
- [ ] `php artisan filament:assets` is included in the deployment process when using registered assets.
|
||||||
- Source: https://filamentphp.com/docs/5.x/advanced/assets — “The FilamentAsset facade”
|
- Source: https://filamentphp.com/docs/5.x/advanced/assets — “The FilamentAsset facade”
|
||||||
|
|
||||||
=== foundation rules ===
|
=== foundation rules ===
|
||||||
@ -292,7 +292,7 @@ ## Application Structure & Architecture
|
|||||||
- Do not change the application's dependencies without approval.
|
- Do not change the application's dependencies without approval.
|
||||||
|
|
||||||
## Frontend Bundling
|
## Frontend Bundling
|
||||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `cd apps/platform && ./vendor/bin/sail npm run build`, `cd apps/platform && ./vendor/bin/sail npm run dev`, or `cd apps/platform && ./vendor/bin/sail composer run dev`. Ask them.
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||||
|
|
||||||
## Replies
|
## Replies
|
||||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||||
@ -372,29 +372,28 @@ ## Enums
|
|||||||
## Laravel Sail
|
## Laravel Sail
|
||||||
|
|
||||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||||
- The canonical application working directory is `apps/platform`. Repo-root launchers such as MCP or VS Code tasks may use `./scripts/platform-sail`, but that helper is compatibility-only.
|
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||||
- Start services using `cd apps/platform && ./vendor/bin/sail up -d` and stop them with `cd apps/platform && ./vendor/bin/sail stop`.
|
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||||
- Open the application in the browser by running `cd apps/platform && ./vendor/bin/sail open`.
|
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
|
||||||
- Always prefix PHP, Artisan, Composer, and Node commands with `cd apps/platform && ./vendor/bin/sail`. Examples:
|
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||||
- Run Artisan Commands: `cd apps/platform && ./vendor/bin/sail artisan migrate`
|
- Install Composer packages: `vendor/bin/sail composer install`
|
||||||
- Install Composer packages: `cd apps/platform && ./vendor/bin/sail composer install`
|
- Execute Node commands: `vendor/bin/sail npm run dev`
|
||||||
- Execute Node commands: `cd apps/platform && ./vendor/bin/sail npm run dev`
|
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||||
- Execute PHP scripts: `cd apps/platform && ./vendor/bin/sail php [script]`
|
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||||
- View all available Sail commands by running `cd apps/platform && ./vendor/bin/sail` without arguments.
|
|
||||||
|
|
||||||
=== tests rules ===
|
=== tests rules ===
|
||||||
|
|
||||||
## Test Enforcement
|
## 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.
|
- 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 `cd apps/platform && ./vendor/bin/sail artisan test --compact` with a specific filename or filter.
|
- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test --compact` with a specific filename or filter.
|
||||||
|
|
||||||
=== laravel/core rules ===
|
=== laravel/core rules ===
|
||||||
|
|
||||||
## Do Things the Laravel Way
|
## Do Things the Laravel Way
|
||||||
|
|
||||||
- Use `cd apps/platform && ./vendor/bin/sail 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.
|
- Use `vendor/bin/sail 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 `cd apps/platform && ./vendor/bin/sail artisan make:class`.
|
- If you're creating a generic PHP class, use `vendor/bin/sail 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.
|
- 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
|
### Database
|
||||||
@ -405,7 +404,7 @@ ### Database
|
|||||||
- Use Laravel's query builder for very complex database operations.
|
- Use Laravel's query builder for very complex database operations.
|
||||||
|
|
||||||
### Model Creation
|
### 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 `cd apps/platform && ./vendor/bin/sail artisan make:model`.
|
- 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 `vendor/bin/sail artisan make:model`.
|
||||||
|
|
||||||
### APIs & Eloquent Resources
|
### 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.
|
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||||
@ -429,10 +428,10 @@ ### Configuration
|
|||||||
### Testing
|
### 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.
|
- 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()`.
|
- 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 `cd apps/platform && ./vendor/bin/sail artisan make:test [options] {name}` 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 `vendor/bin/sail 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
|
### Vite Error
|
||||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `cd apps/platform && ./vendor/bin/sail npm run build` or ask the user to run `cd apps/platform && ./vendor/bin/sail npm run dev` or `cd apps/platform && ./vendor/bin/sail composer run dev`.
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
|
||||||
|
|
||||||
=== laravel/v12 rules ===
|
=== laravel/v12 rules ===
|
||||||
|
|
||||||
@ -461,7 +460,7 @@ ### Models
|
|||||||
## Livewire
|
## Livewire
|
||||||
|
|
||||||
- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests.
|
- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests.
|
||||||
- Use the `cd apps/platform && ./vendor/bin/sail artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
- Use the `vendor/bin/sail artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
|
||||||
- State should live on the server, with the UI reflecting it.
|
- 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.
|
||||||
|
|
||||||
@ -505,8 +504,8 @@ ## Testing Livewire
|
|||||||
|
|
||||||
## Laravel Pint Code Formatter
|
## Laravel Pint Code Formatter
|
||||||
|
|
||||||
- You must run `cd apps/platform && ./vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
- You must run `vendor/bin/sail bin pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
|
||||||
- Do not run `cd apps/platform && ./vendor/bin/sail bin pint --test`, simply run `cd apps/platform && ./vendor/bin/sail bin pint` to fix any formatting issues.
|
- Do not run `vendor/bin/sail bin pint --test`, simply run `vendor/bin/sail bin pint` to fix any formatting issues.
|
||||||
|
|
||||||
=== pest/core rules ===
|
=== pest/core rules ===
|
||||||
|
|
||||||
@ -515,7 +514,7 @@ ### Testing
|
|||||||
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
- If you need to verify a feature is working, write or update a Unit / Feature test.
|
||||||
|
|
||||||
### Pest Tests
|
### Pest Tests
|
||||||
- All tests must be written using Pest. Use `cd apps/platform && ./vendor/bin/sail artisan make:test --pest {name}`.
|
- All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
|
||||||
- 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.
|
||||||
- Tests should test all of the happy paths, failure paths, and weird paths.
|
- Tests should test all of the happy paths, failure paths, and weird paths.
|
||||||
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
- Tests live in the `tests/Feature` and `tests/Unit` directories.
|
||||||
@ -528,9 +527,9 @@ ### Pest Tests
|
|||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
|
||||||
- To run all tests: `cd apps/platform && ./vendor/bin/sail artisan test --compact`.
|
- To run all tests: `vendor/bin/sail artisan test --compact`.
|
||||||
- To run all tests in a file: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
- To run all tests in a file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
||||||
- To filter on a particular test name: `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=testName` (recommended after making a change to a related file).
|
- To filter on a particular test name: `vendor/bin/sail artisan test --compact --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.
|
- 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
|
### Pest Assertions
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@ -15,30 +15,19 @@
|
|||||||
/.zed
|
/.zed
|
||||||
/auth.json
|
/auth.json
|
||||||
/node_modules
|
/node_modules
|
||||||
/apps/platform/node_modules
|
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
coverage/
|
coverage/
|
||||||
/public/build
|
/public/build
|
||||||
/apps/platform/public/build
|
|
||||||
/public/hot
|
/public/hot
|
||||||
/apps/platform/public/hot
|
|
||||||
/public/storage
|
/public/storage
|
||||||
/apps/platform/public/storage
|
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/apps/platform/storage/*.key
|
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/apps/platform/storage/pail
|
|
||||||
/storage/framework
|
/storage/framework
|
||||||
/apps/platform/storage/framework
|
|
||||||
/storage/logs
|
/storage/logs
|
||||||
/apps/platform/storage/logs
|
|
||||||
/storage/debugbar
|
/storage/debugbar
|
||||||
/apps/platform/storage/debugbar
|
|
||||||
/vendor
|
/vendor
|
||||||
/apps/platform/vendor
|
|
||||||
/bootstrap/cache
|
/bootstrap/cache
|
||||||
/apps/platform/bootstrap/cache
|
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
@ -46,5 +35,3 @@ Thumbs.db
|
|||||||
/tests/Browser/Screenshots
|
/tests/Browser/Screenshots
|
||||||
*.tmp
|
*.tmp
|
||||||
*.swp
|
*.swp
|
||||||
/apps/platform/.env
|
|
||||||
/apps/platform/.env.*
|
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
public/build/
|
public/build/
|
||||||
apps/platform/public/build/
|
|
||||||
node_modules/
|
node_modules/
|
||||||
apps/platform/node_modules/
|
|
||||||
vendor/
|
vendor/
|
||||||
apps/platform/vendor/
|
|
||||||
*.log
|
*.log
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|||||||
@ -2,19 +2,12 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
public/build/
|
public/build/
|
||||||
apps/platform/public/build/
|
|
||||||
public/hot/
|
public/hot/
|
||||||
apps/platform/public/hot/
|
|
||||||
public/storage/
|
public/storage/
|
||||||
apps/platform/public/storage/
|
|
||||||
coverage/
|
coverage/
|
||||||
vendor/
|
vendor/
|
||||||
apps/platform/vendor/
|
|
||||||
apps/platform/node_modules/
|
|
||||||
storage/
|
storage/
|
||||||
apps/platform/storage/
|
|
||||||
bootstrap/cache/
|
bootstrap/cache/
|
||||||
apps/platform/bootstrap/cache/
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
@ -1,20 +1,32 @@
|
|||||||
<!--
|
<!--
|
||||||
Sync Impact Report
|
Sync Impact Report
|
||||||
|
|
||||||
- Version change: 2.0.0 -> 2.1.0
|
- Version change: 1.14.0 -> 2.0.0
|
||||||
- Modified principles:
|
- Modified principles:
|
||||||
- UX-001 (Layout & IA): header action line strengthened from SHOULD to MUST
|
- Filament UI - Action Surface Contract -> Operator-Facing UI/UX Constitution v1 / Filament UI - Action Surface Contract
|
||||||
with cross-reference to new HDR-001
|
- Filament UI - Layout & Information Architecture Standards (UX-001) -> Operator-Facing UI/UX Constitution v1 / Filament UI - Layout & Information Architecture Standards (UX-001)
|
||||||
|
- Operator-facing UI Naming Standards (UI-NAMING-001) -> Operator-Facing UI/UX Constitution v1 / Operator-facing UI Naming Standards (UI-NAMING-001)
|
||||||
|
- Operator Surface Principles (OPSURF-001) -> Operator-Facing UI/UX Constitution v1 / Operator Surface Principles (OPSURF-001)
|
||||||
|
- Spec Scope Fields (SCOPE-002) -> Operator-Facing UI/UX Constitution v1 / Spec Scope Fields (SCOPE-002)
|
||||||
- Added sections:
|
- Added sections:
|
||||||
- Header Action Discipline & Contextual Navigation (HDR-001)
|
- Operator-Facing UI/UX Constitution v1 (UI-CONST-001)
|
||||||
|
- Surface Taxonomy (UI-SURF-001)
|
||||||
|
- Hard Rules (UI-HARD-001)
|
||||||
|
- Exception Model (UI-EX-001)
|
||||||
|
- Enforcement Model (UI-REVIEW-001)
|
||||||
|
- Immediate Retrofit Priorities
|
||||||
|
- Appendix A - One-page Condensed Constitution
|
||||||
|
- Appendix B - Feature Review Checklist
|
||||||
|
- Appendix C - Red Flags for Future PRs
|
||||||
- Removed sections: None
|
- Removed sections: None
|
||||||
- Templates requiring updates:
|
- Templates requiring updates:
|
||||||
- ✅ .specify/memory/constitution.md
|
- ✅ .specify/memory/constitution.md
|
||||||
- ✅ .specify/templates/plan-template.md (Constitution Check: HDR-001 added)
|
- ✅ .specify/templates/spec-template.md
|
||||||
- ✅ .specify/templates/tasks-template.md (Filament UI section: HDR-001 added)
|
- ✅ .specify/templates/plan-template.md
|
||||||
- ⚠ .specify/templates/spec-template.md (no changes needed; existing
|
- ✅ .specify/templates/tasks-template.md
|
||||||
UI/UX Surface Classification and Operator Surface Contract tables already
|
- ✅ docs/product/principles.md
|
||||||
cover header action placement implicitly)
|
- ✅ docs/product/standards/README.md
|
||||||
|
- ✅ docs/HANDOVER.md
|
||||||
- Commands checked:
|
- Commands checked:
|
||||||
- N/A `.specify/templates/commands/*.md` directory is not present in this repo
|
- N/A `.specify/templates/commands/*.md` directory is not present in this repo
|
||||||
- Follow-up TODOs:
|
- Follow-up TODOs:
|
||||||
@ -523,7 +535,7 @@ #### Filament UI — Layout & Information Architecture Standards (UX-001)
|
|||||||
- When records exist, that primary CTA moves to the header and MUST NOT be duplicated in the empty state shell.
|
- When records exist, that primary CTA moves to the header and MUST NOT be duplicated in the empty state shell.
|
||||||
|
|
||||||
Actions and flows
|
Actions and flows
|
||||||
- Pages MUST expose at most one primary header action and one secondary header action; all others belong in groups (see HDR-001 for the full header discipline rule).
|
- Pages SHOULD expose at most one primary header action and one secondary header action; all others belong in groups.
|
||||||
- Multi-step or high-risk flows MUST use a wizard or an equivalent staged flow with preview and confirmation.
|
- Multi-step or high-risk flows MUST use a wizard or an equivalent staged flow with preview and confirmation.
|
||||||
- Destructive actions remain non-primary and confirmed.
|
- Destructive actions remain non-primary and confirmed.
|
||||||
|
|
||||||
@ -536,121 +548,6 @@ #### Filament UI — Layout & Information Architecture Standards (UX-001)
|
|||||||
- Shared layout builders such as `MainAsideForm`, `MainAsideInfolist`, and `StandardTableDefaults` SHOULD be reused where available.
|
- Shared layout builders such as `MainAsideForm`, `MainAsideInfolist`, and `StandardTableDefaults` SHOULD be reused where available.
|
||||||
- A change is not Done unless UX-001 is satisfied or an approved exception documents why not.
|
- A change is not Done unless UX-001 is satisfied or an approved exception documents why not.
|
||||||
|
|
||||||
#### Header Action Discipline & Contextual Navigation (HDR-001)
|
|
||||||
|
|
||||||
Goal: record and detail pages MUST be comprehensible within seconds.
|
|
||||||
Header actions are reserved for the primary workflow of the current page
|
|
||||||
and MUST NOT become a dumping ground for every available action or
|
|
||||||
navigation jump.
|
|
||||||
|
|
||||||
##### Core rule
|
|
||||||
|
|
||||||
Header actions MUST contain only workflow-critical actions of the
|
|
||||||
currently displayed record. Pure navigation, relational jumps, and
|
|
||||||
contextual references do not belong in the header; they belong directly
|
|
||||||
at the affected field, status indicator, or relation.
|
|
||||||
|
|
||||||
##### Maximum one primary visible header action
|
|
||||||
|
|
||||||
- Each record/detail page MUST expose at most one clearly prioritized
|
|
||||||
primary visible header action.
|
|
||||||
- That action MUST represent the most obvious next operator step on
|
|
||||||
exactly this page.
|
|
||||||
|
|
||||||
##### Navigation does not belong in headers
|
|
||||||
|
|
||||||
- Actions such as "Open finding", "Open queue", "View related run",
|
|
||||||
"Open tenant", or similar jumps are navigation actions, not primary
|
|
||||||
object actions.
|
|
||||||
- They MUST be placed as contextual navigation at fields, badges,
|
|
||||||
relation entries, or status displays — never in the header.
|
|
||||||
|
|
||||||
##### Destructive or governance-changing actions require friction
|
|
||||||
|
|
||||||
- Actions with operational, security-relevant, or governance-changing
|
|
||||||
effect MUST NOT stand at the same visual level as the primary action.
|
|
||||||
- They MUST either:
|
|
||||||
- be rendered as a clearly separated danger action, or
|
|
||||||
- be placed in an Action Group / More Actions.
|
|
||||||
- They MUST always require explicit confirmation
|
|
||||||
(`->requiresConfirmation()`).
|
|
||||||
- If an action changes governance truth, compliance status, risk
|
|
||||||
acceptance, exception validity, or equivalent system truths,
|
|
||||||
additional friction is mandatory (e.g., typed confirmation, reason
|
|
||||||
field, or staged flow).
|
|
||||||
|
|
||||||
##### Rare secondary actions belong in an Action Group
|
|
||||||
|
|
||||||
- Actions that are not part of the expected core workflow of the page
|
|
||||||
or are only occasionally needed MUST NOT appear as equally weighted
|
|
||||||
visible header buttons.
|
|
||||||
- They MUST be placed in an Action Group.
|
|
||||||
|
|
||||||
##### Header clarity over implementation convenience
|
|
||||||
|
|
||||||
- The fact that a framework makes header actions easy to add is not a
|
|
||||||
reason to place actions there.
|
|
||||||
- Information architecture, scanability, and operator clarity take
|
|
||||||
precedence over implementation convenience.
|
|
||||||
|
|
||||||
##### 5-second scan rule
|
|
||||||
|
|
||||||
Every record/detail page MUST pass the 5-second scan rule:
|
|
||||||
|
|
||||||
1. The operator instantly recognizes where they are.
|
|
||||||
2. The operator instantly sees the status of the object.
|
|
||||||
3. The operator instantly identifies the one central next action.
|
|
||||||
4. The operator immediately understands where secondary or dangerous
|
|
||||||
actions live.
|
|
||||||
|
|
||||||
If multiple equally weighted header buttons degrade this readability,
|
|
||||||
it is a constitution violation.
|
|
||||||
|
|
||||||
##### Placement rules
|
|
||||||
|
|
||||||
Allowed in the header:
|
|
||||||
- One primary workflow action.
|
|
||||||
- Optionally one clearly justified secondary action.
|
|
||||||
- Rare or administrative actions only when grouped.
|
|
||||||
- Critical/destructive actions only when separated and with friction.
|
|
||||||
|
|
||||||
Forbidden in the header:
|
|
||||||
- Pure navigation to related objects.
|
|
||||||
- Relational jumps without immediate workflow relevance.
|
|
||||||
- Collections of technically available standard actions.
|
|
||||||
- Multiple equally weighted buttons without clear prioritization.
|
|
||||||
|
|
||||||
##### Preferred pattern
|
|
||||||
|
|
||||||
| Slot | Placement |
|
|
||||||
|---|---|
|
|
||||||
| Primary visible | Exactly 1 |
|
|
||||||
| Danger | Separated or grouped, never casual beside Primary |
|
|
||||||
| Navigation | Inline at context (field, badge, relation) |
|
|
||||||
| Rare actions | More / Action Group |
|
|
||||||
|
|
||||||
##### Binding decision — Exception / Approval surfaces
|
|
||||||
|
|
||||||
For exception detail pages specifically:
|
|
||||||
- **Renew exception** MAY appear as the primary visible header action.
|
|
||||||
- **Revoke exception** is a governance-changing danger action and MUST
|
|
||||||
require friction (separated + confirmation).
|
|
||||||
- **Open finding** MUST be placed as a link at the Finding field, not
|
|
||||||
in the header.
|
|
||||||
- **Open approval queue** MUST be placed as a contextual link at
|
|
||||||
approval / status context, not in the header.
|
|
||||||
|
|
||||||
##### Reviewer heuristics
|
|
||||||
|
|
||||||
A page violates HDR-001 if any of the following are true:
|
|
||||||
- Multiple equally weighted header actions without clear workflow
|
|
||||||
priority.
|
|
||||||
- Pure navigation buttons in the header.
|
|
||||||
- Danger actions beside normal actions without clear separation.
|
|
||||||
- Rarely used administrative actions as visible standard buttons.
|
|
||||||
- The header resembles an action stockpile instead of a focused
|
|
||||||
workflow entry point.
|
|
||||||
|
|
||||||
#### Operator-facing UI Naming Standards (UI-NAMING-001)
|
#### Operator-facing UI Naming Standards (UI-NAMING-001)
|
||||||
|
|
||||||
Goal: operator-facing actions, runs, notifications, audit prose, and navigation MUST use one clear domain vocabulary.
|
Goal: operator-facing actions, runs, notifications, audit prose, and navigation MUST use one clear domain vocabulary.
|
||||||
@ -775,7 +672,6 @@ #### Appendix A - One-page Condensed Constitution
|
|||||||
- Standard lists stay scanable.
|
- Standard lists stay scanable.
|
||||||
- Exceptions are catalogued, justified, and tested.
|
- Exceptions are catalogued, justified, and tested.
|
||||||
- Features with ambiguous interaction semantics do not ship.
|
- Features with ambiguous interaction semantics do not ship.
|
||||||
- Header actions on record/detail pages expose at most one primary action; navigation belongs at context, not in the header.
|
|
||||||
|
|
||||||
#### Appendix B - Feature Review Checklist
|
#### Appendix B - Feature Review Checklist
|
||||||
|
|
||||||
@ -794,9 +690,6 @@ #### Appendix B - Feature Review Checklist
|
|||||||
- Critical truth is visible.
|
- Critical truth is visible.
|
||||||
- Scanability is preserved.
|
- Scanability is preserved.
|
||||||
- Exceptions are documented and tested.
|
- Exceptions are documented and tested.
|
||||||
- Header passes the 5-second scan rule (HDR-001).
|
|
||||||
- No pure navigation in the header.
|
|
||||||
- Governance-changing actions have extra friction.
|
|
||||||
|
|
||||||
#### Appendix C - Red Flags for Future PRs
|
#### Appendix C - Red Flags for Future PRs
|
||||||
|
|
||||||
@ -811,9 +704,6 @@ #### Appendix C - Red Flags for Future PRs
|
|||||||
- Queue surfaces throw the operator out of context through row click.
|
- Queue surfaces throw the operator out of context through row click.
|
||||||
- Critical health or operability truth is hidden by default.
|
- Critical health or operability truth is hidden by default.
|
||||||
- A contract claims conformance while the rendered UI behaves differently.
|
- A contract claims conformance while the rendered UI behaves differently.
|
||||||
- Header has multiple equally weighted buttons without clear prioritization.
|
|
||||||
- "Open X" navigation links placed in the header instead of at the related field.
|
|
||||||
- Governance-changing actions sit casually beside the primary action without friction.
|
|
||||||
|
|
||||||
### Data Minimization & Safe Logging
|
### Data Minimization & Safe Logging
|
||||||
- Inventory MUST store only metadata + whitelisted `meta_jsonb`.
|
- Inventory MUST store only metadata + whitelisted `meta_jsonb`.
|
||||||
@ -897,4 +787,4 @@ ### Versioning Policy (SemVer)
|
|||||||
- **MINOR**: new principle/section or materially expanded guidance.
|
- **MINOR**: new principle/section or materially expanded guidance.
|
||||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||||
|
|
||||||
**Version**: 2.1.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-07
|
**Version**: 2.0.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-03-28
|
||||||
|
|||||||
@ -70,8 +70,7 @@ ## Constitution Check
|
|||||||
- Operator surfaces (OPSURF-001): workspace and tenant context remain explicit in navigation, actions, and page semantics; tenant surfaces do not silently expose workspace-wide actions
|
- Operator surfaces (OPSURF-001): workspace and tenant context remain explicit in navigation, actions, and page semantics; tenant surfaces do not silently expose workspace-wide actions
|
||||||
- Operator surfaces (OPSURF-001): each new or materially refactored operator-facing page defines a page contract covering persona, surface type, operator question, default-visible info, diagnostics-only info, status dimensions, mutation scope, primary actions, and dangerous actions
|
- Operator surfaces (OPSURF-001): each new or materially refactored operator-facing page defines a page contract covering persona, surface type, operator question, default-visible info, diagnostics-only info, status dimensions, mutation scope, primary actions, and dangerous actions
|
||||||
- Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a surface-appropriate inspect affordance, remove redundant View when row click or identifier click already opens the same destination, keep standard CRUD/Registry rows to inspect plus at most one inline safe shortcut, group or relocate the rest to “More” or detail header, forbid empty bulk/overflow groups, require confirmations for destructive actions, write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted
|
- Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a surface-appropriate inspect affordance, remove redundant View when row click or identifier click already opens the same destination, keep standard CRUD/Registry rows to inspect plus at most one inline safe shortcut, group or relocate the rest to “More” or detail header, forbid empty bulk/overflow groups, require confirmations for destructive actions, write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted
|
||||||
- Filament UI UX-001 (Layout & IA): Create/Edit uses Main/Aside (3-col grid, Main=columnSpan(2), Aside=columnSpan(1)); all fields inside Sections/Cards (no naked inputs); View uses Infolists (not disabled edit forms); status badges use BADGE-001; empty states have specific title + explanation + 1 CTA; max 1 primary + 1 secondary header action (see HDR-001); tables provide search/sort/filters for core dimensions; shared layout builders preferred for consistency
|
- Filament UI UX-001 (Layout & IA): Create/Edit uses Main/Aside (3-col grid, Main=columnSpan(2), Aside=columnSpan(1)); all fields inside Sections/Cards (no naked inputs); View uses Infolists (not disabled edit forms); status badges use BADGE-001; empty states have specific title + explanation + 1 CTA; max 1 primary + 1 secondary header action; tables provide search/sort/filters for core dimensions; shared layout builders preferred for consistency
|
||||||
- Header action discipline (HDR-001): record/detail pages expose at most 1 primary visible header action; pure navigation (Open finding, Open tenant, View related run, etc.) is placed at the relevant field/badge/relation, NOT in the header; destructive or governance-changing actions are separated and require friction; rare actions live in Action Groups; every record/detail page passes the 5-second scan rule
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
### Documentation (this feature)
|
### Documentation (this feature)
|
||||||
|
|||||||
@ -72,7 +72,6 @@ # Tasks: [FEATURE NAME]
|
|||||||
- ensuring View pages use Infolists (not disabled edit forms); status badges use BADGE-001,
|
- ensuring View pages use Infolists (not disabled edit forms); status badges use BADGE-001,
|
||||||
- ensuring empty states show a specific title + explanation + exactly 1 CTA; non-empty tables move CTA to header,
|
- ensuring empty states show a specific title + explanation + exactly 1 CTA; non-empty tables move CTA to header,
|
||||||
- capping header actions to max 1 primary + 1 secondary (rest grouped),
|
- capping header actions to max 1 primary + 1 secondary (rest grouped),
|
||||||
- enforcing HDR-001 header action discipline: at most 1 primary visible action per record/detail page; pure navigation (Open finding, Open tenant, View related run, etc.) placed at the relevant field/badge/relation, NOT in the header; destructive or governance-changing actions separated and requiring friction; rare actions in Action Groups; every record/detail page passing the 5-second scan rule,
|
|
||||||
- using shared layout builders (e.g., `MainAsideForm`, `MainAsideInfolist`, `StandardTableDefaults`) where available,
|
- using shared layout builders (e.g., `MainAsideForm`, `MainAsideInfolist`, `StandardTableDefaults`) where available,
|
||||||
- OR documenting an explicit exemption with rationale if UX-001 is not fully satisfied.
|
- OR documenting an explicit exemption with rationale if UX-001 is not fully satisfied.
|
||||||
**Badges**: If this feature changes status-like badge semantics, tasks MUST use `BadgeCatalog` / `BadgeRenderer` (BADGE-001),
|
**Badges**: If this feature changes status-like badge semantics, tasks MUST use `BadgeCatalog` / `BadgeRenderer` (BADGE-001),
|
||||||
|
|||||||
73
Agents.md
73
Agents.md
@ -318,13 +318,12 @@ ## Security
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Sail (preferred locally)
|
### Sail (preferred locally)
|
||||||
- `cd apps/platform && ./vendor/bin/sail up -d`
|
- `./vendor/bin/sail up -d`
|
||||||
- `cd apps/platform && ./vendor/bin/sail down`
|
- `./vendor/bin/sail down`
|
||||||
- `cd apps/platform && ./vendor/bin/sail composer install`
|
- `./vendor/bin/sail composer install`
|
||||||
- `cd apps/platform && ./vendor/bin/sail artisan migrate`
|
- `./vendor/bin/sail artisan migrate`
|
||||||
- `cd apps/platform && ./vendor/bin/sail artisan test`
|
- `./vendor/bin/sail artisan test`
|
||||||
- `cd apps/platform && ./vendor/bin/sail artisan` (general)
|
- `./vendor/bin/sail artisan` (general)
|
||||||
- Root helper for tooling only: `./scripts/platform-sail ...`
|
|
||||||
|
|
||||||
### Drizzle (local DB tooling, if configured)
|
### Drizzle (local DB tooling, if configured)
|
||||||
- Use only for local/dev workflows.
|
- Use only for local/dev workflows.
|
||||||
@ -336,10 +335,10 @@ ### Drizzle (local DB tooling, if configured)
|
|||||||
(Agents should confirm the exact script names in `package.json` before suggesting them.)
|
(Agents should confirm the exact script names in `package.json` before suggesting them.)
|
||||||
|
|
||||||
### Non-Docker fallback (only if needed)
|
### Non-Docker fallback (only if needed)
|
||||||
- `cd apps/platform && composer install`
|
- `composer install`
|
||||||
- `cd apps/platform && php artisan serve`
|
- `php artisan serve`
|
||||||
- `cd apps/platform && php artisan migrate`
|
- `php artisan migrate`
|
||||||
- `cd apps/platform && php artisan test`
|
- `php artisan test`
|
||||||
|
|
||||||
### Frontend/assets/tooling (if present)
|
### Frontend/assets/tooling (if present)
|
||||||
- `pnpm install`
|
- `pnpm install`
|
||||||
@ -353,11 +352,11 @@ ## Where to look first
|
|||||||
- `.specify/`
|
- `.specify/`
|
||||||
- `AGENTS.md`
|
- `AGENTS.md`
|
||||||
- `README.md`
|
- `README.md`
|
||||||
- `apps/platform/app/`
|
- `app/`
|
||||||
- `apps/platform/database/`
|
- `database/`
|
||||||
- `apps/platform/routes/`
|
- `routes/`
|
||||||
- `apps/platform/resources/`
|
- `resources/`
|
||||||
- `apps/platform/config/`
|
- `config/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -434,7 +433,7 @@ ## 3) Panel setup defaults
|
|||||||
- Assets policy:
|
- Assets policy:
|
||||||
- Panel-only assets: register via panel config.
|
- Panel-only assets: register via panel config.
|
||||||
- Shared/plugin assets: register via `FilamentAsset::register()`.
|
- Shared/plugin assets: register via `FilamentAsset::register()`.
|
||||||
- Deployment must include `cd apps/platform && php artisan filament:assets`.
|
- Deployment must include `php artisan filament:assets`.
|
||||||
|
|
||||||
Sources:
|
Sources:
|
||||||
- https://filamentphp.com/docs/5.x/panel-configuration
|
- https://filamentphp.com/docs/5.x/panel-configuration
|
||||||
@ -671,7 +670,7 @@ ## Testing
|
|||||||
|
|
||||||
## Deployment / Ops
|
## Deployment / Ops
|
||||||
|
|
||||||
- [ ] `cd apps/platform && php artisan filament:assets` is included in the deployment process when using registered assets.
|
- [ ] `php artisan filament:assets` is included in the deployment process when using registered assets.
|
||||||
- Source: https://filamentphp.com/docs/5.x/advanced/assets — “The FilamentAsset facade”
|
- Source: https://filamentphp.com/docs/5.x/advanced/assets — “The FilamentAsset facade”
|
||||||
|
|
||||||
=== foundation rules ===
|
=== foundation rules ===
|
||||||
@ -721,7 +720,7 @@ ## Application Structure & Architecture
|
|||||||
|
|
||||||
## Frontend Bundling
|
## Frontend Bundling
|
||||||
|
|
||||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `cd apps/platform && ./vendor/bin/sail npm run build`, `cd apps/platform && ./vendor/bin/sail npm run dev`, or `cd apps/platform && ./vendor/bin/sail composer run dev`. Ask them.
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||||
|
|
||||||
## Documentation Files
|
## Documentation Files
|
||||||
|
|
||||||
@ -813,28 +812,28 @@ ## PHPDoc Blocks
|
|||||||
# Laravel Sail
|
# Laravel Sail
|
||||||
|
|
||||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||||
- Start services using `cd apps/platform && ./vendor/bin/sail up -d` and stop them with `cd apps/platform && ./vendor/bin/sail stop`.
|
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||||
- Open the application in the browser by running `cd apps/platform && ./vendor/bin/sail open`.
|
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||||
- Always prefix PHP, Artisan, Composer, and Node commands with `cd apps/platform && ./vendor/bin/sail`. Examples:
|
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
|
||||||
- Run Artisan Commands: `cd apps/platform && ./vendor/bin/sail artisan migrate`
|
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||||
- Install Composer packages: `cd apps/platform && ./vendor/bin/sail composer install`
|
- Install Composer packages: `vendor/bin/sail composer install`
|
||||||
- Execute Node commands: `cd apps/platform && ./vendor/bin/sail npm run dev`
|
- Execute Node commands: `vendor/bin/sail npm run dev`
|
||||||
- Execute PHP scripts: `cd apps/platform && ./vendor/bin/sail php [script]`
|
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||||
- View all available Sail commands by running `cd apps/platform && ./vendor/bin/sail` without arguments.
|
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||||
|
|
||||||
=== tests rules ===
|
=== tests rules ===
|
||||||
|
|
||||||
# Test Enforcement
|
# 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.
|
- 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 `cd apps/platform && ./vendor/bin/sail artisan test --compact` with a specific filename or filter.
|
- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test --compact` with a specific filename or filter.
|
||||||
|
|
||||||
=== laravel/core rules ===
|
=== laravel/core rules ===
|
||||||
|
|
||||||
# Do Things the Laravel Way
|
# Do Things the Laravel Way
|
||||||
|
|
||||||
- Use `cd apps/platform && ./vendor/bin/sail 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.
|
- Use `vendor/bin/sail 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 `cd apps/platform && ./vendor/bin/sail artisan make:class`.
|
- If you're creating a generic PHP class, use `vendor/bin/sail 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.
|
- 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
|
## Database
|
||||||
@ -847,7 +846,7 @@ ## Database
|
|||||||
|
|
||||||
### Model Creation
|
### 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 `cd apps/platform && ./vendor/bin/sail artisan make:model`.
|
- 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 `vendor/bin/sail artisan make:model`.
|
||||||
|
|
||||||
### APIs & Eloquent Resources
|
### APIs & Eloquent Resources
|
||||||
|
|
||||||
@ -878,11 +877,11 @@ ## 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.
|
- 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()`.
|
- 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 `cd apps/platform && ./vendor/bin/sail artisan make:test [options] {name}` 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 `vendor/bin/sail 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
|
## Vite Error
|
||||||
|
|
||||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `cd apps/platform && ./vendor/bin/sail npm run build` or ask the user to run `cd apps/platform && ./vendor/bin/sail npm run dev` or `cd apps/platform && ./vendor/bin/sail composer run dev`.
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
|
||||||
|
|
||||||
=== laravel/v12 rules ===
|
=== laravel/v12 rules ===
|
||||||
|
|
||||||
@ -913,15 +912,15 @@ ### Models
|
|||||||
|
|
||||||
# Laravel Pint Code Formatter
|
# Laravel Pint Code Formatter
|
||||||
|
|
||||||
- You must run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
- You must run `vendor/bin/sail bin pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||||
- Do not run `cd apps/platform && ./vendor/bin/sail bin pint --test --format agent`, simply run `cd apps/platform && ./vendor/bin/sail bin pint --format agent` to fix any formatting issues.
|
- Do not run `vendor/bin/sail bin pint --test --format agent`, simply run `vendor/bin/sail bin pint --format agent` to fix any formatting issues.
|
||||||
|
|
||||||
=== pest/core rules ===
|
=== pest/core rules ===
|
||||||
|
|
||||||
## Pest
|
## Pest
|
||||||
|
|
||||||
- This project uses Pest for testing. Create tests: `cd apps/platform && ./vendor/bin/sail artisan make:test --pest {name}`.
|
- This project uses Pest for testing. Create tests: `vendor/bin/sail artisan make:test --pest {name}`.
|
||||||
- Run tests: `cd apps/platform && ./vendor/bin/sail artisan test --compact` or filter: `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=testName`.
|
- Run tests: `vendor/bin/sail artisan test --compact` or filter: `vendor/bin/sail artisan test --compact --filter=testName`.
|
||||||
- Do NOT delete tests without approval.
|
- Do NOT delete tests without approval.
|
||||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||||
|
|||||||
73
GEMINI.md
73
GEMINI.md
@ -156,13 +156,12 @@ ## Security
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Sail (preferred locally)
|
### Sail (preferred locally)
|
||||||
- `cd apps/platform && ./vendor/bin/sail up -d`
|
- `./vendor/bin/sail up -d`
|
||||||
- `cd apps/platform && ./vendor/bin/sail down`
|
- `./vendor/bin/sail down`
|
||||||
- `cd apps/platform && ./vendor/bin/sail composer install`
|
- `./vendor/bin/sail composer install`
|
||||||
- `cd apps/platform && ./vendor/bin/sail artisan migrate`
|
- `./vendor/bin/sail artisan migrate`
|
||||||
- `cd apps/platform && ./vendor/bin/sail artisan test`
|
- `./vendor/bin/sail artisan test`
|
||||||
- `cd apps/platform && ./vendor/bin/sail artisan` (general)
|
- `./vendor/bin/sail artisan` (general)
|
||||||
- Root helper for tooling only: `./scripts/platform-sail ...`
|
|
||||||
|
|
||||||
### Drizzle (local DB tooling, if configured)
|
### Drizzle (local DB tooling, if configured)
|
||||||
- Use only for local/dev workflows.
|
- Use only for local/dev workflows.
|
||||||
@ -174,10 +173,10 @@ ### Drizzle (local DB tooling, if configured)
|
|||||||
(Agents should confirm the exact script names in `package.json` before suggesting them.)
|
(Agents should confirm the exact script names in `package.json` before suggesting them.)
|
||||||
|
|
||||||
### Non-Docker fallback (only if needed)
|
### Non-Docker fallback (only if needed)
|
||||||
- `cd apps/platform && composer install`
|
- `composer install`
|
||||||
- `cd apps/platform && php artisan serve`
|
- `php artisan serve`
|
||||||
- `cd apps/platform && php artisan migrate`
|
- `php artisan migrate`
|
||||||
- `cd apps/platform && php artisan test`
|
- `php artisan test`
|
||||||
|
|
||||||
### Frontend/assets/tooling (if present)
|
### Frontend/assets/tooling (if present)
|
||||||
- `pnpm install`
|
- `pnpm install`
|
||||||
@ -191,11 +190,11 @@ ## Where to look first
|
|||||||
- `.specify/`
|
- `.specify/`
|
||||||
- `AGENTS.md`
|
- `AGENTS.md`
|
||||||
- `README.md`
|
- `README.md`
|
||||||
- `apps/platform/app/`
|
- `app/`
|
||||||
- `apps/platform/database/`
|
- `database/`
|
||||||
- `apps/platform/routes/`
|
- `routes/`
|
||||||
- `apps/platform/resources/`
|
- `resources/`
|
||||||
- `apps/platform/config/`
|
- `config/`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -272,7 +271,7 @@ ## 3) Panel setup defaults
|
|||||||
- Assets policy:
|
- Assets policy:
|
||||||
- Panel-only assets: register via panel config.
|
- Panel-only assets: register via panel config.
|
||||||
- Shared/plugin assets: register via `FilamentAsset::register()`.
|
- Shared/plugin assets: register via `FilamentAsset::register()`.
|
||||||
- Deployment must include `cd apps/platform && php artisan filament:assets`.
|
- Deployment must include `php artisan filament:assets`.
|
||||||
|
|
||||||
Sources:
|
Sources:
|
||||||
- https://filamentphp.com/docs/5.x/panel-configuration
|
- https://filamentphp.com/docs/5.x/panel-configuration
|
||||||
@ -509,7 +508,7 @@ ## Testing
|
|||||||
|
|
||||||
## Deployment / Ops
|
## Deployment / Ops
|
||||||
|
|
||||||
- [ ] `cd apps/platform && php artisan filament:assets` is included in the deployment process when using registered assets.
|
- [ ] `php artisan filament:assets` is included in the deployment process when using registered assets.
|
||||||
- Source: https://filamentphp.com/docs/5.x/advanced/assets — “The FilamentAsset facade”
|
- Source: https://filamentphp.com/docs/5.x/advanced/assets — “The FilamentAsset facade”
|
||||||
|
|
||||||
=== foundation rules ===
|
=== foundation rules ===
|
||||||
@ -559,7 +558,7 @@ ## Application Structure & Architecture
|
|||||||
|
|
||||||
## Frontend Bundling
|
## Frontend Bundling
|
||||||
|
|
||||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `cd apps/platform && ./vendor/bin/sail npm run build`, `cd apps/platform && ./vendor/bin/sail npm run dev`, or `cd apps/platform && ./vendor/bin/sail composer run dev`. Ask them.
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `vendor/bin/sail npm run build`, `vendor/bin/sail npm run dev`, or `vendor/bin/sail composer run dev`. Ask them.
|
||||||
|
|
||||||
## Documentation Files
|
## Documentation Files
|
||||||
|
|
||||||
@ -651,28 +650,28 @@ ## PHPDoc Blocks
|
|||||||
# Laravel Sail
|
# Laravel Sail
|
||||||
|
|
||||||
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
- This project runs inside Laravel Sail's Docker containers. You MUST execute all commands through Sail.
|
||||||
- Start services using `cd apps/platform && ./vendor/bin/sail up -d` and stop them with `cd apps/platform && ./vendor/bin/sail stop`.
|
- Start services using `vendor/bin/sail up -d` and stop them with `vendor/bin/sail stop`.
|
||||||
- Open the application in the browser by running `cd apps/platform && ./vendor/bin/sail open`.
|
- Open the application in the browser by running `vendor/bin/sail open`.
|
||||||
- Always prefix PHP, Artisan, Composer, and Node commands with `cd apps/platform && ./vendor/bin/sail`. Examples:
|
- Always prefix PHP, Artisan, Composer, and Node commands with `vendor/bin/sail`. Examples:
|
||||||
- Run Artisan Commands: `cd apps/platform && ./vendor/bin/sail artisan migrate`
|
- Run Artisan Commands: `vendor/bin/sail artisan migrate`
|
||||||
- Install Composer packages: `cd apps/platform && ./vendor/bin/sail composer install`
|
- Install Composer packages: `vendor/bin/sail composer install`
|
||||||
- Execute Node commands: `cd apps/platform && ./vendor/bin/sail npm run dev`
|
- Execute Node commands: `vendor/bin/sail npm run dev`
|
||||||
- Execute PHP scripts: `cd apps/platform && ./vendor/bin/sail php [script]`
|
- Execute PHP scripts: `vendor/bin/sail php [script]`
|
||||||
- View all available Sail commands by running `cd apps/platform && ./vendor/bin/sail` without arguments.
|
- View all available Sail commands by running `vendor/bin/sail` without arguments.
|
||||||
|
|
||||||
=== tests rules ===
|
=== tests rules ===
|
||||||
|
|
||||||
# Test Enforcement
|
# 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.
|
- 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 `cd apps/platform && ./vendor/bin/sail artisan test --compact` with a specific filename or filter.
|
- Run the minimum number of tests needed to ensure code quality and speed. Use `vendor/bin/sail artisan test --compact` with a specific filename or filter.
|
||||||
|
|
||||||
=== laravel/core rules ===
|
=== laravel/core rules ===
|
||||||
|
|
||||||
# Do Things the Laravel Way
|
# Do Things the Laravel Way
|
||||||
|
|
||||||
- Use `cd apps/platform && ./vendor/bin/sail 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.
|
- Use `vendor/bin/sail 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 `cd apps/platform && ./vendor/bin/sail artisan make:class`.
|
- If you're creating a generic PHP class, use `vendor/bin/sail 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.
|
- 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
|
## Database
|
||||||
@ -685,7 +684,7 @@ ## Database
|
|||||||
|
|
||||||
### Model Creation
|
### 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 `cd apps/platform && ./vendor/bin/sail artisan make:model`.
|
- 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 `vendor/bin/sail artisan make:model`.
|
||||||
|
|
||||||
### APIs & Eloquent Resources
|
### APIs & Eloquent Resources
|
||||||
|
|
||||||
@ -716,11 +715,11 @@ ## 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.
|
- 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()`.
|
- 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 `cd apps/platform && ./vendor/bin/sail artisan make:test [options] {name}` 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 `vendor/bin/sail 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
|
## Vite Error
|
||||||
|
|
||||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `cd apps/platform && ./vendor/bin/sail npm run build` or ask the user to run `cd apps/platform && ./vendor/bin/sail npm run dev` or `cd apps/platform && ./vendor/bin/sail composer run dev`.
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `vendor/bin/sail npm run build` or ask the user to run `vendor/bin/sail npm run dev` or `vendor/bin/sail composer run dev`.
|
||||||
|
|
||||||
=== laravel/v12 rules ===
|
=== laravel/v12 rules ===
|
||||||
|
|
||||||
@ -751,15 +750,15 @@ ### Models
|
|||||||
|
|
||||||
# Laravel Pint Code Formatter
|
# Laravel Pint Code Formatter
|
||||||
|
|
||||||
- You must run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
- You must run `vendor/bin/sail bin pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||||
- Do not run `cd apps/platform && ./vendor/bin/sail bin pint --test --format agent`, simply run `cd apps/platform && ./vendor/bin/sail bin pint --format agent` to fix any formatting issues.
|
- Do not run `vendor/bin/sail bin pint --test --format agent`, simply run `vendor/bin/sail bin pint --format agent` to fix any formatting issues.
|
||||||
|
|
||||||
=== pest/core rules ===
|
=== pest/core rules ===
|
||||||
|
|
||||||
## Pest
|
## Pest
|
||||||
|
|
||||||
- This project uses Pest for testing. Create tests: `cd apps/platform && ./vendor/bin/sail artisan make:test --pest {name}`.
|
- This project uses Pest for testing. Create tests: `vendor/bin/sail artisan make:test --pest {name}`.
|
||||||
- Run tests: `cd apps/platform && ./vendor/bin/sail artisan test --compact` or filter: `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=testName`.
|
- Run tests: `vendor/bin/sail artisan test --compact` or filter: `vendor/bin/sail artisan test --compact --filter=testName`.
|
||||||
- Do NOT delete tests without approval.
|
- Do NOT delete tests without approval.
|
||||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples.
|
||||||
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task.
|
||||||
|
|||||||
43
README.md
43
README.md
@ -9,18 +9,11 @@
|
|||||||
|
|
||||||
## TenantPilot setup
|
## TenantPilot setup
|
||||||
|
|
||||||
- Platform app root: `apps/platform`
|
- Local dev (Sail-first):
|
||||||
- Repo-root ownership: specs, docs, scripts, editor config, agent config, orchestration, and `docker-compose.yml`
|
- Start stack: `./vendor/bin/sail up -d`
|
||||||
- App-root ownership: Laravel runtime, tests, Vite assets, public entrypoints, `composer.json`, `package.json`, `drizzle.config.ts`, and app-local `.env*`
|
- Init DB: `./vendor/bin/sail artisan migrate --seed`
|
||||||
- Local dev (Sail-first, canonical workflow):
|
- Tests: `./vendor/bin/sail artisan test`
|
||||||
- Install: `cd apps/platform && composer install`
|
- Policy sync: `./vendor/bin/sail artisan intune:sync-policies`
|
||||||
- Env bootstrap: `cd apps/platform && cp .env.example .env`
|
|
||||||
- Start stack: `cd apps/platform && ./vendor/bin/sail up -d`
|
|
||||||
- Generate app key: `cd apps/platform && ./vendor/bin/sail artisan key:generate`
|
|
||||||
- Init DB: `cd apps/platform && ./vendor/bin/sail artisan migrate --seed`
|
|
||||||
- Tests: `cd apps/platform && ./vendor/bin/sail artisan test --compact`
|
|
||||||
- Policy sync: `cd apps/platform && ./vendor/bin/sail artisan intune:sync-policies`
|
|
||||||
- Compatibility helper for tooling that cannot set a nested working directory: `./scripts/platform-sail ...`
|
|
||||||
- Filament admin: `/admin` (seed user `test@example.com`, set password via factory or `artisan tinker`).
|
- Filament admin: `/admin` (seed user `test@example.com`, set password via factory or `artisan tinker`).
|
||||||
- Microsoft Graph (Intune) env vars:
|
- Microsoft Graph (Intune) env vars:
|
||||||
- `GRAPH_TENANT_ID`
|
- `GRAPH_TENANT_ID`
|
||||||
@ -32,17 +25,10 @@ ## TenantPilot setup
|
|||||||
- **Missing permissions?** Scope tags will show as "Unknown (ID: X)" - add `DeviceManagementRBAC.Read.All`
|
- **Missing permissions?** Scope tags will show as "Unknown (ID: X)" - add `DeviceManagementRBAC.Read.All`
|
||||||
- Deployment (Dokploy, staging → production):
|
- Deployment (Dokploy, staging → production):
|
||||||
- Containerized deploy; ensure Postgres + Redis are provisioned (see `docker-compose.yml` for local baseline).
|
- Containerized deploy; ensure Postgres + Redis are provisioned (see `docker-compose.yml` for local baseline).
|
||||||
- Run application commands from `apps/platform`, including `php artisan filament:assets`.
|
|
||||||
- Run migrations on staging first, validate backup/restore flows, then promote to production.
|
- Run migrations on staging first, validate backup/restore flows, then promote to production.
|
||||||
- Ensure queue workers are running for jobs (e.g., policy sync) after deploy.
|
- Ensure queue workers are running for jobs (e.g., policy sync) after deploy.
|
||||||
- Keep secrets/env in Dokploy, never in code.
|
- Keep secrets/env in Dokploy, never in code.
|
||||||
|
|
||||||
## Platform relocation rollout notes
|
|
||||||
|
|
||||||
- Open branches that still touch legacy root app paths should merge `dev` first, then remap file moves from `app/`, `bootstrap/`, `config/`, `database/`, `lang/`, `public/`, `resources/`, `routes/`, `storage/`, and `tests/` into `apps/platform/...`.
|
|
||||||
- Keep using merge-based catch-up on shared feature branches; do not rebase long-lived shared branches just to absorb the relocation.
|
|
||||||
- VS Code tasks and MCP launchers now delegate through `./scripts/platform-sail` from the repo root. Human-facing docs remain `apps/platform`-first.
|
|
||||||
|
|
||||||
## Bulk operations (Feature 005)
|
## Bulk operations (Feature 005)
|
||||||
|
|
||||||
- Bulk actions are available in Filament resource tables (Policies, Policy Versions, Backup Sets, Restore Runs).
|
- Bulk actions are available in Filament resource tables (Policies, Policy Versions, Backup Sets, Restore Runs).
|
||||||
@ -53,23 +39,8 @@ ### Troubleshooting
|
|||||||
|
|
||||||
- **Progress stuck on “Queued…”** usually means the queue worker is not running (or not processing the queue you expect).
|
- **Progress stuck on “Queued…”** usually means the queue worker is not running (or not processing the queue you expect).
|
||||||
- Prefer using the Sail/Docker worker (see `docker-compose.yml`) rather than starting an additional local `php artisan queue:work`.
|
- Prefer using the Sail/Docker worker (see `docker-compose.yml`) rather than starting an additional local `php artisan queue:work`.
|
||||||
- Check worker status/logs: `cd apps/platform && ./vendor/bin/sail ps` and `cd apps/platform && ./vendor/bin/sail logs -f queue`.
|
- Check worker status/logs: `./vendor/bin/sail ps` and `./vendor/bin/sail logs -f queue`.
|
||||||
- **Exit code 137** for `queue:work` typically means the process was killed (often OOM). Increase Docker memory/limits or run the worker inside the container.
|
- **Exit code 137** for `queue:work` typically means the process was killed (often OOM). Increase Docker memory/limits or run the worker inside the container.
|
||||||
- **Moved app but old commands still fail** usually means the command is still being run from repo root. Switch to `cd apps/platform && ...` or use `./scripts/platform-sail ...` only for tooling that cannot set `cwd`.
|
|
||||||
|
|
||||||
## Rollback checklist
|
|
||||||
|
|
||||||
1. Revert the relocation commit or merge on your feature branch instead of hard-resetting shared history.
|
|
||||||
2. Preserve any local app env overrides before switching commits: `cp apps/platform/.env /tmp/tenantatlas.platform.env.backup` if needed.
|
|
||||||
3. Stop local containers and clean generated artifacts: `cd apps/platform && ./vendor/bin/sail down -v`, then remove `apps/platform/vendor`, `apps/platform/node_modules`, `apps/platform/public/build`, and `apps/platform/public/hot` if they need a clean rebuild.
|
|
||||||
4. After rollback, restore the matching env file for the restored topology and rerun the documented setup flow for that commit.
|
|
||||||
5. Notify owners of open feature branches that the topology changed so they can remap outstanding work before the next merge from `dev`.
|
|
||||||
|
|
||||||
## Deployment unknowns
|
|
||||||
|
|
||||||
- Dokploy build context for a repo-root compose file plus an app-root Laravel runtime still needs staging confirmation.
|
|
||||||
- Production web, queue, and scheduler working directories must be verified explicitly after the move; do not assume repo root and app root behave interchangeably.
|
|
||||||
- Any Dokploy volume mounts or storage persistence paths that previously targeted repo-root `storage/` must be reviewed against `apps/platform/storage/`.
|
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@ -93,7 +64,7 @@ ## Graph Contract Registry & Drift Guard
|
|||||||
- Sanitizes `$select`/`$expand` to allowed fields; logs warnings on trim.
|
- Sanitizes `$select`/`$expand` to allowed fields; logs warnings on trim.
|
||||||
- Derived @odata.type values within the family are accepted for preview/restore routing.
|
- Derived @odata.type values within the family are accepted for preview/restore routing.
|
||||||
- Capability fallback: on 400s related to select/expand, retries without those clauses and surfaces warnings.
|
- Capability fallback: on 400s related to select/expand, retries without those clauses and surfaces warnings.
|
||||||
- Drift check: `cd apps/platform && php artisan graph:contract:check [--tenant=]` runs lightweight probes against contract endpoints to detect capability/shape issues; useful in staging/CI (prod optional).
|
- Drift check: `php artisan graph:contract:check [--tenant=]` runs lightweight probes against contract endpoints to detect capability/shape issues; useful in staging/CI (prod optional).
|
||||||
- If Graph returns capability errors, TenantPilot downgrades safely, records warnings/audit entries, and avoids breaking preview/restore flows.
|
- If Graph returns capability errors, TenantPilot downgrades safely, records warnings/audit entries, and avoids breaking preview/restore flows.
|
||||||
|
|
||||||
## Policy Settings Display
|
## Policy Settings Display
|
||||||
|
|||||||
@ -6,20 +6,19 @@
|
|||||||
|
|
||||||
use App\Filament\Clusters\Inventory\InventoryCluster;
|
use App\Filament\Clusters\Inventory\InventoryCluster;
|
||||||
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
||||||
use App\Filament\Resources\InventoryItemResource;
|
|
||||||
use App\Filament\Widgets\Inventory\InventoryKpiHeader;
|
use App\Filament\Widgets\Inventory\InventoryKpiHeader;
|
||||||
use App\Models\OperationRun;
|
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Auth\CapabilityResolver;
|
use App\Services\Auth\CapabilityResolver;
|
||||||
|
use App\Services\Inventory\CoverageCapabilitiesResolver;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Badges\BadgeCatalog;
|
use App\Support\Badges\BadgeCatalog;
|
||||||
use App\Support\Badges\BadgeDomain;
|
use App\Support\Badges\BadgeDomain;
|
||||||
use App\Support\Badges\BadgeRenderer;
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\Badges\TagBadgeCatalog;
|
use App\Support\Badges\TagBadgeCatalog;
|
||||||
use App\Support\Badges\TagBadgeDomain;
|
use App\Support\Badges\TagBadgeDomain;
|
||||||
use App\Support\Inventory\TenantCoverageTruth;
|
use App\Support\Badges\TagBadgeRenderer;
|
||||||
use App\Support\Inventory\TenantCoverageTruthResolver;
|
use App\Support\Inventory\InventoryPolicyTypeMeta;
|
||||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||||
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
|
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
|
||||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||||
@ -57,8 +56,6 @@ class InventoryCoverage extends Page implements HasTable
|
|||||||
|
|
||||||
protected string $view = 'filament.pages.inventory-coverage';
|
protected string $view = 'filament.pages.inventory-coverage';
|
||||||
|
|
||||||
protected ?TenantCoverageTruth $cachedCoverageTruth = null;
|
|
||||||
|
|
||||||
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||||
{
|
{
|
||||||
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
|
||||||
@ -70,7 +67,7 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|||||||
->exempt(ActionSurfaceSlot::InspectAffordance, 'Inventory coverage rows are runtime-derived metadata and intentionally omit inspect affordances.')
|
->exempt(ActionSurfaceSlot::InspectAffordance, 'Inventory coverage rows are runtime-derived metadata and intentionally omit inspect affordances.')
|
||||||
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Derived coverage rows do not expose row actions.')
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Derived coverage rows do not expose row actions.')
|
||||||
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Derived coverage rows do not expose bulk actions.')
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Derived coverage rows do not expose bulk actions.')
|
||||||
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state provides a clear-filters CTA to return to the full tenant coverage report.');
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state provides a clear-filters CTA to return to the full coverage matrix.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function shouldRegisterNavigation(): bool
|
public static function shouldRegisterNavigation(): bool
|
||||||
@ -113,12 +110,9 @@ protected function getHeaderWidgets(): array
|
|||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->persistFiltersInSession()
|
|
||||||
->persistSearchInSession()
|
|
||||||
->persistSortInSession()
|
|
||||||
->searchable()
|
->searchable()
|
||||||
->searchPlaceholder('Search by type or label')
|
->searchPlaceholder('Search by policy type or label')
|
||||||
->defaultSort('follow_up_priority')
|
->defaultSort('label')
|
||||||
->defaultPaginationPageOption(50)
|
->defaultPaginationPageOption(50)
|
||||||
->paginated(\App\Support\Filament\TablePaginationProfiles::customPage())
|
->paginated(\App\Support\Filament\TablePaginationProfiles::customPage())
|
||||||
->records(function (
|
->records(function (
|
||||||
@ -148,17 +142,15 @@ public function table(Table $table): Table
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('coverage_state')
|
TextColumn::make('type')
|
||||||
->label('Coverage state')
|
|
||||||
->badge()
|
|
||||||
->formatStateUsing(BadgeRenderer::label(BadgeDomain::InventoryCoverageState))
|
|
||||||
->color(BadgeRenderer::color(BadgeDomain::InventoryCoverageState))
|
|
||||||
->icon(BadgeRenderer::icon(BadgeDomain::InventoryCoverageState))
|
|
||||||
->iconColor(BadgeRenderer::iconColor(BadgeDomain::InventoryCoverageState))
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('label')
|
|
||||||
->label('Type')
|
->label('Type')
|
||||||
->sortable()
|
->sortable()
|
||||||
|
->fontFamily(FontFamily::Mono)
|
||||||
|
->copyable()
|
||||||
|
->wrap(),
|
||||||
|
TextColumn::make('label')
|
||||||
|
->label('Label')
|
||||||
|
->sortable()
|
||||||
->badge()
|
->badge()
|
||||||
->formatStateUsing(function (?string $state, array $record): string {
|
->formatStateUsing(function (?string $state, array $record): string {
|
||||||
return TagBadgeCatalog::spec(
|
return TagBadgeCatalog::spec(
|
||||||
@ -187,29 +179,17 @@ public function table(Table $table): Table
|
|||||||
return $spec->iconColor ?? $spec->color;
|
return $spec->iconColor ?? $spec->color;
|
||||||
})
|
})
|
||||||
->wrap(),
|
->wrap(),
|
||||||
TextColumn::make('follow_up_guidance')
|
TextColumn::make('risk')
|
||||||
->label('Follow-up guidance')
|
->label('Risk')
|
||||||
->wrap()
|
|
||||||
->toggleable(),
|
|
||||||
TextColumn::make('observed_item_count')
|
|
||||||
->label('Observed items')
|
|
||||||
->numeric()
|
|
||||||
->sortable(),
|
|
||||||
TextColumn::make('category')
|
|
||||||
->badge()
|
->badge()
|
||||||
->formatStateUsing(fn (?string $state): string => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state)->label)
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk))
|
||||||
->color(fn (?string $state): string => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state)->color)
|
->color(BadgeRenderer::color(BadgeDomain::PolicyRisk))
|
||||||
->icon(fn (?string $state): ?string => TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state)->icon)
|
->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk))
|
||||||
->iconColor(function (?string $state): ?string {
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk)),
|
||||||
$spec = TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, $state);
|
|
||||||
|
|
||||||
return $spec->iconColor ?? $spec->color;
|
|
||||||
})
|
|
||||||
->toggleable()
|
|
||||||
->wrap(),
|
|
||||||
TextColumn::make('restore')
|
TextColumn::make('restore')
|
||||||
->label('Restore')
|
->label('Restore')
|
||||||
->badge()
|
->badge()
|
||||||
|
->state(fn (array $record): ?string => $record['restore'])
|
||||||
->formatStateUsing(function (?string $state): string {
|
->formatStateUsing(function (?string $state): string {
|
||||||
return filled($state)
|
return filled($state)
|
||||||
? BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state)->label
|
? BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state)->label
|
||||||
@ -233,7 +213,20 @@ public function table(Table $table): Table
|
|||||||
$spec = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state);
|
$spec = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, $state);
|
||||||
|
|
||||||
return $spec->iconColor ?? $spec->color;
|
return $spec->iconColor ?? $spec->color;
|
||||||
})
|
}),
|
||||||
|
TextColumn::make('category')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyCategory))
|
||||||
|
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyCategory))
|
||||||
|
->icon(TagBadgeRenderer::icon(TagBadgeDomain::PolicyCategory))
|
||||||
|
->iconColor(TagBadgeRenderer::iconColor(TagBadgeDomain::PolicyCategory))
|
||||||
|
->toggleable()
|
||||||
|
->wrap(),
|
||||||
|
TextColumn::make('segment')
|
||||||
|
->label('Segment')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(fn (?string $state): string => $state === 'foundation' ? 'Foundation' : 'Policy')
|
||||||
|
->color(fn (?string $state): string => $state === 'foundation' ? 'gray' : 'info')
|
||||||
->toggleable(),
|
->toggleable(),
|
||||||
IconColumn::make('dependencies')
|
IconColumn::make('dependencies')
|
||||||
->label('Dependencies')
|
->label('Dependencies')
|
||||||
@ -244,31 +237,10 @@ public function table(Table $table): Table
|
|||||||
->falseColor('gray')
|
->falseColor('gray')
|
||||||
->alignCenter()
|
->alignCenter()
|
||||||
->toggleable(),
|
->toggleable(),
|
||||||
TextColumn::make('type')
|
|
||||||
->label('Type key')
|
|
||||||
->sortable()
|
|
||||||
->fontFamily(FontFamily::Mono)
|
|
||||||
->copyable()
|
|
||||||
->wrap()
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
TextColumn::make('segment')
|
|
||||||
->label('Segment')
|
|
||||||
->badge()
|
|
||||||
->formatStateUsing(fn (?string $state): string => $state === 'foundation' ? 'Foundation' : 'Policy')
|
|
||||||
->color(fn (?string $state): string => $state === 'foundation' ? 'gray' : 'info')
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
TextColumn::make('risk')
|
|
||||||
->label('Risk')
|
|
||||||
->badge()
|
|
||||||
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk))
|
|
||||||
->color(BadgeRenderer::color(BadgeDomain::PolicyRisk))
|
|
||||||
->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk))
|
|
||||||
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk))
|
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
|
||||||
])
|
])
|
||||||
->filters($this->tableFilters())
|
->filters($this->tableFilters())
|
||||||
->emptyStateHeading('No coverage rows match this report')
|
->emptyStateHeading('No coverage entries match this view')
|
||||||
->emptyStateDescription('Clear the current search or filters to return to the full tenant coverage report.')
|
->emptyStateDescription('Clear the current search or filters to return to the full coverage matrix.')
|
||||||
->emptyStateIcon('heroicon-o-funnel')
|
->emptyStateIcon('heroicon-o-funnel')
|
||||||
->emptyStateActions([
|
->emptyStateActions([
|
||||||
Action::make('clear_filters')
|
Action::make('clear_filters')
|
||||||
@ -289,14 +261,6 @@ public function table(Table $table): Table
|
|||||||
protected function tableFilters(): array
|
protected function tableFilters(): array
|
||||||
{
|
{
|
||||||
$filters = [
|
$filters = [
|
||||||
SelectFilter::make('coverage_state')
|
|
||||||
->label('Coverage state')
|
|
||||||
->options([
|
|
||||||
'succeeded' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'succeeded')->label,
|
|
||||||
'failed' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'failed')->label,
|
|
||||||
'skipped' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'skipped')->label,
|
|
||||||
'unknown' => BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'unknown')->label,
|
|
||||||
]),
|
|
||||||
SelectFilter::make('category')
|
SelectFilter::make('category')
|
||||||
->label('Category')
|
->label('Category')
|
||||||
->options($this->categoryFilterOptions()),
|
->options($this->categoryFilterOptions()),
|
||||||
@ -315,36 +279,84 @@ protected function tableFilters(): array
|
|||||||
* @return Collection<string, array{
|
* @return Collection<string, array{
|
||||||
* __key: string,
|
* __key: string,
|
||||||
* key: string,
|
* key: string,
|
||||||
* type: string,
|
|
||||||
* segment: string,
|
* segment: string,
|
||||||
|
* type: string,
|
||||||
* label: string,
|
* label: string,
|
||||||
* category: string,
|
* category: string,
|
||||||
* platform: ?string,
|
|
||||||
* coverage_state: string,
|
|
||||||
* follow_up_required: bool,
|
|
||||||
* follow_up_priority: int,
|
|
||||||
* follow_up_guidance: string,
|
|
||||||
* observed_item_count: int,
|
|
||||||
* basis_item_count: ?int,
|
|
||||||
* basis_error_code: ?string,
|
|
||||||
* restore: ?string,
|
|
||||||
* risk: ?string,
|
|
||||||
* dependencies: bool,
|
* dependencies: bool,
|
||||||
* is_basis_payload_backed: bool
|
* restore: ?string,
|
||||||
|
* risk: string,
|
||||||
|
* source_order: int
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
protected function coverageRows(): Collection
|
protected function coverageRows(): Collection
|
||||||
{
|
{
|
||||||
$truth = $this->coverageTruth();
|
$resolver = app(CoverageCapabilitiesResolver::class);
|
||||||
|
|
||||||
if (! $truth instanceof TenantCoverageTruth) {
|
$supported = $this->mapCoverageRows(
|
||||||
return collect();
|
rows: InventoryPolicyTypeMeta::supported(),
|
||||||
}
|
segment: 'policy',
|
||||||
|
sourceOrderOffset: 0,
|
||||||
|
resolver: $resolver,
|
||||||
|
);
|
||||||
|
|
||||||
return collect($truth->rows)
|
return $supported->merge($this->mapCoverageRows(
|
||||||
->mapWithKeys(static fn ($row): array => [
|
rows: InventoryPolicyTypeMeta::foundations(),
|
||||||
$row->key => $row->toArray(),
|
segment: 'foundation',
|
||||||
]);
|
sourceOrderOffset: $supported->count(),
|
||||||
|
resolver: $resolver,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $rows
|
||||||
|
* @return Collection<string, array{
|
||||||
|
* __key: string,
|
||||||
|
* key: string,
|
||||||
|
* segment: string,
|
||||||
|
* type: string,
|
||||||
|
* label: string,
|
||||||
|
* category: string,
|
||||||
|
* dependencies: bool,
|
||||||
|
* restore: ?string,
|
||||||
|
* risk: string,
|
||||||
|
* source_order: int
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
protected function mapCoverageRows(
|
||||||
|
array $rows,
|
||||||
|
string $segment,
|
||||||
|
int $sourceOrderOffset,
|
||||||
|
CoverageCapabilitiesResolver $resolver
|
||||||
|
): Collection {
|
||||||
|
return collect($rows)
|
||||||
|
->values()
|
||||||
|
->mapWithKeys(function (array $row, int $index) use ($resolver, $segment, $sourceOrderOffset): array {
|
||||||
|
$type = (string) ($row['type'] ?? '');
|
||||||
|
|
||||||
|
if ($type === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = "{$segment}:{$type}";
|
||||||
|
$restore = $row['restore'] ?? null;
|
||||||
|
$risk = $row['risk'] ?? 'n/a';
|
||||||
|
|
||||||
|
return [
|
||||||
|
$key => [
|
||||||
|
'__key' => $key,
|
||||||
|
'key' => $key,
|
||||||
|
'segment' => $segment,
|
||||||
|
'type' => $type,
|
||||||
|
'label' => (string) ($row['label'] ?? $type),
|
||||||
|
'category' => (string) ($row['category'] ?? 'Other'),
|
||||||
|
'dependencies' => $segment === 'policy' && $resolver->supportsDependencies($type),
|
||||||
|
'restore' => is_string($restore) ? $restore : null,
|
||||||
|
'risk' => is_string($risk) ? $risk : 'n/a',
|
||||||
|
'source_order' => $sourceOrderOffset + $index,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -355,7 +367,6 @@ protected function coverageRows(): Collection
|
|||||||
protected function filterRows(Collection $rows, ?string $search, array $filters): Collection
|
protected function filterRows(Collection $rows, ?string $search, array $filters): Collection
|
||||||
{
|
{
|
||||||
$normalizedSearch = Str::lower(trim((string) $search));
|
$normalizedSearch = Str::lower(trim((string) $search));
|
||||||
$coverageState = $filters['coverage_state']['value'] ?? null;
|
|
||||||
$category = $filters['category']['value'] ?? null;
|
$category = $filters['category']['value'] ?? null;
|
||||||
$restore = $filters['restore']['value'] ?? null;
|
$restore = $filters['restore']['value'] ?? null;
|
||||||
|
|
||||||
@ -369,10 +380,6 @@ function (Collection $rows) use ($normalizedSearch): Collection {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
->when(
|
|
||||||
filled($coverageState),
|
|
||||||
fn (Collection $rows): Collection => $rows->where('coverage_state', (string) $coverageState),
|
|
||||||
)
|
|
||||||
->when(
|
->when(
|
||||||
filled($category),
|
filled($category),
|
||||||
fn (Collection $rows): Collection => $rows->where('category', (string) $category),
|
fn (Collection $rows): Collection => $rows->where('category', (string) $category),
|
||||||
@ -389,35 +396,22 @@ function (Collection $rows) use ($normalizedSearch): Collection {
|
|||||||
*/
|
*/
|
||||||
protected function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection
|
protected function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection
|
||||||
{
|
{
|
||||||
$sortColumn = in_array($sortColumn, ['type', 'label', 'observed_item_count', 'coverage_state', 'follow_up_priority'], true)
|
$sortColumn = in_array($sortColumn, ['type', 'label'], true) ? $sortColumn : null;
|
||||||
? $sortColumn
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if ($sortColumn === null) {
|
if ($sortColumn === null) {
|
||||||
return $rows;
|
return $rows->sortBy('source_order');
|
||||||
}
|
}
|
||||||
|
|
||||||
$records = $rows->all();
|
$records = $rows->all();
|
||||||
|
|
||||||
uasort($records, function (array $left, array $right) use ($sortColumn, $sortDirection): int {
|
uasort($records, function (array $left, array $right) use ($sortColumn, $sortDirection): int {
|
||||||
$comparison = match ($sortColumn) {
|
$comparison = strnatcasecmp(
|
||||||
'observed_item_count' => ((int) ($left[$sortColumn] ?? 0)) <=> ((int) ($right[$sortColumn] ?? 0)),
|
(string) ($left[$sortColumn] ?? ''),
|
||||||
'follow_up_priority' => ((int) ($left[$sortColumn] ?? 0)) <=> ((int) ($right[$sortColumn] ?? 0)),
|
(string) ($right[$sortColumn] ?? ''),
|
||||||
default => strnatcasecmp(
|
);
|
||||||
(string) ($left[$sortColumn] ?? ''),
|
|
||||||
(string) ($right[$sortColumn] ?? ''),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($comparison === 0 && $sortColumn === 'follow_up_priority') {
|
|
||||||
$comparison = ((int) ($right['observed_item_count'] ?? 0)) <=> ((int) ($left['observed_item_count'] ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($comparison === 0) {
|
if ($comparison === 0) {
|
||||||
$comparison = strnatcasecmp(
|
$comparison = ((int) ($left['source_order'] ?? 0)) <=> ((int) ($right['source_order'] ?? 0));
|
||||||
(string) ($left['label'] ?? ''),
|
|
||||||
(string) ($right['label'] ?? ''),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $sortDirection === 'desc' ? ($comparison * -1) : $comparison;
|
return $sortDirection === 'desc' ? ($comparison * -1) : $comparison;
|
||||||
@ -474,99 +468,4 @@ protected function restoreFilterOptions(): array
|
|||||||
})
|
})
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function coverageSummary(): array
|
|
||||||
{
|
|
||||||
$truth = $this->coverageTruth();
|
|
||||||
|
|
||||||
if (! $truth instanceof TenantCoverageTruth) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'supportedTypes' => $truth->supportedTypeCount,
|
|
||||||
'succeededTypes' => $truth->succeededTypeCount,
|
|
||||||
'followUpTypes' => $truth->followUpTypeCount,
|
|
||||||
'observedItems' => $truth->observedItemTotal,
|
|
||||||
'observedTypes' => $truth->observedTypeCount(),
|
|
||||||
'topFollowUpLabel' => $truth->topPriorityFollowUpRow()?->label,
|
|
||||||
'topFollowUpGuidance' => $truth->topPriorityFollowUpRow()?->followUpGuidance,
|
|
||||||
'hasCurrentCoverageResult' => $truth->hasCurrentCoverageResult,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function basisRunSummary(): array
|
|
||||||
{
|
|
||||||
$truth = $this->coverageTruth();
|
|
||||||
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
if (! $truth instanceof TenantCoverageTruth || ! $tenant instanceof Tenant) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $truth->basisRun instanceof OperationRun) {
|
|
||||||
return [
|
|
||||||
'title' => 'No current coverage basis',
|
|
||||||
'body' => $user instanceof User && $user->can(Capabilities::TENANT_INVENTORY_SYNC_RUN, $tenant)
|
|
||||||
? 'Run Inventory Sync from Inventory Items to establish current tenant coverage truth.'
|
|
||||||
: 'A tenant operator with inventory sync permission must establish current tenant coverage truth.',
|
|
||||||
'badgeLabel' => null,
|
|
||||||
'badgeColor' => null,
|
|
||||||
'runUrl' => null,
|
|
||||||
'historyUrl' => null,
|
|
||||||
'inventoryItemsUrl' => InventoryItemResource::getUrl('index', tenant: $tenant),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$badge = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, (string) $truth->basisRun->outcome);
|
|
||||||
$canViewRun = $user instanceof User && $user->can('view', $truth->basisRun);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'title' => sprintf('Latest coverage-bearing sync completed %s.', $truth->basisCompletedAtLabel() ?? 'recently'),
|
|
||||||
'body' => $canViewRun
|
|
||||||
? 'Review the cited inventory sync to inspect provider or permission issues in detail.'
|
|
||||||
: 'The coverage basis is current, but your role cannot open the cited run detail.',
|
|
||||||
'badgeLabel' => $badge->label,
|
|
||||||
'badgeColor' => $badge->color,
|
|
||||||
'runUrl' => $canViewRun ? route('admin.operations.view', ['run' => (int) $truth->basisRun->getKey()]) : null,
|
|
||||||
'historyUrl' => $canViewRun ? $this->inventorySyncHistoryUrl($tenant) : null,
|
|
||||||
'inventoryItemsUrl' => InventoryItemResource::getUrl('index', tenant: $tenant),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function coverageTruth(): ?TenantCoverageTruth
|
|
||||||
{
|
|
||||||
if ($this->cachedCoverageTruth instanceof TenantCoverageTruth) {
|
|
||||||
return $this->cachedCoverageTruth;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->cachedCoverageTruth = app(TenantCoverageTruthResolver::class)->resolve($tenant);
|
|
||||||
|
|
||||||
return $this->cachedCoverageTruth;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function inventorySyncHistoryUrl(Tenant $tenant): string
|
|
||||||
{
|
|
||||||
return route('admin.operations.index', [
|
|
||||||
'tenant_id' => (int) $tenant->getKey(),
|
|
||||||
'tableFilters' => [
|
|
||||||
'type' => [
|
|
||||||
'value' => 'inventory_sync',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -38,7 +38,6 @@
|
|||||||
use Filament\Tables\Contracts\HasTable;
|
use Filament\Tables\Contracts\HasTable;
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@ -50,8 +49,6 @@ class FindingExceptionsQueue extends Page implements HasTable
|
|||||||
|
|
||||||
public ?int $selectedFindingExceptionId = null;
|
public ?int $selectedFindingExceptionId = null;
|
||||||
|
|
||||||
public bool $showSelectedExceptionSummary = false;
|
|
||||||
|
|
||||||
protected static bool $isDiscovered = false;
|
protected static bool $isDiscovered = false;
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-exclamation';
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-exclamation';
|
||||||
@ -119,12 +116,11 @@ public static function canAccess(): bool
|
|||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->selectedFindingExceptionId = is_numeric(request()->query('exception')) ? (int) request()->query('exception') : null;
|
$this->selectedFindingExceptionId = is_numeric(request()->query('exception')) ? (int) request()->query('exception') : null;
|
||||||
$this->showSelectedExceptionSummary = $this->selectedFindingExceptionId !== null;
|
|
||||||
$this->mountInteractsWithTable();
|
$this->mountInteractsWithTable();
|
||||||
$this->applyRequestedTenantPrefilter();
|
$this->applyRequestedTenantPrefilter();
|
||||||
|
|
||||||
if ($this->selectedFindingExceptionId !== null) {
|
if ($this->selectedFindingExceptionId !== null) {
|
||||||
$this->resolveSelectedFindingException($this->selectedFindingExceptionId);
|
$this->selectedFindingException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +141,6 @@ protected function getHeaderActions(): array
|
|||||||
$this->removeTableFilter('status');
|
$this->removeTableFilter('status');
|
||||||
$this->removeTableFilter('current_validity_state');
|
$this->removeTableFilter('current_validity_state');
|
||||||
$this->selectedFindingExceptionId = null;
|
$this->selectedFindingExceptionId = null;
|
||||||
$this->showSelectedExceptionSummary = false;
|
|
||||||
$this->resetTable();
|
$this->resetTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -170,7 +165,6 @@ protected function getHeaderActions(): array
|
|||||||
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
$this->selectedFindingExceptionId = null;
|
$this->selectedFindingExceptionId = null;
|
||||||
$this->showSelectedExceptionSummary = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$actions[] = Action::make('open_selected_exception')
|
$actions[] = Action::make('open_selected_exception')
|
||||||
@ -331,31 +325,8 @@ public function table(Table $table): Table
|
|||||||
->label('Inspect exception')
|
->label('Inspect exception')
|
||||||
->icon('heroicon-o-eye')
|
->icon('heroicon-o-eye')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->before(function (FindingException $record): void {
|
->action(function (FindingException $record): void {
|
||||||
$this->selectedFindingExceptionId = (int) $record->getKey();
|
$this->selectedFindingExceptionId = (int) $record->getKey();
|
||||||
})
|
|
||||||
->slideOver()
|
|
||||||
->stickyModalHeader()
|
|
||||||
->modalSubmitAction(false)
|
|
||||||
->modalCancelAction(fn (Action $action): Action => $action->label('Close details'))
|
|
||||||
->modalHeading(function (): string {
|
|
||||||
$record = $this->inspectedFindingException();
|
|
||||||
|
|
||||||
return $record instanceof FindingException
|
|
||||||
? 'Finding exception #'.$record->getKey()
|
|
||||||
: 'Finding exception';
|
|
||||||
})
|
|
||||||
->modalDescription(fn (): ?string => $this->inspectedFindingException()?->requested_at?->toDayDateTimeString())
|
|
||||||
->modalContent(function (): View {
|
|
||||||
$record = $this->inspectedFindingException();
|
|
||||||
|
|
||||||
if (! $record instanceof FindingException) {
|
|
||||||
return view('filament.pages.monitoring.partials.finding-exception-queue-unavailable');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('filament.pages.monitoring.partials.finding-exception-queue-sidebar', [
|
|
||||||
'selectedException' => $record,
|
|
||||||
]);
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->bulkActions([])
|
->bulkActions([])
|
||||||
@ -372,7 +343,6 @@ public function table(Table $table): Table
|
|||||||
$this->removeTableFilter('status');
|
$this->removeTableFilter('status');
|
||||||
$this->removeTableFilter('current_validity_state');
|
$this->removeTableFilter('current_validity_state');
|
||||||
$this->selectedFindingExceptionId = null;
|
$this->selectedFindingExceptionId = null;
|
||||||
$this->showSelectedExceptionSummary = false;
|
|
||||||
$this->resetTable();
|
$this->resetTable();
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -384,7 +354,15 @@ public function selectedFindingException(): ?FindingException
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->resolveSelectedFindingException($this->selectedFindingExceptionId);
|
$record = $this->queueBaseQuery()
|
||||||
|
->whereKey($this->selectedFindingExceptionId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $record instanceof FindingException) {
|
||||||
|
throw new NotFoundHttpException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function selectedExceptionUrl(): ?string
|
public function selectedExceptionUrl(): ?string
|
||||||
@ -530,30 +508,6 @@ private function hasActiveQueueFilters(): bool
|
|||||||
|| is_string(data_get($this->tableFilters, 'current_validity_state.value'));
|
|| is_string(data_get($this->tableFilters, 'current_validity_state.value'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSelectedFindingException(int $findingExceptionId): FindingException
|
|
||||||
{
|
|
||||||
$record = $this->queueBaseQuery()
|
|
||||||
->whereKey($findingExceptionId)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $record instanceof FindingException) {
|
|
||||||
throw new NotFoundHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $record;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function inspectedFindingException(): ?FindingException
|
|
||||||
{
|
|
||||||
$mountedRecord = $this->getMountedTableActionRecord();
|
|
||||||
|
|
||||||
if ($mountedRecord instanceof FindingException) {
|
|
||||||
return $mountedRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->selectedFindingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function governanceWarning(FindingException $record): ?string
|
private function governanceWarning(FindingException $record): ?string
|
||||||
{
|
{
|
||||||
$finding = $record->relationLoaded('finding')
|
$finding = $record->relationLoaded('finding')
|
||||||
@ -233,8 +233,6 @@ private function applyActiveTab(Builder $query): Builder
|
|||||||
{
|
{
|
||||||
return match ($this->activeTab) {
|
return match ($this->activeTab) {
|
||||||
'active' => $query->healthyActive(),
|
'active' => $query->healthyActive(),
|
||||||
OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION => $query->activeStaleAttention(),
|
|
||||||
OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP => $query->terminalFollowUp(),
|
|
||||||
'blocked' => $query->dashboardNeedsFollowUp(),
|
'blocked' => $query->dashboardNeedsFollowUp(),
|
||||||
'succeeded' => $query
|
'succeeded' => $query
|
||||||
->where('status', OperationRunStatus::Completed->value)
|
->where('status', OperationRunStatus::Completed->value)
|
||||||
@ -283,29 +281,9 @@ private function applyRequestedDashboardPrefilter(): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$requestedProblemClass = request()->query('problemClass');
|
|
||||||
|
|
||||||
if (in_array($requestedProblemClass, [
|
|
||||||
OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION,
|
|
||||||
OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
|
|
||||||
], true)) {
|
|
||||||
$this->activeTab = (string) $requestedProblemClass;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$requestedTab = request()->query('activeTab');
|
$requestedTab = request()->query('activeTab');
|
||||||
|
|
||||||
if (in_array($requestedTab, [
|
if (in_array($requestedTab, ['all', 'active', 'blocked', 'succeeded', 'partial', 'failed'], true)) {
|
||||||
'all',
|
|
||||||
'active',
|
|
||||||
'blocked',
|
|
||||||
'succeeded',
|
|
||||||
'partial',
|
|
||||||
'failed',
|
|
||||||
OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION,
|
|
||||||
OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
|
|
||||||
], true)) {
|
|
||||||
$this->activeTab = (string) $requestedTab;
|
$this->activeTab = (string) $requestedTab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,7 +22,6 @@
|
|||||||
use App\Support\OpsUx\RunDetailPolling;
|
use App\Support\OpsUx\RunDetailPolling;
|
||||||
use App\Support\ReasonTranslation\ReasonPresenter;
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
||||||
use App\Support\RedactionIntegrity;
|
use App\Support\RedactionIntegrity;
|
||||||
use App\Support\RestoreSafety\RestoreSafetyCopy;
|
|
||||||
use App\Support\Tenants\ReferencedTenantLifecyclePresentation;
|
use App\Support\Tenants\ReferencedTenantLifecyclePresentation;
|
||||||
use App\Support\Tenants\TenantInteractionLane;
|
use App\Support\Tenants\TenantInteractionLane;
|
||||||
use App\Support\Tenants\TenantOperabilityQuestion;
|
use App\Support\Tenants\TenantOperabilityQuestion;
|
||||||
@ -245,42 +244,6 @@ public function lifecycleBanner(): ?array
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{tone: string, title: string, body: string, url: ?string, link_label: ?string}|null
|
|
||||||
*/
|
|
||||||
public function restoreContinuationBanner(): ?array
|
|
||||||
{
|
|
||||||
if (! isset($this->run)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$continuation = OperationRunResource::restoreContinuation($this->run);
|
|
||||||
|
|
||||||
if (! is_array($continuation)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tone = ($continuation['follow_up_required'] ?? false) ? 'amber' : 'sky';
|
|
||||||
$body = $continuation['summary'] ?? 'Restore continuation detail is unavailable.';
|
|
||||||
$boundary = $continuation['recovery_claim_boundary'] ?? null;
|
|
||||||
|
|
||||||
if (is_string($boundary) && $boundary !== '') {
|
|
||||||
$body .= ' '.RestoreSafetyCopy::recoveryBoundary($boundary);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! ($continuation['link_available'] ?? false)) {
|
|
||||||
$body .= ' Restore detail is not available from this session.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'tone' => $tone,
|
|
||||||
'title' => 'Restore continuation',
|
|
||||||
'body' => $body,
|
|
||||||
'url' => is_string($continuation['link_url'] ?? null) ? $continuation['link_url'] : null,
|
|
||||||
'link_label' => ($continuation['link_available'] ?? false) ? 'Open restore run' : null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{tone: string, title: string, body: string}|null
|
* @return array{tone: string, title: string, body: string}|null
|
||||||
*/
|
*/
|
||||||
@ -395,7 +395,6 @@ public static function table(Table $table): Table
|
|||||||
return $nextRun->format('M j, Y H:i:s');
|
return $nextRun->format('M j, Y H:i:s');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
->description(fn (BackupSchedule $record): ?string => static::scheduleFollowUpDescription($record))
|
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
@ -1150,31 +1149,4 @@ protected static function dayOfWeekOptions(): array
|
|||||||
7 => 'Sunday',
|
7 => 'Sunday',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function scheduleFollowUpDescription(BackupSchedule $record): ?string
|
|
||||||
{
|
|
||||||
if (! $record->is_enabled || $record->trashed()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$graceCutoff = now('UTC')->subMinutes(max(1, (int) config('tenantpilot.backup_health.schedule_overdue_grace_minutes', 30)));
|
|
||||||
$lastRunStatus = strtolower(trim((string) $record->last_run_status));
|
|
||||||
$isOverdue = $record->next_run_at?->lessThan($graceCutoff) ?? false;
|
|
||||||
$neverSuccessful = $record->last_run_at === null
|
|
||||||
&& ($isOverdue || ($record->created_at?->lessThan($graceCutoff) ?? false));
|
|
||||||
|
|
||||||
if ($neverSuccessful) {
|
|
||||||
return 'No successful run has been recorded yet.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isOverdue) {
|
|
||||||
return 'This schedule looks overdue.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($lastRunStatus, ['failed', 'partial', 'skipped', 'canceled'], true)) {
|
|
||||||
return 'The last run needs follow-up.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -3,11 +3,7 @@
|
|||||||
namespace App\Filament\Resources\BackupScheduleResource\Pages;
|
namespace App\Filament\Resources\BackupScheduleResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\BackupScheduleResource;
|
use App\Filament\Resources\BackupScheduleResource;
|
||||||
use App\Models\Tenant;
|
|
||||||
use App\Support\BackupHealth\TenantBackupHealthAssessment;
|
|
||||||
use App\Support\BackupHealth\TenantBackupHealthResolver;
|
|
||||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||||
use Filament\Facades\Filament;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
|
||||||
@ -68,23 +64,4 @@ private function syncCanonicalAdminTenantFilterState(): void
|
|||||||
tenantFilterName: null,
|
tenantFilterName: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSubheading(): ?string
|
|
||||||
{
|
|
||||||
if (request()->string('backup_health_reason')->toString() !== TenantBackupHealthAssessment::REASON_SCHEDULE_FOLLOW_UP) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenant = Filament::getTenant();
|
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
|
||||||
return 'One or more enabled schedules need follow-up.';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var TenantBackupHealthResolver $resolver */
|
|
||||||
$resolver = app(TenantBackupHealthResolver::class);
|
|
||||||
$summary = $resolver->assess($tenant)->scheduleFollowUp->summaryMessage;
|
|
||||||
|
|
||||||
return $summary ?? 'One or more enabled schedules need follow-up.';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -18,9 +18,6 @@
|
|||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Operations\BulkSelectionIdentity;
|
use App\Services\Operations\BulkSelectionIdentity;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\BackupHealth\TenantBackupHealthAssessment;
|
|
||||||
use App\Support\BackupHealth\TenantBackupHealthResolver;
|
|
||||||
use App\Support\BackupQuality\BackupQualityResolver;
|
|
||||||
use App\Support\Badges\BadgeDomain;
|
use App\Support\Badges\BadgeDomain;
|
||||||
use App\Support\Badges\BadgeRenderer;
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\Filament\TablePaginationProfiles;
|
use App\Support\Filament\TablePaginationProfiles;
|
||||||
@ -164,15 +161,6 @@ public static function table(Table $table): Table
|
|||||||
->persistFiltersInSession()
|
->persistFiltersInSession()
|
||||||
->persistSearchInSession()
|
->persistSearchInSession()
|
||||||
->persistSortInSession()
|
->persistSortInSession()
|
||||||
->modifyQueryUsing(fn (Builder $query): Builder => $query->with([
|
|
||||||
'items' => fn ($itemQuery) => $itemQuery->select([
|
|
||||||
'id',
|
|
||||||
'backup_set_id',
|
|
||||||
'payload',
|
|
||||||
'metadata',
|
|
||||||
'assignments',
|
|
||||||
]),
|
|
||||||
]))
|
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->searchable()
|
->searchable()
|
||||||
@ -184,11 +172,6 @@ public static function table(Table $table): Table
|
|||||||
->icon(BadgeRenderer::icon(BadgeDomain::BackupSetStatus))
|
->icon(BadgeRenderer::icon(BadgeDomain::BackupSetStatus))
|
||||||
->iconColor(BadgeRenderer::iconColor(BadgeDomain::BackupSetStatus)),
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::BackupSetStatus)),
|
||||||
Tables\Columns\TextColumn::make('item_count')->label('Items')->numeric()->sortable(),
|
Tables\Columns\TextColumn::make('item_count')->label('Items')->numeric()->sortable(),
|
||||||
Tables\Columns\TextColumn::make('backup_quality')
|
|
||||||
->label('Backup quality')
|
|
||||||
->state(fn (BackupSet $record): string => static::backupQualitySummary($record)->compactSummary)
|
|
||||||
->description(fn (BackupSet $record): string => static::backupQualitySummary($record)->nextAction)
|
|
||||||
->wrap(),
|
|
||||||
Tables\Columns\TextColumn::make('created_by')->label('Created by')->toggleable(isToggledHiddenByDefault: true),
|
Tables\Columns\TextColumn::make('created_by')->label('Created by')->toggleable(isToggledHiddenByDefault: true),
|
||||||
Tables\Columns\TextColumn::make('completed_at')->label('Completed')->dateTime()->since()->sortable(),
|
Tables\Columns\TextColumn::make('completed_at')->label('Completed')->dateTime()->since()->sortable(),
|
||||||
Tables\Columns\TextColumn::make('created_at')->label('Captured')->dateTime()->since()->sortable()->toggleable(isToggledHiddenByDefault: true),
|
Tables\Columns\TextColumn::make('created_at')->label('Captured')->dateTime()->since()->sortable()->toggleable(isToggledHiddenByDefault: true),
|
||||||
@ -676,23 +659,6 @@ private static function enterpriseDetailPage(BackupSet $record): EnterpriseDetai
|
|||||||
$metadataKeyCount = count($metadata);
|
$metadataKeyCount = count($metadata);
|
||||||
$relatedContext = static::relatedContextEntries($record);
|
$relatedContext = static::relatedContextEntries($record);
|
||||||
$isArchived = $record->trashed();
|
$isArchived = $record->trashed();
|
||||||
$qualitySummary = static::backupQualitySummary($record);
|
|
||||||
$backupHealthAssessment = static::backupHealthContinuityAssessment($record);
|
|
||||||
$qualityBadge = match (true) {
|
|
||||||
$qualitySummary->totalItems === 0 => $factory->statusBadge('No items', 'gray'),
|
|
||||||
$qualitySummary->hasDegradations() => $factory->statusBadge('Degraded input', 'warning', 'heroicon-m-exclamation-triangle'),
|
|
||||||
default => $factory->statusBadge('No degradations', 'success', 'heroicon-m-check-circle'),
|
|
||||||
};
|
|
||||||
$backupHealthBadge = $backupHealthAssessment instanceof TenantBackupHealthAssessment
|
|
||||||
? $factory->statusBadge(
|
|
||||||
static::backupHealthContinuityLabel($backupHealthAssessment),
|
|
||||||
$backupHealthAssessment->tone(),
|
|
||||||
'heroicon-m-exclamation-triangle',
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
$descriptionHint = $backupHealthAssessment instanceof TenantBackupHealthAssessment
|
|
||||||
? trim($backupHealthAssessment->headline.' '.($backupHealthAssessment->supportingMessage ?? ''))
|
|
||||||
: 'Backup quality, lifecycle status, and related operations stay ahead of raw backup metadata.';
|
|
||||||
|
|
||||||
return EnterpriseDetailBuilder::make('backup_set', 'tenant')
|
return EnterpriseDetailBuilder::make('backup_set', 'tenant')
|
||||||
->header(new SummaryHeaderData(
|
->header(new SummaryHeaderData(
|
||||||
@ -701,46 +667,14 @@ private static function enterpriseDetailPage(BackupSet $record): EnterpriseDetai
|
|||||||
statusBadges: [
|
statusBadges: [
|
||||||
$factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
|
$factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
|
||||||
$factory->statusBadge($isArchived ? 'Archived' : 'Active', $isArchived ? 'warning' : 'success'),
|
$factory->statusBadge($isArchived ? 'Archived' : 'Active', $isArchived ? 'warning' : 'success'),
|
||||||
...array_filter([$backupHealthBadge]),
|
|
||||||
$qualityBadge,
|
|
||||||
],
|
],
|
||||||
keyFacts: [
|
keyFacts: [
|
||||||
$factory->keyFact('Items', $record->item_count),
|
$factory->keyFact('Items', $record->item_count),
|
||||||
...array_filter([
|
|
||||||
$backupHealthAssessment instanceof TenantBackupHealthAssessment
|
|
||||||
? $factory->keyFact('Backup posture', static::backupHealthContinuityLabel($backupHealthAssessment), badge: $backupHealthBadge)
|
|
||||||
: null,
|
|
||||||
]),
|
|
||||||
$factory->keyFact('Backup quality', $qualitySummary->compactSummary),
|
|
||||||
$factory->keyFact('Created by', $record->created_by),
|
$factory->keyFact('Created by', $record->created_by),
|
||||||
$factory->keyFact('Completed', static::formatDetailTimestamp($record->completed_at)),
|
$factory->keyFact('Completed', static::formatDetailTimestamp($record->completed_at)),
|
||||||
$factory->keyFact('Captured', static::formatDetailTimestamp($record->created_at)),
|
$factory->keyFact('Captured', static::formatDetailTimestamp($record->created_at)),
|
||||||
],
|
],
|
||||||
descriptionHint: $descriptionHint,
|
descriptionHint: 'Lifecycle status, recovery readiness, and related operations stay ahead of raw backup metadata.',
|
||||||
))
|
|
||||||
->decisionZone($factory->decisionZone(
|
|
||||||
facts: array_values(array_filter([
|
|
||||||
$backupHealthAssessment instanceof TenantBackupHealthAssessment
|
|
||||||
? $factory->keyFact('Backup posture', static::backupHealthContinuityLabel($backupHealthAssessment), badge: $backupHealthBadge)
|
|
||||||
: null,
|
|
||||||
$factory->keyFact('Backup quality', $qualitySummary->compactSummary, badge: $qualityBadge),
|
|
||||||
$factory->keyFact('Degraded items', $qualitySummary->degradedItemCount),
|
|
||||||
$factory->keyFact('Metadata only', $qualitySummary->metadataOnlyCount),
|
|
||||||
$factory->keyFact('Assignment issues', $qualitySummary->assignmentIssueCount),
|
|
||||||
$factory->keyFact('Orphaned assignments', $qualitySummary->orphanedAssignmentCount),
|
|
||||||
$factory->keyFact('Integrity warnings', $qualitySummary->integrityWarningCount),
|
|
||||||
$qualitySummary->unknownQualityCount > 0
|
|
||||||
? $factory->keyFact('Unknown quality', $qualitySummary->unknownQualityCount)
|
|
||||||
: null,
|
|
||||||
])),
|
|
||||||
primaryNextStep: $factory->primaryNextStep(
|
|
||||||
$qualitySummary->nextAction,
|
|
||||||
'Backup quality',
|
|
||||||
),
|
|
||||||
description: 'Start here to judge whether this backup set looks strong or weak as restore input before reading diagnostics or raw metadata.',
|
|
||||||
compactCounts: $factory->countPresentation(summaryLine: $qualitySummary->summaryMessage),
|
|
||||||
attentionNote: $backupHealthAssessment?->positiveClaimBoundary ?? $qualitySummary->positiveClaimBoundary,
|
|
||||||
title: 'Backup quality',
|
|
||||||
))
|
))
|
||||||
->addSection(
|
->addSection(
|
||||||
$factory->factsSection(
|
$factory->factsSection(
|
||||||
@ -766,12 +700,11 @@ private static function enterpriseDetailPage(BackupSet $record): EnterpriseDetai
|
|||||||
->addSupportingCard(
|
->addSupportingCard(
|
||||||
$factory->supportingFactsCard(
|
$factory->supportingFactsCard(
|
||||||
kind: 'status',
|
kind: 'status',
|
||||||
title: 'Backup quality counts',
|
title: 'Recovery readiness',
|
||||||
items: [
|
items: [
|
||||||
$factory->keyFact('Degraded items', $qualitySummary->degradedItemCount),
|
$factory->keyFact('Backup state', $statusSpec->label, badge: $factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor)),
|
||||||
$factory->keyFact('Metadata only', $qualitySummary->metadataOnlyCount),
|
$factory->keyFact('Archived', $isArchived),
|
||||||
$factory->keyFact('Assignment issues', $qualitySummary->assignmentIssueCount),
|
$factory->keyFact('Metadata keys', $metadataKeyCount),
|
||||||
$factory->keyFact('Orphaned assignments', $qualitySummary->orphanedAssignmentCount),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
$factory->supportingFactsCard(
|
$factory->supportingFactsCard(
|
||||||
@ -807,64 +740,4 @@ private static function formatDetailTimestamp(mixed $value): string
|
|||||||
|
|
||||||
return $value->toDayDateTimeString();
|
return $value->toDayDateTimeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function backupQualitySummary(BackupSet $record): \App\Support\BackupQuality\BackupQualitySummary
|
|
||||||
{
|
|
||||||
if ($record->trashed()) {
|
|
||||||
$record->setRelation('items', $record->items()->withTrashed()->select([
|
|
||||||
'id',
|
|
||||||
'backup_set_id',
|
|
||||||
'payload',
|
|
||||||
'metadata',
|
|
||||||
'assignments',
|
|
||||||
])->get());
|
|
||||||
} elseif (! $record->relationLoaded('items')) {
|
|
||||||
$record->loadMissing([
|
|
||||||
'items' => fn ($query) => $query->select([
|
|
||||||
'id',
|
|
||||||
'backup_set_id',
|
|
||||||
'payload',
|
|
||||||
'metadata',
|
|
||||||
'assignments',
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return app(BackupQualityResolver::class)->summarizeBackupSet($record);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function backupHealthContinuityAssessment(BackupSet $record): ?TenantBackupHealthAssessment
|
|
||||||
{
|
|
||||||
$requestedReason = request()->string('backup_health_reason')->toString();
|
|
||||||
|
|
||||||
if (! in_array($requestedReason, [
|
|
||||||
TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
|
|
||||||
TenantBackupHealthAssessment::REASON_LATEST_BACKUP_DEGRADED,
|
|
||||||
], true)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var TenantBackupHealthResolver $resolver */
|
|
||||||
$resolver = app(TenantBackupHealthResolver::class);
|
|
||||||
$assessment = $resolver->assess((int) $record->tenant_id);
|
|
||||||
|
|
||||||
if ($assessment->latestRelevantBackupSetId !== (int) $record->getKey()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($assessment->primaryReason !== $requestedReason) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $assessment;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function backupHealthContinuityLabel(TenantBackupHealthAssessment $assessment): string
|
|
||||||
{
|
|
||||||
return match ($assessment->primaryReason) {
|
|
||||||
TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE => 'Latest backup is stale',
|
|
||||||
TenantBackupHealthAssessment::REASON_LATEST_BACKUP_DEGRADED => 'Latest backup is degraded',
|
|
||||||
default => ucfirst($assessment->posture),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -3,7 +3,6 @@
|
|||||||
namespace App\Filament\Resources\BackupSetResource\Pages;
|
namespace App\Filament\Resources\BackupSetResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\BackupSetResource;
|
use App\Filament\Resources\BackupSetResource;
|
||||||
use App\Support\BackupHealth\TenantBackupHealthAssessment;
|
|
||||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
@ -41,14 +40,4 @@ protected function getTableEmptyStateActions(): array
|
|||||||
BackupSetResource::makeCreateAction(),
|
BackupSetResource::makeCreateAction(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSubheading(): ?string
|
|
||||||
{
|
|
||||||
return match (request()->string('backup_health_reason')->toString()) {
|
|
||||||
TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS => 'No usable completed backup basis is currently available for this tenant.',
|
|
||||||
TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE => 'The latest backup detail is no longer available, so this view stays on the backup-set list.',
|
|
||||||
TenantBackupHealthAssessment::REASON_LATEST_BACKUP_DEGRADED => 'The latest backup detail is no longer available, so this view stays on the backup-set list.',
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -11,7 +11,6 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\BackupQuality\BackupQualityResolver;
|
|
||||||
use App\Support\Badges\BadgeDomain;
|
use App\Support\Badges\BadgeDomain;
|
||||||
use App\Support\Badges\BadgeRenderer;
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\Badges\TagBadgeDomain;
|
use App\Support\Badges\TagBadgeDomain;
|
||||||
@ -280,32 +279,11 @@ public function table(Table $table): Table
|
|||||||
->sortable()
|
->sortable()
|
||||||
->searchable()
|
->searchable()
|
||||||
->getStateUsing(fn (BackupItem $record) => $record->resolvedDisplayName()),
|
->getStateUsing(fn (BackupItem $record) => $record->resolvedDisplayName()),
|
||||||
Tables\Columns\TextColumn::make('snapshot_mode')
|
|
||||||
->label('Snapshot')
|
|
||||||
->badge()
|
|
||||||
->state(fn (BackupItem $record): string => $this->backupItemQualitySummary($record)->snapshotMode)
|
|
||||||
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicySnapshotMode))
|
|
||||||
->color(BadgeRenderer::color(BadgeDomain::PolicySnapshotMode))
|
|
||||||
->icon(BadgeRenderer::icon(BadgeDomain::PolicySnapshotMode))
|
|
||||||
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicySnapshotMode)),
|
|
||||||
Tables\Columns\TextColumn::make('policyVersion.version_number')
|
Tables\Columns\TextColumn::make('policyVersion.version_number')
|
||||||
->label('Version')
|
->label('Version')
|
||||||
->badge()
|
->badge()
|
||||||
->default('—')
|
->default('—')
|
||||||
->getStateUsing(fn (BackupItem $record): ?int => $record->policyVersion?->version_number),
|
->getStateUsing(fn (BackupItem $record): ?int => $record->policyVersion?->version_number),
|
||||||
Tables\Columns\TextColumn::make('backup_quality')
|
|
||||||
->label('Backup quality')
|
|
||||||
->state(fn (BackupItem $record): string => $this->backupItemQualitySummary($record)->compactSummary)
|
|
||||||
->description(function (BackupItem $record): string {
|
|
||||||
$summary = $this->backupItemQualitySummary($record);
|
|
||||||
|
|
||||||
if ($summary->assignmentCaptureReason === 'separate_role_assignments') {
|
|
||||||
return 'Assignments are captured separately for this item type.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $summary->nextAction;
|
|
||||||
})
|
|
||||||
->wrap(),
|
|
||||||
Tables\Columns\TextColumn::make('policy_type')
|
Tables\Columns\TextColumn::make('policy_type')
|
||||||
->label('Type')
|
->label('Type')
|
||||||
->badge()
|
->badge()
|
||||||
@ -502,11 +480,6 @@ private function backupItemInspectUrl(BackupItem $record): ?string
|
|||||||
return PolicyResource::getUrl('view', ['record' => $resolvedRecord->policy_id], tenant: $tenant);
|
return PolicyResource::getUrl('view', ['record' => $resolvedRecord->policy_id], tenant: $tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function backupItemQualitySummary(BackupItem $record): \App\Support\BackupQuality\BackupQualitySummary
|
|
||||||
{
|
|
||||||
return app(BackupQualityResolver::class)->forBackupItem($record);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveOwnerScopedBackupItemId(BackupSet $backupSet, mixed $record): int
|
private function resolveOwnerScopedBackupItemId(BackupSet $backupSet, mixed $record): int
|
||||||
{
|
{
|
||||||
$recordId = $this->normalizeBackupItemKey($record);
|
$recordId = $this->normalizeBackupItemKey($record);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user