# TenantPilot — Comprehensive Filter Audit

> **Stack:** Laravel 12 · Filament v5 · Livewire v4 · PostgreSQL
> **Scope:** Every table surface across Admin Panel, System Console, Dashboard Widgets & Livewire Pickers
> **Date:** 2025-07-11

---

## 1. Executive Summary

The platform has **36 table surfaces** across 18 Resources, 6 RelationManagers, 9 custom/system pages, 2 dashboard widgets, and 3 Livewire picker components.

**Key findings:**

| Metric | Value |
|---|---|
| Surfaces with ≥1 filter | 16 of 36 (44 %) |
| Surfaces with zero filters | 20 of 36 (56 %) |
| Total filter instances | ~54 |
| Surfaces with `persistFiltersInSession()` | 7 of 36 (19 %) |
| Surfaces with filter defaults | 6 |
| Filter types used | `SelectFilter` (dominant), `Filter::make` (boolean), `TrashedFilter`, `TernaryFilter`, `DatePicker` range |
| Highest filter count on one surface | 9 (FindingResource) |
| Resources with zero filters that need them | 5+ (identified below) |

**Critical inconsistencies:**
- Session persistence is applied to only 7 of 16 filtered surfaces — the other 9 filtered surfaces lose user state on navigation.
- Filter defaults vary wildly: some surfaces pre-filter to "active" records, others show everything.
- No date range filter exists on surfaces that clearly need one (Findings, AlertDelivery, RestoreRun).
- Status/enum filters use inconsistent option sets and labeling across the platform.
- The `TrashedFilter` (Active/All/Archived) pattern is consistent where used but only applied to 4 resources.

---

## 2. Complete Surface Inventory

### 2A. Resources (18)

| # | Resource | Domain | Scope | Filters | Persist? | Default |
|---|---|---|---|---|---|---|
| 1 | PolicyResource | Inventory | Tenant | 4 (visibility, policy_type, category, platform) | ✅ | visibility=active |
| 2 | FindingResource | Governance | Tenant | 9 (open, overdue, high_severity, my_assigned, status, finding_type, evidence_fidelity, scope_key, run_ids) | ✅ | open=true |
| 3 | OperationRunResource | Monitoring | Workspace | 6 (tenant_id, type, status, outcome, initiator_name, created_at) | ✅ | tenant=active, created_at=30d |
| 4 | TenantResource | Settings | Workspace | 3 (trashed, environment, app_status) | ✅ | trashed=active |
| 5 | BackupScheduleResource | Backups | Tenant | 2 (trashed, enabled_state) | ✅ | — |
| 6 | BackupSetResource | Backups | Tenant | 1 (trashed) | ✅ | — |
| 7 | ProviderConnectionResource | Settings | Workspace | 5 (tenant, provider, status, health_status, default_only) | ✅ | tenant=scoped |
| 8 | PolicyVersionResource | Inventory | Tenant | 1 (trashed) | ❌ | — |
| 9 | RestoreRunResource | Backups | Tenant | 1 (trashed) | ❌ | — |
| 10 | InventoryItemResource | Inventory | Tenant | 2 (policy_type, category) | ❌ | — |
| 11 | ReviewPackResource | Reporting | Tenant | 1 (status) | ❌ | — |
| 12 | AlertDeliveryResource | Monitoring | Workspace | 3 (status, event_type, alert_destination_id) | ❌ | — |
| 13 | EntraGroupResource | Directory | Tenant | 2 (stale, group_type) | ❌ | — |
| 14 | AlertRuleResource | Monitoring | Workspace | 0 | ❌ | — |
| 15 | AlertDestinationResource | Monitoring | Workspace | 0 | ❌ | — |
| 16 | BaselineProfileResource | Governance | Workspace | 0 | ❌ | — |
| 17 | BaselineSnapshotResource | Governance | Workspace | 0 | ❌ | — |
| 18 | WorkspaceResource | Settings | Workspace | 0 | ❌ | — |

### 2B. RelationManagers (6)

| # | RelationManager | Parent | Filters | Notes |
|---|---|---|---|---|
| 1 | VersionsRelationManager | PolicyResource | 0 (`->filters([])`) | Explicit empty |
| 2 | BackupItemsRelationManager | BackupSetResource | 0 (`->filters([])`) | Explicit empty |
| 3 | BackupScheduleOperationRunsRelationManager | BackupScheduleResource | 0 (`->filters([])`) | Explicit empty |
| 4 | BaselineTenantAssignmentsRelationManager | BaselineProfileResource | 0 | No filters block |
| 5 | TenantMembershipsRelationManager | TenantResource | 0 | No filters block |
| 6 | WorkspaceMembershipsRelationManager | WorkspaceResource | 0 | No filters block |

### 2C. Custom & System Pages (9)

| # | Page | Panel | Filters | Notes |
|---|---|---|---|---|
| 1 | InventoryCoverage | Admin | 1-2 (category, restore mode) | Dynamic — restore filter only if options exist |
| 2 | Monitoring/Operations | Admin | Reuses OperationRunResource::table() | Inherits 6 filters + persistence |
| 3 | System/Ops/Runs | System | 0 | No filters on platform ops listing |
| 4 | System/Ops/Failures | System | 0 | Pre-filtered query (failed only), no user filters |
| 5 | System/Ops/Stuck | System | 0 | Pre-filtered via StuckRunClassifier, no user filters |
| 6 | System/Directory/Tenants | System | 0 | No filters |
| 7 | System/Directory/Workspaces | System | 0 | No filters |
| 8 | System/Security/AccessLogs | System | 0 | No filters |
| 9 | System/RepairWorkspaceOwners | System | 0 | No filters |

### 2D. Dashboard Widgets (2)

| # | Widget | Filters | Notes |
|---|---|---|---|
| 1 | RecentDriftFindings | 0 | Polling-based, no filter UI |
| 2 | RecentOperations | 0 | Polling-based, no filter UI |

### 2E. Livewire Picker Components (3)

| # | Component | Filters | Notes |
|---|---|---|---|
| 1 | BackupSetPolicyPickerTable | 5 (policy_type, platform, synced_within, ignored, has_versions) | Most filters of any picker; synced_within default=7, ignored default=false |
| 2 | EntraGroupCachePickerTable | 2 (stale, group_type) | Mirrors EntraGroupResource filters |
| 3 | SettingsCatalogSettingsTable | 0 | Read-only inspection table; purely search-driven |

---

## 3. Metrics Dashboard

### 3A. Filter Type Distribution

| Filter Type | Count | Surfaces Using It |
|---|---|---|
| `SelectFilter` | ~35 | PolicyResource, FindingResource, OperationRunResource, TenantResource, BackupScheduleResource, ProviderConnectionResource, InventoryItemResource, ReviewPackResource, AlertDeliveryResource, EntraGroupResource, InventoryCoverage, BackupSetPolicyPicker, EntraGroupCachePicker |
| `Filter::make` (boolean/custom) | ~8 | FindingResource (4: open, overdue, high_severity, my_assigned), ProviderConnectionResource (1: default_only), FindingResource (2: scope_key, run_ids) |
| `TrashedFilter` | 4 | TenantResource, BackupScheduleResource, BackupSetResource, PolicyVersionResource, RestoreRunResource |
| `TernaryFilter` | 1 | BackupSetPolicyPickerTable (ignored) |
| Date range (`DatePicker`) | 1 | OperationRunResource (created_at) |

### 3B. Persistence Coverage

| Category | With Persistence | Without | Total |
|---|---|---|---|
| Resources with filters | 7 | 6 | 13 |
| RelationManagers | 0 | 6 | 6 |
| Custom/System Pages | 1 (via reuse) | 8 | 9 |
| Widgets | 0 | 2 | 2 |
| Livewire Pickers | 0 | 3 | 3 |

### 3C. Default Filter Coverage

| Surface | Filter | Default Value | Impact |
|---|---|---|---|
| PolicyResource | visibility | `active` | Hides ignored policies on first load |
| FindingResource | open | `true` | Shows only open findings on first load |
| OperationRunResource | tenant_id | Active tenant | Scopes to current tenant context |
| OperationRunResource | created_at | Last 30 days | Time-bounds initial view |
| TenantResource | trashed | `true` (Active) | Hides archived tenants |
| BackupSetPolicyPickerTable | synced_within | `7` (7 days) | Only recently synced policies |
| BackupSetPolicyPickerTable | ignored | `false` | Hides ignored policies |
| ProviderConnectionResource | tenant | Scoped tenant external_id | Pre-scopes to current tenant |

---

## 4. Detailed Findings Per Table

### 4.1 PolicyResource

**Filters (4):**
```
SelectFilter('visibility') → default('active'), custom query (active hides ignored_at)
SelectFilter('policy_type') → from config, no searchable
SelectFilter('category') → custom query mapping types to categories
SelectFilter('platform') → from distinct DB values
```

**Implicit filter:** `modifyQueryUsing` hides policies not synced in 7 days.

**Assessment:**
- ✅ Persistence: full (search, sort, filters)
- ✅ Default: sensible (filters to active policies)
- ⚠️ `policy_type` options come from config — could be `->searchable()` for large lists
- ⚠️ `platform` options use raw DB distinct — no label formatting
- ⚠️ No date range filter (last_synced_at is sortable but not filterable)

---

### 4.2 FindingResource

**Filters (9) — highest in the product:**
```
Filter::make('open') → boolean quick, default(true)
Filter::make('overdue') → boolean quick
Filter::make('high_severity') → boolean quick
Filter::make('my_assigned') → boolean quick
SelectFilter('status') → 8 manual options
SelectFilter('finding_type') → 3 manual options
SelectFilter('evidence_fidelity') → 2 manual options
Filter::make('scope_key') → TextInput form
Filter::make('run_ids') → 2 TextInputs (baseline + current run)
```

**Assessment:**
- ✅ Persistence: full
- ✅ Default: sensible (open=true)
- ⚠️ 9 filters may overwhelm users — consider filter groups or collapsible sections
- ⚠️ `scope_key` and `run_ids` are advanced/developer filters — should be in an "Advanced" section
- ⚠️ No date range filter (created_at, due_at exist but are not filterable)
- ⚠️ `status` has 8 manual options — should use enum for consistency

---

### 4.3 OperationRunResource

**Filters (6):**
```
SelectFilter('tenant_id') → dynamic default=active tenant, relationship options, searchable
SelectFilter('type') → from distinct DB values
SelectFilter('status') → from OperationRunStatus enum
SelectFilter('outcome') → from OperationRunOutcome enum with labels
SelectFilter('initiator_name') → from distinct DB, searchable
Filter::make('created_at') → DatePicker from/until, default=last 30 days
```

**Assessment:**
- ✅ Persistence: full
- ✅ Defaults: excellent (tenant + date range scoped)
- ✅ Best filter UX in the product — good model for others
- ⚠️ `type` uses raw DB distinct instead of OperationCatalog labels
- ✅ Date range filter — only surface that has one

---

### 4.4 TenantResource

**Filters (3):**
```
TrashedFilter::make() → labels (Active/All/Archived), default(true)
SelectFilter('environment') → 4 options
SelectFilter('app_status') → 4 options
```

**Assessment:**
- ✅ Persistence: full
- ✅ TrashedFilter pattern with default
- ✅ Good but minimal

---

### 4.5 BackupScheduleResource

**Filters (2):**
```
TrashedFilter::make() → Active/All/Archived
SelectFilter('enabled_state') → custom query
```

**Assessment:**
- ✅ Persistence: full
- ⚠️ Only 2 filters for a moderately complex resource

---

### 4.6 BackupSetResource

**Filters (1):**
```
TrashedFilter::make() → Active/All/Archived
```

**Assessment:**
- ✅ Persistence: full
- ⚠️ Minimal filtering — no status, no date range, no policy type summary filter

---

### 4.7 ProviderConnectionResource

**Filters (5):**
```
SelectFilter('tenant') → default=scoped tenant external_id, custom query
SelectFilter('provider') → 1 option (microsoft)
SelectFilter('status') → 4 options
SelectFilter('health_status') → 4 options
Filter::make('default_only') → boolean
```

**Assessment:**
- ✅ Persistence: full
- ✅ Good filter coverage for the domain
- ⚠️ `provider` filter has only 1 option — may be premature; could be hidden until >1 provider exists

---

### 4.8 PolicyVersionResource

**Filters (1):**
```
TrashedFilter::make() → Active/All/Archived
```

**Assessment:**
- ❌ No persistence
- ⚠️ Missing: policy_type, platform, date range (captured_at)
- ⚠️ Gap: this is a high-traffic resource with version history — more filters needed

---

### 4.9 RestoreRunResource

**Filters (1):**
```
TrashedFilter::make() → Active/All/Archived
```

**Assessment:**
- ❌ No persistence
- ⚠️ Missing: status, outcome, date range (started_at), dry_run flag
- ⚠️ Gap: operators need to quickly find failed or dry-run restores

---

### 4.10 InventoryItemResource

**Filters (2):**
```
SelectFilter('policy_type') → searchable
SelectFilter('category') → searchable
```

**Assessment:**
- ❌ **No persistence** — critical gap since this is a frequently navigated list
- ⚠️ Missing: platform, sync freshness, version count
- ⚠️ Both filters are searchable — good, but should match PolicyResource pattern

---

### 4.11 ReviewPackResource

**Filters (1):**
```
SelectFilter('status') → from ReviewPackStatus enum
```

**Assessment:**
- ❌ No persistence
- ⚠️ Single filter is reasonable for current scope

---

### 4.12 AlertDeliveryResource

**Filters (3):**
```
SelectFilter('status') → 6 options
SelectFilter('event_type') → dynamic from DB
SelectFilter('alert_destination_id') → dynamic from DB
```

**Assessment:**
- ❌ No persistence
- ⚠️ Missing: date range (created_at), tenant filter if multi-tenant scope
- ⚠️ Status options are manual strings — should use enum

---

### 4.13 EntraGroupResource

**Filters (2):**
```
SelectFilter('stale') → config-based staleness cutoff query
SelectFilter('group_type') → complex JSON/boolean query
```

**Assessment:**
- ❌ No persistence
- ⚠️ Complex custom queries — well-implemented but should document the staleness logic
- ⚠️ Mirrors EntraGroupCachePickerTable filters — good consistency

---

### 4.14–4.18 Resources with ZERO filters

| Resource | Assessment |
|---|---|
| **AlertRuleResource** | ⚠️ Missing: is_active toggle, event type, severity |
| **AlertDestinationResource** | ⚠️ Missing: type/channel filter |
| **BaselineProfileResource** | ⚠️ Missing: status filter |
| **BaselineSnapshotResource** | ⚠️ Missing: state filter (gaps/complete) |
| **WorkspaceResource** | ✅ Acceptable — small dataset, search suffices |

---

## 5. Cross-Cutting Inconsistencies

### 5.1 Persistence Gap

**7 resources** have persistence. **11 resources + all 6 relation managers + 8 system pages** do not.

The guard test (`FilamentTableStandardsGuardTest`) only enforces persistence on 7 "critical" resources:
- TenantResource, PolicyResource, BackupSetResource, BackupScheduleResource
- ProviderConnectionResource, FindingResource, OperationRunResource

**Missing from guard:** PolicyVersionResource, RestoreRunResource, InventoryItemResource, AlertDeliveryResource, EntraGroupResource, ReviewPackResource.

### 5.2 Filter Default Inconsistency

| Pattern | Active-by-default Behavior | Surfaces |
|---|---|---|
| `SelectFilter('visibility')->default('active')` | Custom query | PolicyResource |
| `Filter::make('open')->default()` | Boolean toggle | FindingResource |
| `TrashedFilter->default(true)` | Hides archived | TenantResource |
| `SelectFilter('tenant_id')->default(...)` | Scopes to context | OperationRunResource |
| No default | Shows everything | 10+ surfaces |

**Problem:** Users expect consistent behavior across similar resources. Some show "active" records by default, some show everything.

### 5.3 Status Filter Inconsistency

Status filtering is implemented differently across surfaces:

| Surface | Approach |
|---|---|
| OperationRunResource | Enum → `OperationRunStatus::class` |
| FindingResource | Manual array of 8 strings |
| AlertDeliveryResource | Manual array of 6 strings |
| ReviewPackResource | Enum → `ReviewPackStatus::class` |
| PolicyVersion/RestoreRun | TrashedFilter (soft-delete based) |

**Recommendation:** Always source status options from the model's status enum or a centralized catalog.

### 5.4 Date Range Filter Gap

Only **OperationRunResource** has a date range filter (`created_at` with DatePicker form).

Surfaces that should have one but don't:
- **FindingResource** — has `created_at`, `due_at`
- **AlertDeliveryResource** — has `created_at`
- **RestoreRunResource** — has `started_at`, `completed_at`
- **PolicyVersionResource** — has `captured_at`
- **BackupScheduleResource** — has execution history

### 5.5 TrashedFilter Labeling

All 4 resources using TrashedFilter apply the same label pattern — **this is consistent ✅:**
```php
TrashedFilter::make()
    ->label('Archived')
    ->placeholder('Active')
    ->trueLabel('All')
    ->falseLabel('Archived')
```

### 5.6 Tenant Scoping in Filters

| Surface | Approach |
|---|---|
| OperationRunResource | Explicit `SelectFilter('tenant_id')` with dynamic default |
| ProviderConnectionResource | Explicit `SelectFilter('tenant')` with external_id default |
| AlertDeliveryResource | ❌ No tenant filter (scoped in query only) |
| System pages | ❌ No filters at all (but cross-tenant by design) |

---

## 6. Repeated Patterns (Extractable)

### Pattern A: TrashedFilter Standard
```php
TrashedFilter::make()
    ->label('Archived')
    ->placeholder('Active')
    ->trueLabel('All')
    ->falseLabel('Archived')
```
**Used in:** TenantResource, BackupScheduleResource, BackupSetResource, PolicyVersionResource, RestoreRunResource
**Extraction:** Could be a helper `FilterPresets::trashedArchive()`.

### Pattern B: Config-Sourced SelectFilter
```php
SelectFilter::make('policy_type')
    ->label('Policy type')
    ->options($options) // from config or DB
    ->searchable()
```
**Used in:** PolicyResource, InventoryItemResource, BackupSetPolicyPickerTable
**Note:** Options source varies (config vs DB distinct vs static array).

### Pattern C: Dynamic DB Distinct Options
```php
SelectFilter::make('platform')
    ->options(fn (): array => Policy::query()
        ->where(...)
        ->whereNotNull('platform')
        ->distinct()
        ->orderBy('platform')
        ->pluck('platform', 'platform')
        ->all())
```
**Used in:** PolicyResource (platform), OperationRunResource (type, initiator_name), BackupSetPolicyPickerTable (platform), AlertDeliveryResource (event_type, alert_destination_id)
**Risk:** N+1 on every filter render — should cache or use `->options()` with static method.

### Pattern D: Date Range with DatePicker (ONLY ONE INSTANCE)
```php
Filter::make('created_at')
    ->form([
        Forms\Components\DatePicker::make('from')->label('From'),
        Forms\Components\DatePicker::make('until')->label('Until'),
    ])
    ->query(...)
    ->indicateUsing(...)
```
**Used only in:** OperationRunResource
**Should be in:** FindingResource, AlertDeliveryResource, RestoreRunResource, PolicyVersionResource

### Pattern E: Boolean Quick Filters
```php
Filter::make('open')
    ->query(fn (Builder $query): Builder => $query->where('status', 'open'))
    ->default()
```
**Used in:** FindingResource (4 boolean quick filters)
**Note:** Filament v5's `ToggleFilter` or `TernaryFilter` may provide better UX than `Filter::make` with `->default()`.

### Pattern F: Staleness Cutoff Filter
```php
SelectFilter::make('stale')
    ->options(['1' => 'Stale', '0' => 'Fresh'])
    ->query(function (Builder $query, array $data) use ($cutoff): Builder { ... })
```
**Used in:** EntraGroupResource, EntraGroupCachePickerTable (**consistent ✅**)

---

## 7. Recommended Filter UX Standard

### 7.1 Tier System

| Tier | Surface Type | Filter Requirements |
|---|---|---|
| **Tier 1 — Critical** | High-traffic Resource lists (Policy, Finding, OperationRun, Tenant, InventoryItem) | Full persistence, smart defaults, date range where applicable |
| **Tier 2 — Important** | Moderate-traffic Resources (BackupSchedule, BackupSet, RestoreRun, PolicyVersion, ProviderConnection, AlertDelivery, EntraGroup) | Persistence, domain-appropriate filters, defaults for soft-delete |
| **Tier 3 — Standard** | Low-traffic Resources (ReviewPack, AlertRule, AlertDestination, BaselineProfile, BaselineSnapshot, Workspace) | Optional persistence, basic status filter if applicable |
| **Tier 4 — Embedded** | RelationManagers, Widgets, System pages | No persistence needed, filters only if >50 typical records |
| **Tier 5 — Pickers** | Modal picker tables | Task-specific filters, smart defaults to reduce noise |

### 7.2 Mandatory Filter Rules

1. **Persistence:** All Tier 1 and Tier 2 surfaces MUST have `->persistFiltersInSession()`, `->persistSearchInSession()`, `->persistSortInSession()`.
2. **Soft-delete:** Every resource with `SoftDeletes` MUST have the standard `TrashedFilter` with Active/All/Archived labels.
3. **Status enums:** All status filters MUST source options from the model's enum — no manual string arrays.
4. **Date range:** Surfaces with time-series data (created_at, captured_at, started_at) in Tier 1–2 SHOULD have a date range filter.
5. **Defaults:** Surfaces that commonly have "inactive" records (ignored, archived, completed) SHOULD default to showing only "active" records.

### 7.3 Filter Catalog (Extractable Presets)

```php
// Proposed: App\Support\Filament\FilterPresets
class FilterPresets
{
    public static function trashedArchive(): TrashedFilter;
    public static function dateRange(string $column, ?int $defaultDays = null): Filter;
    public static function policyType(?int $tenantId = null): SelectFilter;
    public static function platform(?int $tenantId = null): SelectFilter;
    public static function tenantScope(): SelectFilter;
}
```

---

## 8. Filament-Native Feasibility

| Pattern | Filament v5 Native? | Notes |
|---|---|---|
| `SelectFilter` | ✅ | Core, well-supported |
| `TrashedFilter` | ✅ | Built-in, perfect for soft-delete |
| `TernaryFilter` | ✅ | Used once (BackupSetPolicyPicker); underutilized |
| `Filter::make` + `->default()` | ✅ | Works but boolean toggles are better as `TernaryFilter` |
| `Filter::make` + `DatePicker` form | ✅ | Native pattern |
| `->persistFiltersInSession()` | ✅ | Just needs to be added |
| `->indicateUsing()` | ✅ | Critical UX — shows active filter badges |
| Filter groups / sections | ⚠️ | Not natively supported; would need layout-level workaround |
| FilterPresets helper | Custom | Thin wrapper — not against Filament philosophy |
| Filter guards (tests) | Custom | Already done via `FilamentTableStandardsGuardTest` |

**Verdict:** 100% achievable with Filament-native APIs. The only custom code needed is a thin `FilterPresets` helper for DRY and a guard test expansion.

---

## 9. Prioritized Gap List & Refactor Plan

### P0 — Critical (Session + Data Integrity)

| # | Surface | Action | Effort |
|---|---|---|---|
| 1 | InventoryItemResource | Add `persistFiltersInSession()` + `persistSearchInSession()` + `persistSortInSession()` | XS |
| 2 | PolicyVersionResource | Add full persistence trio | XS |
| 3 | RestoreRunResource | Add full persistence trio | XS |
| 4 | AlertDeliveryResource | Add full persistence trio | XS |
| 5 | EntraGroupResource | Add full persistence trio | XS |
| 6 | Guard test | Expand persistence enforcement list to include all Tier 1–2 resources | S |

### P1 — High (Missing Essential Filters)

| # | Surface | Action | Effort |
|---|---|---|---|
| 7 | RestoreRunResource | Add status, outcome, date range filters | S |
| 8 | PolicyVersionResource | Add policy_type, platform, date range (captured_at) filters | S |
| 9 | FindingResource | Add date range filter (created_at/due_at) | S |
| 10 | AlertDeliveryResource | Add date range filter (created_at) | S |
| 11 | InventoryItemResource | Add platform, sync freshness filters | S |
| 12 | BaselineProfileResource | Add status filter | XS |

### P2 — Medium (Consistency & UX Polish)

| # | Surface | Action | Effort |
|---|---|---|---|
| 13 | FindingResource status | Refactor from manual strings to enum options | XS |
| 14 | AlertDeliveryResource status | Refactor from manual strings to enum options | XS |
| 15 | OperationRunResource type | Use OperationCatalog labels instead of raw DB strings | XS |
| 16 | FilterPresets helper | Extract TrashedFilter, dateRange, policyType presets | S |
| 17 | ProviderConnectionResource | Consider hiding provider filter until >1 provider exists | XS |
| 18 | AlertRuleResource | Add is_active, event_type filters | S |
| 19 | System/Ops/Runs | Add basic type/status/workspace filters | M |
| 20 | System/Directory/Tenants | Add workspace, status filters | S |

### P3 — Low (Nice-to-Have)

| # | Surface | Action | Effort |
|---|---|---|---|
| 21 | FindingResource | Move scope_key and run_ids filters to "Advanced" section | S |
| 22 | BackupSetResource | Add backup frequency or item count filter | S |
| 23 | BaselineSnapshotResource | Add state filter (has_gaps/complete) | S |
| 24 | System/Security/AccessLogs | Add status and date range filters | S |
| 25 | `->indicateUsing()` | Add active filter indicators to all date range filters | S |

---

## 10. Test Strategy

### Existing Coverage
- `FilamentTableStandardsGuardTest` enforces: defaultSort, emptyStateHeading, persistence (7 resources), TablePaginationProfiles usage.

### Recommended Extensions

**10.1 Expand Persistence Guard**
Add all Tier 1–2 resources to the persistence enforcement list.

**10.2 Filter Consistency Guard**
```php
it('uses enum-based options for status filters', function () {
    // Scan all files for SelectFilter('status') and verify they use enum classes
});

it('applies TrashedFilter on all soft-deletable resource tables', function () {
    // Cross-reference models with SoftDeletes trait against resource filter lists
});
```

**10.3 Filter Default Guard**
```php
it('applies smart defaults on tier-1 resource filters', function () {
    // Verify PolicyResource, FindingResource, OperationRunResource have defaults
});
```

**10.4 Functional Filter Tests**
For each Tier 1–2 surface, test:
- Filter applies correct query scope
- Filter default shows expected subset
- Filter clears fully when reset
- Multiple filters compose correctly

---

## 11. Spec Recommendations

### Proposed Spec: `XXX-filter-ux-standardization`

**Scope:** Establish and enforce a repo-wide filter UX standard.

**Deliverables:**
1. `FilterPresets` helper class with extractable patterns
2. Persistence trio added to all Tier 1–2 surfaces
3. Date range filters added to 5 time-series surfaces
4. Status filters migrated to enum-based options
5. Guard test expanded with filter-specific assertions
6. TrashedFilter standardized across all soft-deletable resources

**Dependencies:**
- Builds on completed `125-table-ux-standardization` spec
- No breaking changes — purely additive

**Estimated effort:** ~3–4 working sessions

---

## Appendix A: Full Filter Inventory Matrix

| Surface | SelectFilter | Filter::make | TrashedFilter | TernaryFilter | DatePicker | Total | Persist | Defaults |
|---|---|---|---|---|---|---|---|---|
| PolicyResource | 4 | 0 | 0 | 0 | 0 | 4 | ✅ | 1 |
| FindingResource | 3 | 6 | 0 | 0 | 0 | 9 | ✅ | 1 |
| OperationRunResource | 5 | 1 | 0 | 0 | 1 | 6 | ✅ | 2 |
| TenantResource | 2 | 0 | 1 | 0 | 0 | 3 | ✅ | 1 |
| BackupScheduleResource | 1 | 0 | 1 | 0 | 0 | 2 | ✅ | 0 |
| BackupSetResource | 0 | 0 | 1 | 0 | 0 | 1 | ✅ | 0 |
| ProviderConnectionResource | 4 | 1 | 0 | 0 | 0 | 5 | ✅ | 1 |
| PolicyVersionResource | 0 | 0 | 1 | 0 | 0 | 1 | ❌ | 0 |
| RestoreRunResource | 0 | 0 | 1 | 0 | 0 | 1 | ❌ | 0 |
| InventoryItemResource | 2 | 0 | 0 | 0 | 0 | 2 | ❌ | 0 |
| ReviewPackResource | 1 | 0 | 0 | 0 | 0 | 1 | ❌ | 0 |
| AlertDeliveryResource | 3 | 0 | 0 | 0 | 0 | 3 | ❌ | 0 |
| EntraGroupResource | 2 | 0 | 0 | 0 | 0 | 2 | ❌ | 0 |
| AlertRuleResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| AlertDestinationResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| BaselineProfileResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| BaselineSnapshotResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| WorkspaceResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| InventoryCoverage | 1–2 | 0 | 0 | 0 | 0 | 1–2 | ❌ | 0 |
| BackupSetPolicyPickerTable | 3 | 0 | 0 | 1 | 0 | 5* | ❌ | 2 |
| EntraGroupCachePickerTable | 2 | 0 | 0 | 0 | 0 | 2 | ❌ | 0 |
| All 6 RelationManagers | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| All 7 System pages | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| All 2 Widgets | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |
| SettingsCatalogSettingsTable | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 |

*BackupSetPolicyPickerTable has `SelectFilter::make('synced_within')` (custom query acting as date filter) + `SelectFilter::make('has_versions')` in addition to the standard ones.

---

## Appendix B: Quick Reference — Which Files to Touch

### P0 (Persistence only — 5 files + 1 test):
- `app/Filament/Resources/InventoryItemResource.php`
- `app/Filament/Resources/PolicyVersionResource.php`
- `app/Filament/Resources/RestoreRunResource.php`
- `app/Filament/Resources/AlertDeliveryResource.php`
- `app/Filament/Resources/EntraGroupResource.php`
- `tests/Feature/Guards/FilamentTableStandardsGuardTest.php`

### P1 (New filters — 5 files):
- `app/Filament/Resources/RestoreRunResource.php`
- `app/Filament/Resources/PolicyVersionResource.php`
- `app/Filament/Resources/FindingResource.php`
- `app/Filament/Resources/AlertDeliveryResource.php`
- `app/Filament/Resources/InventoryItemResource.php`

### P2 (Presets + consistency — 4 files + 1 new):
- `app/Support/Filament/FilterPresets.php` (new)
- `app/Filament/Resources/FindingResource.php` (enum migration)
- `app/Filament/Resources/AlertDeliveryResource.php` (enum migration)
- `app/Filament/Resources/OperationRunResource.php` (labels)
- `app/Filament/Resources/BaselineProfileResource.php` (status filter)
