TenantAtlas/docs/product/principles.md
ahmido 73a3a62451 Spec 122: Empty state consistency pass (#148)
## Summary
- unify empty-state UX across the six in-scope Filament list pages
- move empty-state ownership toward resource `table()` definitions while preserving existing RBAC behavior
- add focused Pest coverage for empty-state rendering, CTA outcomes, populated-state regression behavior, and action-surface compliance
- add the Spec 122 planning artifacts and product discovery documents used for this pass

## Changed surfaces
- `PolicyResource`
- `BackupSetResource`
- `RestoreRunResource`
- `BackupScheduleResource`
- `WorkspaceResource`
- `AlertDeliveryResource`

## Tests
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/EmptyStateConsistencyTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/CreateCtaPlacementTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/PolicySyncStartSurfaceTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/BackupScheduling/BackupScheduleLifecycleAuthorizationTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/BackupSetUiEnforcementTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/RestoreRunUiEnforcementTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php`
- `vendor/bin/sail bin pint --dirty --format agent`

## Notes
- Filament v5 / Livewire v4.0+ compliance is preserved.
- Panel provider registration remains unchanged in `bootstrap/providers.php`.
- No new globally searchable resources were added.
- Destructive actions were not introduced by this pass.
- Alert Deliveries is documented as the explicit no-header-action exemption for the empty-state CTA relocation rule.
- Manual light/dark visual QA evidence is still expected in the PR/review artifact set for the remaining checklist items (`T018`, `T025`).

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #148
2026-03-08 02:17:51 +00:00

4.1 KiB

Product Principles

Permanent product principles that govern every spec, every UI decision, and every architectural choice. New specs must align with these. If a principle needs to change, update this file first.

Last reviewed: 2026-03-08


Identity & Isolation

Workspace-first context

Workspace is the primary session context. Every UI surface, every query, every action is workspace-scoped. Non-members receive deny-as-not-found (404 semantics) — they never learn the resource exists.

Tenant isolation (non-negotiable)

Every read/write is tenant-scoped. Cross-tenant views are explicit, access-checked, aggregation-based. Non-member → 404. No cross-embedding of workspace-owned and tenant-owned data.

SCOPE-001: Strict ownership model

  • Workspace-owned = standards, templates, configuration, baselines
  • Tenant-owned = observed state, evidence, artifacts, inventory

Authorization & Safety

Capability-first RBAC

Single canonical registry (Capabilities.php). No raw strings. CI fails on unknown capabilities. UI visibility is never a security boundary — missing server-side auth is a P0 bug.

Visible-but-disabled UX

Members see disabled actions with tooltip explaining the missing capability. Non-members see nothing (404 semantics).

Destructive actions require safe flows

All destructive actions → requiresConfirmation(). No exceptions. Write operations require: preview/dry-run → confirmation → audit log → tests. High-risk types default to preview-only.


Operations & Observability

3-Surface Feedback (non-negotiable)

  1. Toast — intent acknowledged
  2. Progress — active work visible
  3. Terminal DB Notification — audit record

No other feedback patterns. No silent mutations.

OperationRun lifecycle is service-owned

All status/outcome transitions via OperationRunService only. Summary counts via OperationSummaryKeys::all(). Flat numeric only.

Enterprise-grade auditability

Every mutation has a trail. Backup created, restore attempted, policy change detected — logged, tenant-scoped, RBAC-respecting.


Data & Architecture

Inventory-first, Snapshots-second

  • InventoryItem = last observed metadata
  • PolicyVersion.snapshot = explicit immutable JSONB capture
  • Intune remains external source of truth

Single Contract Path to Graph

All MS Graph calls via GraphClientInterface. Endpoints modeled in config/graph_contracts.php. No hardcoded "quick endpoints". Unknown types fail safe.

Deterministic Capabilities

Backup/restore/risk flags derived deterministically from config via Capabilities Resolver. Must be snapshot-testable.

Data minimization & safe logging

Inventory = metadata only. No secrets in logs. Monitoring relies on run records + error codes.


UI & Information Architecture

UX-001: Layout & IA Standards

Main/Aside layout. Sections required. View pages use Infolists. Empty states with specific title + explanation + exactly 1 CTA.

Action Surface Contract (non-negotiable)

Required surfaces per page type (list/view/create/edit). Max 2 visible row actions. Destructive requires confirmation. Every spec with UI changes must include a UI Action Matrix.

Badge semantics centralized

All status badges via BadgeCatalog / BadgeRenderer. No ad-hoc badge mappings.

Canonical navigation and terminology

Consistent naming, consistent routing, consistent mental model. No competing terms for the same concept.


Process

Spec-first workflow

Runtime behavior changes require spec update first. Every spec must declare: scope, primary routes, data ownership, RBAC requirements (SCOPE-002).

Regression guards mandatory

RBAC regression tests per role. Ops-UX regression guards prevent direct status writes and ad-hoc notifications. Architectural guard tests enforce code-level contracts.


Filament v5 Alignment

Non-negotiables

  • Livewire v4.0+
  • Panel providers in bootstrap/providers.php
  • Global search requires Edit/View page or is disabled
  • Prefer render hooks + CSS hooks over publishing internal views
  • Heavy assets loaded on-demand (loadedOnRequest())