Compare commits
No commits in common. "a315b415d811fe4e9a2d0865df2c55feebf5730b" and "4c92ec1e64fd2f585509c8cb23da0a85929b4183" have entirely different histories.
a315b415d8
...
4c92ec1e64
5
.github/agents/copilot-instructions.md
vendored
5
.github/agents/copilot-instructions.md
vendored
@ -167,8 +167,6 @@ ## Active Technologies
|
||||
- PostgreSQL via Laravel Eloquent with one new table `tenant_triage_reviews` and no new external caches or background stores (189-portfolio-triage-review-state)
|
||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareService`, `BaselineSnapshotTruthResolver`, `BaselineCompareStats`, `RelatedNavigationResolver`, `CanonicalNavigationContext`, `BadgeCatalog`, and `UiEnforcement` patterns (190-baseline-compare-matrix)
|
||||
- PostgreSQL via existing `baseline_profiles`, `baseline_snapshots`, `baseline_snapshot_items`, `baseline_tenant_assignments`, `operation_runs`, and `findings` tables; no new persistence planned (190-baseline-compare-matrix)
|
||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns (191-baseline-compare-operator-mode)
|
||||
- PostgreSQL via existing baseline, assignment, compare-run, and finding tables; no new persistence planned (191-baseline-compare-operator-mode)
|
||||
|
||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||
|
||||
@ -203,7 +201,8 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 191-baseline-compare-operator-mode: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
|
||||
- 190-baseline-compare-matrix: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareService`, `BaselineSnapshotTruthResolver`, `BaselineCompareStats`, `RelatedNavigationResolver`, `CanonicalNavigationContext`, `BadgeCatalog`, and `UiEnforcement` patterns
|
||||
- 189-portfolio-triage-review-state: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `WorkspaceOverviewBuilder`, `TenantResource`, `TenantDashboard`, `PortfolioArrivalContext`, `TenantBackupHealthResolver`, `RestoreSafetyResolver`, `BadgeCatalog`, `UiEnforcement`, and `AuditRecorder` patterns
|
||||
- 188-provider-connection-state-cleanup: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ProviderConnection` model, `ProviderConnectionResolver`, `ProviderConnectionStateProjector`, `ProviderConnectionMutationService`, `ProviderConnectionHealthCheckJob`, `StartVerification`, `ProviderConnectionResource`, `TenantResource`, system directory pages, `BadgeCatalog`, `BadgeRenderer`, and shared provider-state Blade entries
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
|
||||
@ -23,19 +23,12 @@
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class BaselineCompareMatrix extends Page implements HasForms
|
||||
class BaselineCompareMatrix extends Page
|
||||
{
|
||||
use InteractsWithForms;
|
||||
use InteractsWithRecord;
|
||||
|
||||
protected static bool $isDiscovered = false;
|
||||
@ -90,82 +83,6 @@ public function mount(int|string $record): void
|
||||
$this->record = $this->resolveRecord($record);
|
||||
$this->hydrateFiltersFromRequest();
|
||||
$this->refreshMatrix();
|
||||
$this->form->fill($this->filterFormState());
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Grid::make([
|
||||
'default' => 1,
|
||||
'xl' => 2,
|
||||
])
|
||||
->schema([
|
||||
Grid::make([
|
||||
'default' => 1,
|
||||
'lg' => 5,
|
||||
])
|
||||
->schema([
|
||||
CheckboxList::make('selectedPolicyTypes')
|
||||
->label('Policy types')
|
||||
->options(fn (): array => $this->matrixOptions('policyTypeOptions'))
|
||||
->helperText(fn (): ?string => $this->matrixOptions('policyTypeOptions') === []
|
||||
? 'Policy type filters appear after a usable reference snapshot is available.'
|
||||
: null)
|
||||
->extraFieldWrapperAttributes([
|
||||
'data-testid' => 'matrix-policy-type-filter',
|
||||
])
|
||||
->columns(1)
|
||||
->columnSpan([
|
||||
'lg' => 2,
|
||||
])
|
||||
->live(),
|
||||
CheckboxList::make('selectedStates')
|
||||
->label('Technical states')
|
||||
->options(fn (): array => $this->matrixOptions('stateOptions'))
|
||||
->columnSpan([
|
||||
'lg' => 2,
|
||||
])
|
||||
->columns(1)
|
||||
->live(),
|
||||
CheckboxList::make('selectedSeverities')
|
||||
->label('Severity')
|
||||
->options(fn (): array => $this->matrixOptions('severityOptions'))
|
||||
->columns(1)
|
||||
->live(),
|
||||
])
|
||||
->columnSpan([
|
||||
'xl' => 1,
|
||||
]),
|
||||
Grid::make([
|
||||
'default' => 1,
|
||||
'md' => 2,
|
||||
'xl' => 1,
|
||||
])
|
||||
->schema([
|
||||
Select::make('tenantSort')
|
||||
->label('Tenant sort')
|
||||
->options(fn (): array => $this->matrixOptions('tenantSortOptions'))
|
||||
->default('tenant_name')
|
||||
->native(false)
|
||||
->live()
|
||||
->extraFieldWrapperAttributes(['data-testid' => 'matrix-tenant-sort'])
|
||||
->extraInputAttributes(['data-testid' => 'matrix-tenant-sort']),
|
||||
Select::make('subjectSort')
|
||||
->label('Subject sort')
|
||||
->options(fn (): array => $this->matrixOptions('subjectSortOptions'))
|
||||
->default('deviation_breadth')
|
||||
->native(false)
|
||||
->live()
|
||||
->extraFieldWrapperAttributes(['data-testid' => 'matrix-subject-sort'])
|
||||
->extraInputAttributes(['data-testid' => 'matrix-subject-sort']),
|
||||
])
|
||||
->columnSpan([
|
||||
'xl' => 1,
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
@ -251,11 +168,6 @@ public function refreshMatrix(): void
|
||||
]);
|
||||
}
|
||||
|
||||
public function pollMatrix(): void
|
||||
{
|
||||
$this->refreshMatrix();
|
||||
}
|
||||
|
||||
public function tenantCompareUrl(int $tenantId, ?string $subjectKey = null): ?string
|
||||
{
|
||||
$tenant = $this->tenant($tenantId);
|
||||
@ -342,40 +254,6 @@ public function updatedFocusedSubjectKey(): void
|
||||
$this->refreshMatrix();
|
||||
}
|
||||
|
||||
public function activeFilterCount(): int
|
||||
{
|
||||
return count($this->selectedPolicyTypes)
|
||||
+ count($this->selectedStates)
|
||||
+ count($this->selectedSeverities)
|
||||
+ ($this->focusedSubjectKey !== null ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function activeFilterSummary(): array
|
||||
{
|
||||
$summary = [];
|
||||
|
||||
if ($this->selectedPolicyTypes !== []) {
|
||||
$summary['Policy types'] = count($this->selectedPolicyTypes);
|
||||
}
|
||||
|
||||
if ($this->selectedStates !== []) {
|
||||
$summary['Technical states'] = count($this->selectedStates);
|
||||
}
|
||||
|
||||
if ($this->selectedSeverities !== []) {
|
||||
$summary['Severity'] = count($this->selectedSeverities);
|
||||
}
|
||||
|
||||
if ($this->focusedSubjectKey !== null) {
|
||||
$summary['Focused subject'] = $this->focusedSubjectKey;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@ -405,30 +283,6 @@ private function hydrateFiltersFromRequest(): void
|
||||
$this->focusedSubjectKey = is_string($subjectKey) && trim($subjectKey) !== '' ? trim($subjectKey) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function filterFormState(): array
|
||||
{
|
||||
return [
|
||||
'selectedPolicyTypes' => $this->selectedPolicyTypes,
|
||||
'selectedStates' => $this->selectedStates,
|
||||
'selectedSeverities' => $this->selectedSeverities,
|
||||
'tenantSort' => $this->tenantSort,
|
||||
'subjectSort' => $this->subjectSort,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function matrixOptions(string $key): array
|
||||
{
|
||||
$options = $this->matrix[$key] ?? null;
|
||||
|
||||
return is_array($options) ? $options : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<x-filament::page>
|
||||
@if (($hasActiveRuns ?? false) === true)
|
||||
<div wire:poll.5s="pollMatrix"></div>
|
||||
<div wire:poll.5s="refreshMatrix"></div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
@ -19,9 +19,6 @@
|
||||
$currentFilters = is_array($currentFilters ?? null) ? $currentFilters : [];
|
||||
$referenceReady = ($reference['referenceState'] ?? null) === 'ready';
|
||||
$matrixSourceNavigation = is_array($navigationContext ?? null) ? $navigationContext : null;
|
||||
$activeFilterCount = $this->activeFilterCount();
|
||||
$activeFilterSummary = $this->activeFilterSummary();
|
||||
$hiddenAssignedTenantCount = max(0, (int) ($reference['assignedTenantCount'] ?? 0) - (int) ($reference['visibleTenantCount'] ?? 0));
|
||||
|
||||
$stateBadge = static fn (mixed $value) => \App\Support\Badges\BadgeCatalog::spec(\App\Support\Badges\BadgeDomain::BaselineCompareMatrixState, $value);
|
||||
$freshnessBadge = static fn (mixed $value) => \App\Support\Badges\BadgeCatalog::spec(\App\Support\Badges\BadgeDomain::BaselineCompareMatrixFreshness, $value);
|
||||
@ -58,12 +55,6 @@
|
||||
Snapshot #{{ (int) $reference['referenceSnapshotId'] }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($hiddenAssignedTenantCount > 0)
|
||||
<x-filament::badge color="info" icon="heroicon-m-eye-slash" size="sm">
|
||||
{{ $hiddenAssignedTenantCount }} hidden by access scope
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
@ -84,12 +75,6 @@
|
||||
Reference reason: {{ \Illuminate\Support\Str::headline(str_replace('.', ' ', (string) $reference['referenceReasonCode'])) }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if ($hiddenAssignedTenantCount > 0)
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Showing only the visible assigned set for your current access scope. Hidden tenants are excluded from summaries, rows, and drilldowns.
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -130,434 +115,424 @@
|
||||
Narrow the matrix by policy type, technical state, severity, or one focused subject. Only the visible tenant set contributes to the rendered counts, rows, and drilldowns.
|
||||
</x-slot>
|
||||
|
||||
<div class="space-y-4" data-testid="baseline-compare-matrix-filters">
|
||||
<div class="rounded-2xl border border-gray-200 bg-gray-50/80 px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/50" data-testid="matrix-active-filters">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="space-y-1 min-w-0">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">Current matrix scope</div>
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
@if ($activeFilterCount === 0)
|
||||
No narrowing filters are active. Showing every visible subject and tenant in the current baseline scope.
|
||||
@else
|
||||
{{ $activeFilterCount }} active {{ \Illuminate\Support\Str::plural('filter', $activeFilterCount) }} are narrowing the matrix before you scan drift and follow-up links.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]" data-testid="baseline-compare-matrix-filters">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">Policy types</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
<x-filament::badge :color="$activeFilterCount === 0 ? 'gray' : 'info'" icon="heroicon-m-funnel" size="sm">
|
||||
@if ($activeFilterCount === 0)
|
||||
All visible results
|
||||
@else
|
||||
{{ $activeFilterCount }} active {{ \Illuminate\Support\Str::plural('filter', $activeFilterCount) }}
|
||||
@endif
|
||||
</x-filament::badge>
|
||||
@if ($policyTypeOptions !== [])
|
||||
<div class="space-y-2">
|
||||
@foreach ($policyTypeOptions as $value => $label)
|
||||
<label class="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $value }}"
|
||||
wire:model.live="selectedPolicyTypes"
|
||||
data-testid="matrix-filter-policy-type-{{ \Illuminate\Support\Str::slug($value, '-') }}"
|
||||
class="mt-0.5 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-900"
|
||||
/>
|
||||
<span>{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Policy type filters appear after a usable reference snapshot is available.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($hiddenAssignedTenantCount > 0)
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Visible-set only
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">Technical states</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@foreach ($stateOptions as $value => $label)
|
||||
@php
|
||||
$spec = $stateBadge($value);
|
||||
@endphp
|
||||
|
||||
<label class="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $value }}"
|
||||
wire:model.live="selectedStates"
|
||||
data-testid="matrix-filter-state-{{ \Illuminate\Support\Str::slug($value, '-') }}"
|
||||
class="mt-0.5 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-900"
|
||||
/>
|
||||
<span class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge :color="$spec->color" :icon="$spec->icon" size="sm">
|
||||
{{ $label }}
|
||||
</x-filament::badge>
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($activeFilterSummary !== [])
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
@foreach ($activeFilterSummary as $label => $value)
|
||||
<x-filament::badge color="info" size="sm">
|
||||
{{ $label }}: {{ $value }}
|
||||
</x-filament::badge>
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">Severity</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@foreach ($severityOptions as $value => $label)
|
||||
@php
|
||||
$spec = $severityBadge($value);
|
||||
@endphp
|
||||
|
||||
<label class="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $value }}"
|
||||
wire:model.live="selectedSeverities"
|
||||
data-testid="matrix-filter-severity-{{ \Illuminate\Support\Str::slug($value, '-') }}"
|
||||
class="mt-0.5 rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-900"
|
||||
/>
|
||||
<span class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge :color="$spec->color" :icon="$spec->icon" size="sm">
|
||||
{{ $label }}
|
||||
</x-filament::badge>
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form wire:submit.prevent="refreshMatrix">
|
||||
{{ $this->form }}
|
||||
</form>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-1">
|
||||
<label class="space-y-2 text-sm">
|
||||
<span class="font-semibold text-gray-950 dark:text-white">Tenant sort</span>
|
||||
<select wire:model.live="tenantSort" data-testid="matrix-tenant-sort" class="w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-white">
|
||||
@foreach ($tenantSortOptions as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="flex-1 rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-2.5 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span class="font-semibold text-gray-950 dark:text-white">Focused subject</span>
|
||||
<label class="space-y-2 text-sm">
|
||||
<span class="font-semibold text-gray-950 dark:text-white">Subject sort</span>
|
||||
<select wire:model.live="subjectSort" data-testid="matrix-subject-sort" class="w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-900 dark:text-white">
|
||||
@foreach ($subjectSortOptions as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (filled($currentFilters['subject_key'] ?? null))
|
||||
<div class="rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">Focused subject</div>
|
||||
|
||||
@if (filled($currentFilters['subject_key'] ?? null))
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge color="info" icon="heroicon-m-funnel" size="sm">
|
||||
{{ $currentFilters['subject_key'] }}
|
||||
</x-filament::badge>
|
||||
|
||||
<x-filament::button tag="a" :href="$this->clearSubjectFocusUrl()" color="gray" size="sm">
|
||||
<x-filament::link :href="$this->clearSubjectFocusUrl()" color="gray" size="sm">
|
||||
Clear subject focus
|
||||
</x-filament::button>
|
||||
@else
|
||||
<span class="text-gray-500 dark:text-gray-400">None set yet. Use Focus subject from a row when you want a subject-first drilldown.</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 lg:shrink-0">
|
||||
@if ($activeFilterCount > 0)
|
||||
<x-filament::button tag="a" :href="\App\Filament\Resources\BaselineProfileResource::compareMatrixUrl($profile)" color="gray" size="sm">
|
||||
Clear all filters
|
||||
</x-filament::button>
|
||||
</x-filament::link>
|
||||
</div>
|
||||
@else
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
No filter reset needed
|
||||
</x-filament::badge>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Focus a single row from the matrix when you want a subject-first drilldown.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-filament-actions::modals />
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<x-filament::link :href="\App\Filament\Resources\BaselineProfileResource::compareMatrixUrl($profile)" color="gray" size="sm">
|
||||
Clear all filters
|
||||
</x-filament::link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<div class="relative" data-testid="baseline-compare-matrix-results">
|
||||
@if (($hasActiveRuns ?? false) === true)
|
||||
<div class="mb-2 flex justify-end" data-testid="baseline-compare-matrix-auto-refresh-note">
|
||||
<div class="flex items-center gap-2 rounded-full border border-gray-200 bg-white/95 px-3 py-1.5 text-xs shadow-sm dark:border-gray-800 dark:bg-gray-900/95">
|
||||
<x-filament::badge color="info" icon="heroicon-m-arrow-path" size="sm">
|
||||
Auto-refresh every 5 seconds while compare runs are queued or running.
|
||||
<div class="grid gap-4 xl:grid-cols-3">
|
||||
<x-filament::section heading="State legend">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($stateLegend as $item)
|
||||
<x-filament::badge :color="$item['color']" :icon="$item['icon']" size="sm">
|
||||
{{ $item['label'] }}
|
||||
</x-filament::badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Freshness legend">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($freshnessLegend as $item)
|
||||
<x-filament::badge :color="$item['color']" :icon="$item['icon']" size="sm">
|
||||
{{ $item['label'] }}
|
||||
</x-filament::badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Trust legend">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($trustLegend as $item)
|
||||
<x-filament::badge :color="$item['color']" :icon="$item['icon']" size="sm">
|
||||
{{ $item['label'] }}
|
||||
</x-filament::badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
|
||||
@if ($emptyState !== null)
|
||||
<x-filament::section>
|
||||
<div class="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-6 py-8 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div class="space-y-2" data-testid="baseline-compare-matrix-empty-state">
|
||||
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">{{ $emptyState['title'] ?? 'Nothing to show' }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">{{ $emptyState['body'] ?? 'Adjust the current inputs and try again.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
@else
|
||||
<x-filament::section heading="Tenant summaries">
|
||||
<x-slot name="description">
|
||||
Tenant-level freshness, trust, and breadth stay visible before you scan the subject-by-tenant body.
|
||||
</x-slot>
|
||||
|
||||
<div class="space-y-6">
|
||||
<x-filament::section heading="Matrix signal legend">
|
||||
<div class="grid gap-3 xl:grid-cols-3">
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50/70 px-3 py-2 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">State legend</div>
|
||||
<div class="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
||||
@foreach ($tenantSummaries as $tenantSummary)
|
||||
@php
|
||||
$freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null);
|
||||
$trustSpec = $trustBadge($tenantSummary['trustLevel'] ?? null);
|
||||
$tenantSeveritySpec = filled($tenantSummary['maxSeverity'] ?? null) ? $severityBadge($tenantSummary['maxSeverity']) : null;
|
||||
$tenantCompareUrl = $this->tenantCompareUrl((int) $tenantSummary['tenantId']);
|
||||
$tenantRunUrl = filled($tenantSummary['compareRunId'] ?? null)
|
||||
? $this->runUrl((int) $tenantSummary['compareRunId'], (int) $tenantSummary['tenantId'])
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
@foreach ($stateLegend as $item)
|
||||
<x-filament::badge :color="$item['color']" :icon="$item['icon']" size="sm">
|
||||
{{ $item['label'] }}
|
||||
</x-filament::badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50/70 px-3 py-2 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Freshness legend</div>
|
||||
|
||||
@foreach ($freshnessLegend as $item)
|
||||
<x-filament::badge :color="$item['color']" :icon="$item['icon']" size="sm">
|
||||
{{ $item['label'] }}
|
||||
</x-filament::badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50/70 px-3 py-2 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Trust legend</div>
|
||||
|
||||
@foreach ($trustLegend as $item)
|
||||
<x-filament::badge :color="$item['color']" :icon="$item['icon']" size="sm">
|
||||
{{ $item['label'] }}
|
||||
</x-filament::badge>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@if ($emptyState !== null)
|
||||
<x-filament::section>
|
||||
<div class="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-6 py-8 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div class="space-y-2" data-testid="baseline-compare-matrix-empty-state">
|
||||
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">{{ $emptyState['title'] ?? 'Nothing to show' }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">{{ $emptyState['body'] ?? 'Adjust the current inputs and try again.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@else
|
||||
<x-filament::section heading="Tenant summaries">
|
||||
<x-slot name="description">
|
||||
Tenant-level freshness, trust, and breadth stay visible before you scan the subject-by-tenant body.
|
||||
</x-slot>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
||||
@foreach ($tenantSummaries as $tenantSummary)
|
||||
@php
|
||||
$freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null);
|
||||
$trustSpec = $trustBadge($tenantSummary['trustLevel'] ?? null);
|
||||
$tenantSeveritySpec = filled($tenantSummary['maxSeverity'] ?? null) ? $severityBadge($tenantSummary['maxSeverity']) : null;
|
||||
$tenantCompareUrl = $this->tenantCompareUrl((int) $tenantSummary['tenantId']);
|
||||
$tenantRunUrl = filled($tenantSummary['compareRunId'] ?? null)
|
||||
? $this->runUrl((int) $tenantSummary['compareRunId'], (int) $tenantSummary['tenantId'])
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-gray-950 dark:text-white">{{ $tenantSummary['tenantName'] }}</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$freshnessSpec->color" :icon="$freshnessSpec->icon" size="sm">
|
||||
{{ $freshnessSpec->label }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$trustSpec->color" :icon="$trustSpec->icon" size="sm">
|
||||
{{ $trustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@if ($tenantSeveritySpec)
|
||||
<x-filament::badge :color="$tenantSeveritySpec->color" :icon="$tenantSeveritySpec->icon" size="sm">
|
||||
{{ $tenantSeveritySpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
@if (filled($tenantSummary['lastComparedAt'] ?? null))
|
||||
Compared {{ \Illuminate\Support\Carbon::parse($tenantSummary['lastComparedAt'])->diffForHumans() }}
|
||||
@else
|
||||
No completed compare yet
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Aligned</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">{{ (int) ($tenantSummary['matchedCount'] ?? 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Drift</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">{{ (int) ($tenantSummary['differingCount'] ?? 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Missing</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">{{ (int) ($tenantSummary['missingCount'] ?? 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Ambiguous / not compared</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">
|
||||
{{ (int) ($tenantSummary['ambiguousCount'] ?? 0) + (int) ($tenantSummary['notComparedCount'] ?? 0) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3 text-sm">
|
||||
@if ($tenantCompareUrl)
|
||||
<x-filament::link :href="$tenantCompareUrl" size="sm">
|
||||
Open tenant compare
|
||||
</x-filament::link>
|
||||
@endif
|
||||
|
||||
@if ($tenantRunUrl)
|
||||
<x-filament::link :href="$tenantRunUrl" color="gray" size="sm">
|
||||
Open latest run
|
||||
</x-filament::link>
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-gray-950 dark:text-white">{{ $tenantSummary['tenantName'] }}</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$freshnessSpec->color" :icon="$freshnessSpec->icon" size="sm">
|
||||
{{ $freshnessSpec->label }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$trustSpec->color" :icon="$trustSpec->icon" size="sm">
|
||||
{{ $trustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@if ($tenantSeveritySpec)
|
||||
<x-filament::badge :color="$tenantSeveritySpec->color" :icon="$tenantSeveritySpec->icon" size="sm">
|
||||
{{ $tenantSeveritySpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Subject-by-tenant matrix">
|
||||
<x-slot name="description">
|
||||
Row click is intentionally disabled. The subject column stays pinned while you scan across visible tenants.
|
||||
</x-slot>
|
||||
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
@if (filled($tenantSummary['lastComparedAt'] ?? null))
|
||||
Compared {{ \Illuminate\Support\Carbon::parse($tenantSummary['lastComparedAt'])->diffForHumans() }}
|
||||
@else
|
||||
No completed compare yet
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-2xl" data-testid="baseline-compare-matrix-grid">
|
||||
<div class="min-w-[72rem] overflow-hidden rounded-2xl border border-gray-200 dark:border-gray-800">
|
||||
<table class="min-w-full border-separate border-spacing-0">
|
||||
<thead class="bg-gray-50 dark:bg-gray-950/70">
|
||||
<tr>
|
||||
<th class="sticky left-0 z-20 w-[26rem] border-r border-gray-200 bg-gray-50 px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:border-gray-800 dark:bg-gray-950/70 dark:text-gray-400">
|
||||
Baseline subject
|
||||
</th>
|
||||
<dl class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Aligned</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">{{ (int) ($tenantSummary['matchedCount'] ?? 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Drift</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">{{ (int) ($tenantSummary['differingCount'] ?? 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Missing</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">{{ (int) ($tenantSummary['missingCount'] ?? 0) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500 dark:text-gray-400">Ambiguous / not compared</dt>
|
||||
<dd class="font-semibold text-gray-950 dark:text-white">
|
||||
{{ (int) ($tenantSummary['ambiguousCount'] ?? 0) + (int) ($tenantSummary['notComparedCount'] ?? 0) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@foreach ($tenantSummaries as $tenantSummary)
|
||||
@php
|
||||
$freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null);
|
||||
@endphp
|
||||
<th class="min-w-72 px-4 py-3 text-left align-top text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold normal-case text-gray-950 dark:text-white">{{ $tenantSummary['tenantName'] }}</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$freshnessSpec->color" :icon="$freshnessSpec->icon" size="sm">
|
||||
{{ $freshnessSpec->label }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3 text-sm">
|
||||
@if ($tenantCompareUrl)
|
||||
<x-filament::link :href="$tenantCompareUrl" size="sm">
|
||||
Open tenant compare
|
||||
</x-filament::link>
|
||||
@endif
|
||||
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($rows as $row)
|
||||
@php
|
||||
$subject = is_array($row['subject'] ?? null) ? $row['subject'] : [];
|
||||
$cells = is_array($row['cells'] ?? null) ? $row['cells'] : [];
|
||||
$subjectSeveritySpec = filled($subject['maxSeverity'] ?? null) ? $severityBadge($subject['maxSeverity']) : null;
|
||||
$subjectTrustSpec = $trustBadge($subject['trustLevel'] ?? null);
|
||||
$rowKey = (string) ($subject['subjectKey'] ?? 'subject-'.$loop->index);
|
||||
$rowSurfaceClasses = $loop->even
|
||||
? 'bg-gray-50/70 dark:bg-gray-950/20'
|
||||
: 'bg-white dark:bg-gray-900/60';
|
||||
@endphp
|
||||
|
||||
<tr wire:key="baseline-compare-matrix-row-{{ $rowKey }}" data-testid="baseline-compare-matrix-row" class="group transition-colors hover:bg-primary-50/30 dark:hover:bg-primary-950/10 {{ $rowSurfaceClasses }}">
|
||||
<td class="sticky left-0 z-10 border-r border-gray-200 px-4 py-4 align-top dark:border-gray-800 {{ $rowSurfaceClasses }}">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $subject['displayName'] ?? $subject['subjectKey'] ?? 'Subject' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $subject['policyType'] ?? 'Unknown policy type' }}
|
||||
</div>
|
||||
@if (filled($subject['baselineExternalId'] ?? null))
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Reference ID: {{ $subject['baselineExternalId'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Drift breadth {{ (int) ($subject['deviationBreadth'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Missing {{ (int) ($subject['missingBreadth'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Ambiguous {{ (int) ($subject['ambiguousBreadth'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$subjectTrustSpec->color" :icon="$subjectTrustSpec->icon" size="sm">
|
||||
{{ $subjectTrustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@if ($subjectSeveritySpec)
|
||||
<x-filament::badge :color="$subjectSeveritySpec->color" :icon="$subjectSeveritySpec->icon" size="sm">
|
||||
{{ $subjectSeveritySpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (filled($subject['subjectKey'] ?? null))
|
||||
<div class="flex flex-wrap gap-3 text-sm">
|
||||
<x-filament::link :href="$this->filterUrl(['subject_key' => $subject['subjectKey']])" color="gray" size="sm" data-testid="matrix-focus-subject">
|
||||
Focus subject
|
||||
</x-filament::link>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@foreach ($cells as $cell)
|
||||
@php
|
||||
$cellStateSpec = $stateBadge($cell['state'] ?? null);
|
||||
$cellTrustSpec = $trustBadge($cell['trustLevel'] ?? null);
|
||||
$cellSeveritySpec = filled($cell['severity'] ?? null) ? $severityBadge($cell['severity']) : null;
|
||||
$cellState = (string) ($cell['state'] ?? '');
|
||||
$cellSeverity = is_string($cell['severity'] ?? null) ? (string) $cell['severity'] : null;
|
||||
$cellNeedsAttention = in_array($cellState, ['differ', 'missing', 'ambiguous'], true)
|
||||
|| in_array($cellSeverity, ['critical', 'high'], true);
|
||||
$cellNeedsRefresh = in_array($cellState, ['stale_result', 'not_compared'], true);
|
||||
$cellLooksHealthy = $cellState === 'match' && $cellNeedsAttention === false && $cellNeedsRefresh === false;
|
||||
$cellSurfaceClasses = $cellNeedsAttention
|
||||
? 'border-danger-300 bg-danger-50/80 ring-1 ring-danger-200/70 dark:border-danger-900/70 dark:bg-danger-950/15 dark:ring-danger-900/40'
|
||||
: ($cellNeedsRefresh
|
||||
? 'border-warning-300 bg-warning-50/80 ring-1 ring-warning-200/70 dark:border-warning-900/70 dark:bg-warning-950/15 dark:ring-warning-900/40'
|
||||
: ($cellLooksHealthy
|
||||
? 'border-success-200 bg-success-50/70 dark:border-success-900/60 dark:bg-success-950/10'
|
||||
: 'border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950/40'));
|
||||
$cellPriorityLabel = $cellNeedsAttention
|
||||
? 'Needs attention'
|
||||
: ($cellNeedsRefresh ? 'Refresh recommended' : ($cellLooksHealthy ? 'Aligned' : 'Review'));
|
||||
$cellPriorityClasses = $cellNeedsAttention
|
||||
? 'bg-danger-100 text-danger-700 dark:bg-danger-950/40 dark:text-danger-300'
|
||||
: ($cellNeedsRefresh
|
||||
? 'bg-warning-100 text-warning-700 dark:bg-warning-950/40 dark:text-warning-300'
|
||||
: 'bg-success-100 text-success-700 dark:bg-success-950/40 dark:text-success-300');
|
||||
$subjectKey = $subject['subjectKey'] ?? ($cell['subjectKey'] ?? null);
|
||||
$tenantId = (int) ($cell['tenantId'] ?? 0);
|
||||
$tenantCompareUrl = $tenantId > 0 ? $this->tenantCompareUrl($tenantId, $subjectKey) : null;
|
||||
$cellFindingUrl = ($tenantId > 0 && filled($cell['findingId'] ?? null))
|
||||
? $this->findingUrl($tenantId, (int) $cell['findingId'], $subjectKey)
|
||||
: null;
|
||||
$cellRunUrl = filled($cell['compareRunId'] ?? null)
|
||||
? $this->runUrl((int) $cell['compareRunId'], $tenantId, $subjectKey)
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
<td wire:key="baseline-compare-matrix-cell-{{ $rowKey }}-{{ $tenantId > 0 ? $tenantId : $loop->index }}" class="px-4 py-4 align-top">
|
||||
<div class="space-y-3 rounded-xl border p-3 transition-colors group-hover:border-primary-200 group-hover:bg-white dark:group-hover:border-primary-900 dark:group-hover:bg-gray-950/60 {{ $cellSurfaceClasses }}">
|
||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$cellStateSpec->color" :icon="$cellStateSpec->icon" size="sm">
|
||||
{{ $cellStateSpec->label }}
|
||||
</x-filament::badge>
|
||||
@if ($cellSeveritySpec)
|
||||
<x-filament::badge :color="$cellSeveritySpec->color" :icon="$cellSeveritySpec->icon" size="sm">
|
||||
{{ $cellSeveritySpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<span class="inline-flex items-center rounded-full px-2 py-1 text-[11px] font-semibold uppercase tracking-wide {{ $cellPriorityClasses }}">
|
||||
{{ $cellPriorityLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$cellTrustSpec->color" :icon="$cellTrustSpec->icon" size="sm">
|
||||
{{ $cellTrustSpec->label }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
@if (filled($cell['reasonCode'] ?? null))
|
||||
<div>
|
||||
Reason: {{ \Illuminate\Support\Str::headline(str_replace('_', ' ', (string) $cell['reasonCode'])) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (filled($cell['lastComparedAt'] ?? null))
|
||||
<div>
|
||||
Compared {{ \Illuminate\Support\Carbon::parse($cell['lastComparedAt'])->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (($cell['policyTypeCovered'] ?? true) === false)
|
||||
<div>Policy type coverage was not proven in the latest compare run.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-sm">
|
||||
@if ($cellFindingUrl)
|
||||
<x-filament::link :href="$cellFindingUrl" size="sm">
|
||||
Open finding
|
||||
</x-filament::link>
|
||||
@elseif ($tenantCompareUrl)
|
||||
<x-filament::link :href="$tenantCompareUrl" size="sm">
|
||||
Open tenant compare
|
||||
</x-filament::link>
|
||||
@endif
|
||||
|
||||
@if ($cellRunUrl)
|
||||
<x-filament::link :href="$cellRunUrl" color="gray" size="sm">
|
||||
Open run
|
||||
</x-filament::link>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@if ($tenantRunUrl)
|
||||
<x-filament::link :href="$tenantRunUrl" color="gray" size="sm">
|
||||
Open latest run
|
||||
</x-filament::link>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Subject-by-tenant matrix">
|
||||
<x-slot name="description">
|
||||
Row click is intentionally disabled. Use the explicit subject, tenant, finding, and run links inside the matrix cells.
|
||||
</x-slot>
|
||||
|
||||
<div class="overflow-x-auto" data-testid="baseline-compare-matrix-grid">
|
||||
<div class="min-w-[72rem] overflow-hidden rounded-2xl border border-gray-200 dark:border-gray-800">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<thead class="bg-gray-50 dark:bg-gray-950/70">
|
||||
<tr>
|
||||
<th class="w-80 px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Baseline subject
|
||||
</th>
|
||||
|
||||
@foreach ($tenantSummaries as $tenantSummary)
|
||||
@php
|
||||
$freshnessSpec = $freshnessBadge($tenantSummary['freshnessState'] ?? null);
|
||||
@endphp
|
||||
<th class="min-w-64 px-4 py-3 text-left align-top text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold normal-case text-gray-950 dark:text-white">{{ $tenantSummary['tenantName'] }}</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$freshnessSpec->color" :icon="$freshnessSpec->icon" size="sm">
|
||||
{{ $freshnessSpec->label }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-800 dark:bg-gray-900/60">
|
||||
@foreach ($rows as $row)
|
||||
@php
|
||||
$subject = is_array($row['subject'] ?? null) ? $row['subject'] : [];
|
||||
$cells = is_array($row['cells'] ?? null) ? $row['cells'] : [];
|
||||
$subjectSeveritySpec = filled($subject['maxSeverity'] ?? null) ? $severityBadge($subject['maxSeverity']) : null;
|
||||
$subjectTrustSpec = $trustBadge($subject['trustLevel'] ?? null);
|
||||
@endphp
|
||||
|
||||
<tr data-testid="baseline-compare-matrix-row">
|
||||
<td class="px-4 py-4 align-top">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $subject['displayName'] ?? $subject['subjectKey'] ?? 'Subject' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $subject['policyType'] ?? 'Unknown policy type' }}
|
||||
</div>
|
||||
@if (filled($subject['baselineExternalId'] ?? null))
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Reference ID: {{ $subject['baselineExternalId'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Drift breadth {{ (int) ($subject['deviationBreadth'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Missing {{ (int) ($subject['missingBreadth'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
Ambiguous {{ (int) ($subject['ambiguousBreadth'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$subjectTrustSpec->color" :icon="$subjectTrustSpec->icon" size="sm">
|
||||
{{ $subjectTrustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@if ($subjectSeveritySpec)
|
||||
<x-filament::badge :color="$subjectSeveritySpec->color" :icon="$subjectSeveritySpec->icon" size="sm">
|
||||
{{ $subjectSeveritySpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (filled($subject['subjectKey'] ?? null))
|
||||
<div class="flex flex-wrap gap-3 text-sm">
|
||||
<x-filament::link :href="$this->filterUrl(['subject_key' => $subject['subjectKey']])" color="gray" size="sm" data-testid="matrix-focus-subject">
|
||||
Focus subject
|
||||
</x-filament::link>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@foreach ($cells as $cell)
|
||||
@php
|
||||
$cellStateSpec = $stateBadge($cell['state'] ?? null);
|
||||
$cellTrustSpec = $trustBadge($cell['trustLevel'] ?? null);
|
||||
$cellSeveritySpec = filled($cell['severity'] ?? null) ? $severityBadge($cell['severity']) : null;
|
||||
$subjectKey = $subject['subjectKey'] ?? ($cell['subjectKey'] ?? null);
|
||||
$tenantId = (int) ($cell['tenantId'] ?? 0);
|
||||
$tenantCompareUrl = $tenantId > 0 ? $this->tenantCompareUrl($tenantId, $subjectKey) : null;
|
||||
$cellFindingUrl = ($tenantId > 0 && filled($cell['findingId'] ?? null))
|
||||
? $this->findingUrl($tenantId, (int) $cell['findingId'], $subjectKey)
|
||||
: null;
|
||||
$cellRunUrl = filled($cell['compareRunId'] ?? null)
|
||||
? $this->runUrl((int) $cell['compareRunId'], $tenantId, $subjectKey)
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
<td class="px-4 py-4 align-top">
|
||||
<div class="space-y-3 rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$cellStateSpec->color" :icon="$cellStateSpec->icon" size="sm">
|
||||
{{ $cellStateSpec->label }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$cellTrustSpec->color" :icon="$cellTrustSpec->icon" size="sm">
|
||||
{{ $cellTrustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@if ($cellSeveritySpec)
|
||||
<x-filament::badge :color="$cellSeveritySpec->color" :icon="$cellSeveritySpec->icon" size="sm">
|
||||
{{ $cellSeveritySpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
@if (filled($cell['reasonCode'] ?? null))
|
||||
<div>
|
||||
Reason: {{ \Illuminate\Support\Str::headline(str_replace('_', ' ', (string) $cell['reasonCode'])) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (filled($cell['lastComparedAt'] ?? null))
|
||||
<div>
|
||||
Compared {{ \Illuminate\Support\Carbon::parse($cell['lastComparedAt'])->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (($cell['policyTypeCovered'] ?? true) === false)
|
||||
<div>Policy type coverage was not proven in the latest compare run.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-sm">
|
||||
@if ($cellFindingUrl)
|
||||
<x-filament::link :href="$cellFindingUrl" size="sm">
|
||||
Open finding
|
||||
</x-filament::link>
|
||||
@elseif ($tenantCompareUrl)
|
||||
<x-filament::link :href="$tenantCompareUrl" size="sm">
|
||||
Open tenant compare
|
||||
</x-filament::link>
|
||||
@endif
|
||||
|
||||
@if ($cellRunUrl)
|
||||
<x-filament::link :href="$cellRunUrl" color="gray" size="sm">
|
||||
Open run
|
||||
</x-filament::link>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
</x-filament::page>
|
||||
|
||||
@ -48,34 +48,21 @@
|
||||
->assertNoJavaScriptErrors()
|
||||
->waitForText('Visible-set baseline')
|
||||
->assertSee('Reference overview')
|
||||
->assertSee('No narrowing filters are active')
|
||||
->assertSee('Subject-by-tenant matrix')
|
||||
->assertSee('WiFi Corp Profile')
|
||||
->assertSee('Windows Compliance')
|
||||
->assertSee('Open finding');
|
||||
|
||||
$page->script(<<<'JS'
|
||||
const input = Array.from(document.querySelectorAll('input[type="checkbox"]')).find((element) => {
|
||||
if (element.getAttribute('aria-label') === 'Drift detected') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const label = element.closest('label');
|
||||
|
||||
return label instanceof HTMLLabelElement && label.innerText.includes('Drift detected');
|
||||
});
|
||||
|
||||
if (! (input instanceof HTMLInputElement)) {
|
||||
throw new Error('Drift detected checkbox not found.');
|
||||
const input = document.querySelector('[data-testid="matrix-filter-state-differ"]');
|
||||
if (input instanceof HTMLInputElement) {
|
||||
input.click();
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
input.click();
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
JS);
|
||||
|
||||
$page
|
||||
->wait(1)
|
||||
->waitForText('Open finding')
|
||||
->assertDontSee('Windows Compliance')
|
||||
->click('Open finding')
|
||||
|
||||
@ -39,21 +39,14 @@
|
||||
->assertOk()
|
||||
->assertSee('Visible-set baseline')
|
||||
->assertSee('Reference overview')
|
||||
->assertSee('fi-fo-checkbox-list', false)
|
||||
->assertSee('fi-fo-select', false)
|
||||
->assertSee('State legend')
|
||||
->assertSee('Tenant summaries')
|
||||
->assertSee('Subject-by-tenant matrix')
|
||||
->assertSee('No narrowing filters are active. Showing every visible subject and tenant in the current baseline scope.')
|
||||
->assertSee('1 hidden by access scope')
|
||||
->assertSee('WiFi Corp Profile')
|
||||
->assertSee((string) $fixture['visibleTenant']->name)
|
||||
->assertSee((string) $fixture['visibleTenantTwo']->name)
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Open finding')
|
||||
->assertSee('Open tenant compare')
|
||||
->assertSee('data-testid="matrix-active-filters"', false)
|
||||
->assertSee('sticky left-0', false);
|
||||
->assertSee('Open tenant compare');
|
||||
});
|
||||
|
||||
it('keeps query-backed filters and subject focus on the matrix route and drilldown links', function (): void {
|
||||
@ -89,9 +82,6 @@
|
||||
])
|
||||
->actingAs($fixture['user'])
|
||||
->test(BaselineCompareMatrix::class, ['record' => $fixture['profile']->getKey()])
|
||||
->assertSee('4 active filters')
|
||||
->assertSee('Policy types: 1')
|
||||
->assertSee('Focused subject: wifi-corp-profile')
|
||||
->assertSee('Clear subject focus')
|
||||
->assertDontSee('Windows Compliance');
|
||||
|
||||
@ -149,31 +139,6 @@
|
||||
->assertSee('No visible assigned tenants');
|
||||
});
|
||||
|
||||
it('renders a passive auto-refresh note instead of a perpetual loading state while compare runs remain active', function (): void {
|
||||
$fixture = $this->makeBaselineCompareMatrixFixture();
|
||||
|
||||
$this->makeBaselineCompareMatrixRun(
|
||||
$fixture['visibleTenant'],
|
||||
$fixture['profile'],
|
||||
$fixture['snapshot'],
|
||||
attributes: [
|
||||
'status' => \App\Support\OperationRunStatus::Queued->value,
|
||||
'outcome' => \App\Support\OperationRunOutcome::Pending->value,
|
||||
'completed_at' => null,
|
||||
'started_at' => now(),
|
||||
],
|
||||
);
|
||||
|
||||
$session = $this->setAdminWorkspaceContext($fixture['user'], $fixture['workspace']);
|
||||
|
||||
$this->withSession($session)
|
||||
->get(BaselineProfileResource::compareMatrixUrl($fixture['profile']))
|
||||
->assertOk()
|
||||
->assertSee('Auto-refresh every 5 seconds while compare runs are queued or running.')
|
||||
->assertSee('wire:poll.5s="pollMatrix"', false)
|
||||
->assertDontSee('Refreshing matrix');
|
||||
});
|
||||
|
||||
it('renders an empty state when no rows match the current filters', function (): void {
|
||||
$fixture = $this->makeBaselineCompareMatrixFixture();
|
||||
|
||||
|
||||
@ -139,7 +139,6 @@ ## Phase 7: Polish & Cross-Cutting Concerns
|
||||
- [X] T036 [P] Add no-ad-hoc-badge and no-diagnostic-warning guard coverage for matrix state surfaces in `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` and `apps/platform/tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php`
|
||||
- [X] T037 [P] Add browser smoke coverage for matrix render, one filter interaction, and one drilldown or compare affordance in `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`
|
||||
- [X] T038 [P] Review `Verb + Object`, `visible-set only`, and `simulation only` copy in `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php`, `apps/platform/app/Filament/Resources/BaselineProfileResource/Pages/ViewBaselineProfile.php`, and `apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php`
|
||||
- [X] T041 [P] Tighten matrix scanability with active-filter scope summaries, visible-set scope disclosure, non-blocking refresh feedback, sticky subject-column treatment, and focused UI regression coverage in `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php`, `apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php`, `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`, and `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`
|
||||
- [X] T039 [P] Run formatting with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` using `specs/190-baseline-compare-matrix/quickstart.md`
|
||||
- [ ] T040 Run the focused verification pack from `specs/190-baseline-compare-matrix/quickstart.md` against `apps/platform/tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareFindingsTest.php`, `apps/platform/tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php`, `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, `apps/platform/tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php`, `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php`, `apps/platform/tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php`, and `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`
|
||||
|
||||
|
||||
@ -1,501 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Baseline Compare Matrix Operator Mode Internal Surface Contract
|
||||
version: 0.1.0
|
||||
summary: Internal logical contract for adaptive operator-density rendering on the existing baseline compare matrix route
|
||||
description: |
|
||||
This contract is an internal planning artifact for Spec 191. The affected surface
|
||||
still renders HTML through Filament and Livewire. The schemas below define the
|
||||
bounded request-scoped presentation models and staged filter interactions that must
|
||||
be derivable from existing Spec 190 matrix truth before the operator-density
|
||||
refactor can render safely.
|
||||
servers:
|
||||
- url: /internal
|
||||
x-baseline-compare-operator-mode-consumers:
|
||||
- surface: baseline.compare.matrix
|
||||
sourceFiles:
|
||||
- apps/platform/app/Filament/Pages/BaselineCompareMatrix.php
|
||||
- apps/platform/resources/views/filament/pages/baseline-compare-matrix.blade.php
|
||||
mustRender:
|
||||
- reference
|
||||
- presentation_state
|
||||
- support_surface_state
|
||||
- applied_filters
|
||||
- tenant_summaries
|
||||
- dense_rows_or_compact_results
|
||||
- last_updated_at
|
||||
- auto_refresh_state
|
||||
mustAccept:
|
||||
- mode
|
||||
- policy_type
|
||||
- state
|
||||
- severity
|
||||
- tenant_sort
|
||||
- subject_sort
|
||||
- subject_key
|
||||
mustStage:
|
||||
- selectedPolicyTypes
|
||||
- selectedStates
|
||||
- selectedSeverities
|
||||
paths:
|
||||
/admin/baseline-profiles/{profile}/compare-matrix:
|
||||
get:
|
||||
summary: Render the existing baseline compare matrix using adaptive operator-density presentation
|
||||
operationId: viewBaselineCompareOperatorMode
|
||||
parameters:
|
||||
- name: profile
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: mode
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/PresentationMode'
|
||||
- name: policy_type
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: state
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MatrixCellState'
|
||||
- name: severity
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FindingSeverity'
|
||||
- name: tenant_sort
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: subject_sort
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: subject_key
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Rendered matrix plus adaptive operator-density read models
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
application/vnd.tenantpilot.baseline-compare-operator-mode+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BaselineCompareOperatorModeBundle'
|
||||
'403':
|
||||
description: Actor is in scope but lacks workspace baseline view capability
|
||||
'404':
|
||||
description: Workspace or baseline profile is outside actor scope
|
||||
/internal/workspaces/{workspace}/baseline-profiles/{profile}/compare-matrix/apply-filters:
|
||||
post:
|
||||
summary: Apply staged heavy filters to the operator-density matrix route
|
||||
operationId: applyBaselineCompareOperatorFilters
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: profile
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MatrixFilterDraft'
|
||||
responses:
|
||||
'200':
|
||||
description: Updated operator-density bundle using the applied filter state
|
||||
content:
|
||||
application/vnd.tenantpilot.baseline-compare-operator-mode+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BaselineCompareOperatorModeBundle'
|
||||
'403':
|
||||
description: Actor is in scope but lacks workspace baseline view capability
|
||||
'404':
|
||||
description: Workspace or baseline profile is outside actor scope
|
||||
/internal/workspaces/{workspace}/baseline-profiles/{profile}/compare-matrix/reset-filters:
|
||||
post:
|
||||
summary: Reset staged and applied heavy filters for the operator-density matrix route
|
||||
operationId: resetBaselineCompareOperatorFilters
|
||||
parameters:
|
||||
- name: workspace
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: profile
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Updated operator-density bundle with default filter state restored
|
||||
content:
|
||||
application/vnd.tenantpilot.baseline-compare-operator-mode+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BaselineCompareOperatorModeBundle'
|
||||
'403':
|
||||
description: Actor is in scope but lacks workspace baseline view capability
|
||||
'404':
|
||||
description: Workspace or baseline profile is outside actor scope
|
||||
components:
|
||||
schemas:
|
||||
PresentationMode:
|
||||
type: string
|
||||
enum:
|
||||
- auto
|
||||
- dense
|
||||
- compact
|
||||
MatrixCellState:
|
||||
type: string
|
||||
enum:
|
||||
- match
|
||||
- differ
|
||||
- missing
|
||||
- ambiguous
|
||||
- not_compared
|
||||
- stale_result
|
||||
FindingSeverity:
|
||||
type: string
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
- critical
|
||||
FreshnessState:
|
||||
type: string
|
||||
enum:
|
||||
- fresh
|
||||
- stale
|
||||
- never_compared
|
||||
- unknown
|
||||
TrustLevel:
|
||||
type: string
|
||||
enum:
|
||||
- trustworthy
|
||||
- limited_confidence
|
||||
- diagnostic_only
|
||||
- unusable
|
||||
AttentionLevel:
|
||||
type: string
|
||||
enum:
|
||||
- aligned
|
||||
- review
|
||||
- refresh_recommended
|
||||
- needs_attention
|
||||
MatrixReference:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- baselineProfileId
|
||||
- baselineProfileName
|
||||
- referenceState
|
||||
- assignedTenantCount
|
||||
- visibleTenantCount
|
||||
properties:
|
||||
baselineProfileId:
|
||||
type: integer
|
||||
baselineProfileName:
|
||||
type: string
|
||||
referenceSnapshotId:
|
||||
type:
|
||||
- integer
|
||||
- 'null'
|
||||
referenceState:
|
||||
type: string
|
||||
assignedTenantCount:
|
||||
type: integer
|
||||
visibleTenantCount:
|
||||
type: integer
|
||||
MatrixFilterDraft:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- selectedPolicyTypes
|
||||
- selectedStates
|
||||
- selectedSeverities
|
||||
- tenantSort
|
||||
- subjectSort
|
||||
properties:
|
||||
selectedPolicyTypes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
selectedStates:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MatrixCellState'
|
||||
selectedSeverities:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FindingSeverity'
|
||||
tenantSort:
|
||||
type: string
|
||||
subjectSort:
|
||||
type: string
|
||||
focusedSubjectKey:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
MatrixPresentationState:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- requestedMode
|
||||
- resolvedMode
|
||||
- visibleTenantCount
|
||||
- activeFilterCount
|
||||
- hasStagedFilterChanges
|
||||
- autoRefreshActive
|
||||
- canOverrideMode
|
||||
properties:
|
||||
requestedMode:
|
||||
$ref: '#/components/schemas/PresentationMode'
|
||||
resolvedMode:
|
||||
type: string
|
||||
enum:
|
||||
- dense
|
||||
- compact
|
||||
visibleTenantCount:
|
||||
type: integer
|
||||
activeFilterCount:
|
||||
type: integer
|
||||
hasStagedFilterChanges:
|
||||
type: boolean
|
||||
autoRefreshActive:
|
||||
type: boolean
|
||||
lastUpdatedAt:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
format: date-time
|
||||
canOverrideMode:
|
||||
type: boolean
|
||||
MatrixTenantSummary:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- tenantId
|
||||
- tenantName
|
||||
- freshnessState
|
||||
- differingCount
|
||||
- missingCount
|
||||
- ambiguousCount
|
||||
- trustLevel
|
||||
properties:
|
||||
tenantId:
|
||||
type: integer
|
||||
tenantName:
|
||||
type: string
|
||||
freshnessState:
|
||||
$ref: '#/components/schemas/FreshnessState'
|
||||
lastComparedAt:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
format: date-time
|
||||
differingCount:
|
||||
type: integer
|
||||
missingCount:
|
||||
type: integer
|
||||
ambiguousCount:
|
||||
type: integer
|
||||
trustLevel:
|
||||
$ref: '#/components/schemas/TrustLevel'
|
||||
maxSeverity:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
DenseCellView:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- tenantId
|
||||
- subjectKey
|
||||
- state
|
||||
- freshnessState
|
||||
- trustLevel
|
||||
- attentionLevel
|
||||
properties:
|
||||
tenantId:
|
||||
type: integer
|
||||
subjectKey:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/MatrixCellState'
|
||||
freshnessState:
|
||||
$ref: '#/components/schemas/FreshnessState'
|
||||
trustLevel:
|
||||
$ref: '#/components/schemas/TrustLevel'
|
||||
severity:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
attentionLevel:
|
||||
$ref: '#/components/schemas/AttentionLevel'
|
||||
reasonSummary:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
primaryDrilldownUrl:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
secondaryDrilldownUrls:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
DenseSubjectRowView:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- subjectKey
|
||||
- displayName
|
||||
- policyType
|
||||
- deviationBreadth
|
||||
- missingBreadth
|
||||
- ambiguousBreadth
|
||||
- trustLevel
|
||||
- cells
|
||||
properties:
|
||||
subjectKey:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
policyType:
|
||||
type: string
|
||||
baselineExternalId:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
deviationBreadth:
|
||||
type: integer
|
||||
missingBreadth:
|
||||
type: integer
|
||||
ambiguousBreadth:
|
||||
type: integer
|
||||
maxSeverity:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
trustLevel:
|
||||
$ref: '#/components/schemas/TrustLevel'
|
||||
cells:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DenseCellView'
|
||||
CompactSubjectResultView:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- tenantId
|
||||
- subjectKey
|
||||
- displayName
|
||||
- policyType
|
||||
- state
|
||||
- freshnessState
|
||||
- trustLevel
|
||||
properties:
|
||||
tenantId:
|
||||
type: integer
|
||||
subjectKey:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
policyType:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/MatrixCellState'
|
||||
freshnessState:
|
||||
$ref: '#/components/schemas/FreshnessState'
|
||||
trustLevel:
|
||||
$ref: '#/components/schemas/TrustLevel'
|
||||
severity:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
reasonSummary:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
primaryDrilldownUrl:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
runUrl:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
MatrixSupportSurfaceState:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- legendMode
|
||||
- showActiveFilterSummary
|
||||
- showLastUpdated
|
||||
- showAutoRefreshHint
|
||||
- showBlockingRefreshState
|
||||
properties:
|
||||
legendMode:
|
||||
type: string
|
||||
showActiveFilterSummary:
|
||||
type: boolean
|
||||
showLastUpdated:
|
||||
type: boolean
|
||||
showAutoRefreshHint:
|
||||
type: boolean
|
||||
showBlockingRefreshState:
|
||||
type: boolean
|
||||
BaselineCompareOperatorModeBundle:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- reference
|
||||
- presentation
|
||||
- supportSurface
|
||||
- appliedFilters
|
||||
- tenantSummaries
|
||||
properties:
|
||||
reference:
|
||||
$ref: '#/components/schemas/MatrixReference'
|
||||
presentation:
|
||||
$ref: '#/components/schemas/MatrixPresentationState'
|
||||
supportSurface:
|
||||
$ref: '#/components/schemas/MatrixSupportSurfaceState'
|
||||
appliedFilters:
|
||||
$ref: '#/components/schemas/MatrixFilterDraft'
|
||||
tenantSummaries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/MatrixTenantSummary'
|
||||
denseRows:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DenseSubjectRowView'
|
||||
compactResults:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CompactSubjectResultView'
|
||||
@ -1,166 +0,0 @@
|
||||
# Data Model: Baseline Compare Matrix: High-Density Operator Mode
|
||||
|
||||
## Overview
|
||||
|
||||
This follow-up introduces no new persisted entity. It reuses the existing Spec 190 matrix truth and adds derived presentation models for operator density, staged filtering, and non-blocking status cues.
|
||||
|
||||
## Existing Source Truths Reused Without Change
|
||||
|
||||
### Baseline compare truth from Spec 190
|
||||
|
||||
The following derived or canonical inputs remain authoritative and are not redefined by this spec:
|
||||
|
||||
- workspace-scoped baseline reference truth
|
||||
- visible tenant summaries
|
||||
- subject summaries
|
||||
- subject-by-tenant matrix cells
|
||||
- compare-start availability and existing drilldown destinations
|
||||
|
||||
This spec changes how those inputs are rendered and interacted with, not how they are computed.
|
||||
|
||||
## New Derived Presentation Models
|
||||
|
||||
### MatrixPresentationState
|
||||
|
||||
**Type**: request-scoped page presentation contract
|
||||
**Source**: route/query state + visible tenant count + existing run state
|
||||
|
||||
| Field | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `requestedMode` | string | `auto`, `dense`, or `compact` from route/query state |
|
||||
| `resolvedMode` | string | Final mode used for rendering: `dense` or `compact` |
|
||||
| `visibleTenantCount` | integer | Existing visible-set count from the matrix bundle |
|
||||
| `activeFilterCount` | integer | Count of currently applied filters |
|
||||
| `hasStagedFilterChanges` | boolean | Whether filter draft state differs from applied state |
|
||||
| `autoRefreshActive` | boolean | True when background polling is active because compare work is queued or running |
|
||||
| `lastUpdatedAt` | datetime or null | Timestamp for the currently rendered matrix data |
|
||||
| `canOverrideMode` | boolean | Whether the operator may locally switch away from `auto` |
|
||||
|
||||
### MatrixFilterDraft
|
||||
|
||||
**Type**: request-scoped staged filter model
|
||||
**Source**: page form state only
|
||||
|
||||
| Field | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `selectedPolicyTypes` | array<string> | Draft policy-type filter selection |
|
||||
| `selectedStates` | array<string> | Draft state-group selection |
|
||||
| `selectedSeverities` | array<string> | Draft severity selection |
|
||||
| `tenantSort` | string | Current tenant sort choice |
|
||||
| `subjectSort` | string | Current subject sort choice |
|
||||
| `focusedSubjectKey` | string or null | Optional current subject focus |
|
||||
|
||||
### DenseSubjectRowView
|
||||
|
||||
**Type**: request-scoped dense-mode row view
|
||||
**Source**: existing subject summary + existing matrix cells
|
||||
|
||||
| Field | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `subjectKey` | string | Stable row key |
|
||||
| `displayName` | string | Primary row label |
|
||||
| `policyType` | string | Compact secondary label |
|
||||
| `baselineExternalId` | string or null | Optional secondary context |
|
||||
| `deviationBreadth` | integer | Existing subject summary metric |
|
||||
| `missingBreadth` | integer | Existing subject summary metric |
|
||||
| `ambiguousBreadth` | integer | Existing subject summary metric |
|
||||
| `maxSeverity` | string or null | Existing subject summary severity |
|
||||
| `trustLevel` | string | Existing subject summary trust |
|
||||
| `cells` | array<DenseCellView> | One condensed cell per visible tenant |
|
||||
|
||||
### DenseCellView
|
||||
|
||||
**Type**: request-scoped dense-mode cell view
|
||||
**Source**: existing matrix cell + existing tenant summary freshness
|
||||
|
||||
| Field | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `tenantId` | integer | Visible tenant identifier |
|
||||
| `subjectKey` | string | Subject row key |
|
||||
| `state` | string | Existing Spec 190 state |
|
||||
| `freshnessState` | string | Freshness signal shown in compact form |
|
||||
| `trustLevel` | string | Trust signal shown in compact form |
|
||||
| `severity` | string or null | Optional attention signal |
|
||||
| `attentionLevel` | string | Derived presentation label such as `aligned`, `refresh_recommended`, or `needs_attention` |
|
||||
| `reasonSummary` | string or null | Short secondary explanation for compact reveal surfaces |
|
||||
| `primaryDrilldownUrl` | string or null | Preferred next follow-up action |
|
||||
| `secondaryDrilldownUrls` | array<string, string> | Additional compact follow-up links when available |
|
||||
|
||||
### CompactSubjectResultView
|
||||
|
||||
**Type**: request-scoped single-tenant row view
|
||||
**Source**: one visible tenant summary + existing matrix cell + existing subject summary
|
||||
|
||||
| Field | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `tenantId` | integer | The single visible tenant in compact mode |
|
||||
| `subjectKey` | string | Stable subject key |
|
||||
| `displayName` | string | Primary subject label |
|
||||
| `policyType` | string | Secondary grouping/context |
|
||||
| `state` | string | Existing Spec 190 state |
|
||||
| `freshnessState` | string | Compact freshness label |
|
||||
| `trustLevel` | string | Compact trust label |
|
||||
| `severity` | string or null | Optional attention indicator |
|
||||
| `reasonSummary` | string or null | Short explanation line |
|
||||
| `primaryDrilldownUrl` | string or null | Main follow-up action |
|
||||
| `runUrl` | string or null | Secondary run-level follow-up |
|
||||
|
||||
### MatrixSupportSurfaceState
|
||||
|
||||
**Type**: request-scoped supporting-context contract
|
||||
**Source**: page state + existing legends + refresh metadata
|
||||
|
||||
| Field | Type | Notes |
|
||||
|------|------|-------|
|
||||
| `legendMode` | string | `grouped`, `collapsed`, or equivalent compact support behavior |
|
||||
| `showActiveFilterSummary` | boolean | Whether applied filters are summarized inline |
|
||||
| `showLastUpdated` | boolean | Whether the page displays last-updated metadata |
|
||||
| `showAutoRefreshHint` | boolean | Whether passive auto-refresh copy is visible |
|
||||
| `showBlockingRefreshState` | boolean | Reserved for deliberate user-triggered reloads only |
|
||||
|
||||
## Rendering and Resolution Rules
|
||||
|
||||
### Mode resolution rules
|
||||
|
||||
1. If `requestedMode = auto` and `visibleTenantCount > 1`, resolve to `dense`.
|
||||
2. If `requestedMode = auto` and `visibleTenantCount = 1`, resolve to `compact`.
|
||||
3. If a manual override is present, use it unless it would produce an invalid empty layout.
|
||||
4. Manual override remains route-local and must never be persisted as product truth.
|
||||
|
||||
### Dense-mode rules
|
||||
|
||||
- The subject column remains sticky during horizontal scroll.
|
||||
- The primary visible content per cell is state, trust, freshness, and attention.
|
||||
- Long explanatory text and repeated action links do not render as the dominant cell body.
|
||||
|
||||
### Compact single-tenant rules
|
||||
|
||||
- The tenant header does not repeat as a pseudo-column structure.
|
||||
- Each subject entry shows one primary status line and a reduced set of secondary metadata.
|
||||
- Existing subject focus and drilldown continuity remain available.
|
||||
|
||||
### Filter workflow rules
|
||||
|
||||
- Heavy multi-select filters use staged state first and apply only when the operator confirms.
|
||||
- Applied filter count and scope summary reflect the applied state, not merely the draft state.
|
||||
- Reset may clear both draft and applied state in one explicit action.
|
||||
|
||||
### Status signal rules
|
||||
|
||||
- `blocking refresh` is reserved for deliberate user-triggered reload or recalculation moments.
|
||||
- `auto-refresh active` indicates passive polling while compare work is still queued or running.
|
||||
- `lastUpdatedAt` reflects the timestamp of the rendered matrix payload, not merely the latest compare run in the system.
|
||||
|
||||
### Safety rules
|
||||
|
||||
- No rendering path may widen tenant visibility beyond the existing visible set.
|
||||
- No presentation-state change may change the underlying compare state, trust, or freshness semantics.
|
||||
- No grouped legend or compact cell may invent new status vocabulary outside existing centralized badge semantics.
|
||||
|
||||
## Relationships
|
||||
|
||||
- One `MatrixPresentationState` governs one rendered matrix page.
|
||||
- One `MatrixFilterDraft` belongs to one `MatrixPresentationState`.
|
||||
- In dense mode, one `DenseSubjectRowView` maps to many `DenseCellView` entries.
|
||||
- In compact mode, one visible tenant yields many `CompactSubjectResultView` entries.
|
||||
- One `MatrixSupportSurfaceState` coordinates legends, refresh hints, and active-filter summaries for the same page render.
|
||||
@ -3,108 +3,64 @@ # Implementation Plan: Baseline Compare Matrix: High-Density Operator Mode
|
||||
**Branch**: `191-baseline-compare-operator-mode` | **Date**: 2026-04-11 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/spec.md`
|
||||
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/spec.md`
|
||||
|
||||
**Note**: This plan formalizes the existing 191 spec slice and keeps the work strictly inside the already-shipped Spec 190 matrix surface.
|
||||
|
||||
## Summary
|
||||
|
||||
Refactor the existing workspace baseline compare matrix into an adaptive operator-density surface. The route, baseline reference, visible-set-only truth, compare-start behavior, and drilldowns stay unchanged, but the page gains local presentation-mode state, dense multi-tenant scanning, compact single-tenant rendering, staged heavy-filter application, grouped legends, and clearer separation between blocking refresh, passive auto-refresh, and last-updated status.
|
||||
Rework the existing baseline compare matrix route into an operator-density follow-up to Spec 190. The route stays workspace-scoped and fully derived, but gains adaptive presentation rules: dense multi-tenant scanning when several visible tenants are present, compact single-tenant comparison when only one visible tenant remains, and calmer filter, legend, action, and refresh surfaces.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15
|
||||
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
|
||||
**Storage**: PostgreSQL via existing baseline, assignment, compare-run, and finding tables; no new persistence planned
|
||||
**Testing**: Pest feature tests and browser smoke coverage run through Laravel Sail
|
||||
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
|
||||
**Storage**: Existing PostgreSQL truth only; no new tables or artifacts
|
||||
**Testing**: Pest feature tests and one browser smoke path through Sail
|
||||
**Target Platform**: Laravel monolith web application under `apps/platform`
|
||||
**Project Type**: web application
|
||||
**Performance Goals**: Improve scan throughput without increasing query shape beyond Spec 190, keep heavy filter changes non-chatty, and preserve DB-only render-time matrix surfaces
|
||||
**Constraints**: No compare-logic change, no new persistence, no hidden-tenant leakage, no generalized density framework, no provider or panel changes, and no new asset pipeline
|
||||
**Scale/Scope**: One existing matrix page, one existing Blade view, one existing builder, one logical contract file, and focused feature plus browser regressions
|
||||
**Performance Goals**: Improve operator scan throughput without adding more data queries than Spec 190; keep heavy filter changes explicit rather than chatty
|
||||
**Constraints**: No compare-logic changes, no new persistence, no hidden-tenant leakage, no generalized UI framework, no Filament provider changes
|
||||
**Scale/Scope**: One existing matrix page, one existing view, one existing builder, and focused test coverage updates
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.*
|
||||
*GATE: Passed before design. No new source-of-truth or persistence changes are expected.*
|
||||
|
||||
| Principle | Pre-Research | Post-Design | Notes |
|
||||
|-----------|--------------|-------------|-------|
|
||||
| Inventory-first / snapshots-second | PASS | PASS | The spec changes presentation only and keeps Spec 190 truth sources intact. |
|
||||
| Read/write separation | PASS | PASS | `Compare assigned tenants` remains the only mutation and is unchanged. |
|
||||
| Graph contract path | N/A | N/A | No new Graph behavior or contract-registry work is introduced. |
|
||||
| Deterministic capabilities | PASS | PASS | Existing capabilities remain canonical and unchanged. |
|
||||
| Workspace + tenant isolation | PASS | PASS | Visible-set-only aggregation and drilldown scope remain unchanged. |
|
||||
| RBAC-UX authorization semantics | PASS | PASS | Existing `404` vs `403` semantics and server-side enforcement remain unchanged. |
|
||||
| Run observability / Ops-UX | PASS | PASS | Compare-run truth is reused exactly as in Spec 190; this spec only clarifies the visual cues around it. |
|
||||
| Data minimization | PASS | PASS | No new data copies, exports, or persisted UI artifacts are introduced. |
|
||||
| Proportionality / anti-bloat | PASS | PASS | The work stays local to one page and does not add a new abstraction or stored artifact. |
|
||||
| Persisted truth / behavioral state | PASS | PASS | Presentation mode and staged filter state remain request-scoped only. |
|
||||
| UI semantics / few layers | PASS | PASS | Existing state, trust, freshness, and severity semantics are reused rather than redefined. |
|
||||
| Filament v5 / Livewire v4 compliance | PASS | PASS | The work remains inside the existing Filament page and Livewire-backed route. |
|
||||
| Provider registration location | PASS | PASS | No provider changes are required; Laravel 11+ registration remains in `bootstrap/providers.php`. |
|
||||
| Global search hard rule | PASS | PASS | No new searchable resource or page is introduced. |
|
||||
| Destructive action safety | PASS | PASS | No destructive action is added. Existing confirmation behavior for compare-start remains unchanged. |
|
||||
| Asset strategy | PASS | PASS | No new assets are required. Existing deployment behavior for `cd apps/platform && php artisan filament:assets` remains unchanged. |
|
||||
| Principle | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Inventory-first / snapshots-second | PASS | The feature changes presentation only and keeps Spec 190 truth sources intact. |
|
||||
| Read/write separation | PASS | `Compare assigned tenants` remains the only mutation and already exists. |
|
||||
| Workspace + tenant isolation | PASS | Visible-set-only behavior remains unchanged. |
|
||||
| RBAC-UX | PASS | Existing `404` vs `403` semantics stay intact; only presentation changes. |
|
||||
| Ops-UX 3-surface feedback | PASS | Refresh and polling surfaces are clarified visually without changing run semantics. |
|
||||
| Proportionality / anti-bloat | PASS | No new persistence, enum, framework, or cross-domain abstraction is introduced. |
|
||||
| UI semantics / few layers | PASS | Dense and compact modes reuse existing badge and compare semantics rather than inventing new status taxonomies. |
|
||||
| Filament v5 / Livewire v4 compliance | PASS | Work remains on the existing Filament page and Livewire-backed route. |
|
||||
| Provider registration location | PASS | No provider changes; registration remains in `bootstrap/providers.php`. |
|
||||
| Global search hard rule | PASS | No new global-searchable resource or page is introduced. |
|
||||
| Destructive action safety | PASS | No destructive action is added by this spec. |
|
||||
| Asset strategy | PASS | No new panel assets or shared assets are required. Existing deployment use of `filament:assets` remains unchanged. |
|
||||
|
||||
## Filament-Specific Compliance Notes
|
||||
|
||||
- **Livewire v4.0+ compliance**: This plan remains on Filament v5 + Livewire v4 and does not introduce legacy APIs.
|
||||
- **Provider registration location**: No panel or provider changes are needed; Laravel 11+ provider registration remains in `bootstrap/providers.php`.
|
||||
- **Global search**: The feature does not add a new globally searchable resource. Existing baseline-resource search behavior is unchanged.
|
||||
- **Livewire v4.0+ compliance**: This plan stays on the existing Filament v5 + Livewire v4 page stack and does not introduce legacy APIs.
|
||||
- **Provider registration location**: No panel/provider work is needed. Laravel 11+ provider registration remains in `bootstrap/providers.php`.
|
||||
- **Global search**: This spec does not add a new globally searchable resource. Existing baseline-resource search behavior is unchanged.
|
||||
- **Destructive actions**: No new destructive action is introduced. Existing compare-start actions remain confirmation-gated where already defined.
|
||||
- **Asset strategy**: No new global or on-demand asset registration is planned. Deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged.
|
||||
- **Testing plan**: Extend the existing matrix feature, builder, guard, and browser suites to cover presentation mode, staged filter application, and non-blocking status surfaces.
|
||||
|
||||
## Phase 0 Research
|
||||
|
||||
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/research.md`.
|
||||
|
||||
Key decisions:
|
||||
|
||||
- Keep the existing matrix route and truth model and change presentation only.
|
||||
- Resolve `auto`, `dense`, and `compact` mode from visible tenant count, with a route-local override only.
|
||||
- Make dense mode state-first rather than action-first.
|
||||
- Render single-tenant review as a compact compare list rather than a one-column matrix.
|
||||
- Convert heavy filters to staged apply/reset semantics.
|
||||
- Replace the long policy-type checkbox stack with a more compact operator-first selector.
|
||||
- Group legends into compact support context and separate blocking refresh from passive auto-refresh and last-updated cues.
|
||||
- Reuse existing drilldown and visible-set semantics unchanged.
|
||||
|
||||
## Phase 1 Design
|
||||
|
||||
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/191-baseline-compare-operator-mode/`:
|
||||
|
||||
- `research.md`: decisions and rejected alternatives for local operator-density work
|
||||
- `data-model.md`: request-scoped presentation models for mode state, staged filters, dense rows, compact results, and support-surface state
|
||||
- `contracts/baseline-compare-operator-mode.logical.openapi.yaml`: internal logical contract for adaptive rendering and staged filter application
|
||||
- `quickstart.md`: implementation and verification sequence for the follow-up spec
|
||||
|
||||
Design decisions:
|
||||
|
||||
- `auto` remains the default requested mode and resolves to `dense` for multiple visible tenants and `compact` for exactly one visible tenant.
|
||||
- Manual mode override remains route-local and must never become stored product truth.
|
||||
- Dense mode reuses existing compare truth but condenses cell content to state, trust, freshness, and attention.
|
||||
- Compact mode reuses the same truth but removes pseudo-matrix structure once only one visible tenant remains.
|
||||
- Heavy filter inputs stage locally and apply explicitly; lightweight route-state changes may remain immediate.
|
||||
- Grouped legends, passive auto-refresh, and last-updated signals become support context rather than competing top-level content.
|
||||
- **Testing plan**: Extend the existing matrix feature and browser suites to cover presentation mode, density, compact controls, and non-blocking status surfaces.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
### Documentation
|
||||
|
||||
```text
|
||||
specs/191-baseline-compare-operator-mode/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── tasks.md
|
||||
├── contracts/
|
||||
│ └── baseline-compare-operator-mode.logical.openapi.yaml
|
||||
└── checklists/
|
||||
└── requirements.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
### Source Code
|
||||
|
||||
```text
|
||||
apps/platform/
|
||||
@ -121,80 +77,84 @@ ### Source Code (repository root)
|
||||
├── Browser/
|
||||
│ └── Spec190BaselineCompareMatrixSmokeTest.php
|
||||
├── Feature/
|
||||
│ ├── Baselines/
|
||||
│ │ └── BaselineCompareMatrixBuilderTest.php
|
||||
│ ├── Filament/
|
||||
│ │ └── BaselineCompareMatrixPageTest.php
|
||||
│ └── Guards/
|
||||
│ └── ActionSurfaceContractTest.php
|
||||
└── Unit/
|
||||
└── Badges/
|
||||
└── Feature/Baselines/
|
||||
└── BaselineCompareMatrixBuilderTest.php
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the work inside the existing Spec 190 matrix implementation surface. This is a presentation refactor of one existing page and its supporting builder/view behavior, not a new domain slice or a new application area.
|
||||
**Structure Decision**: Keep the work inside the existing Spec 190 implementation surface. This follow-up spec is a refactor of one page and its supporting builder/view behavior, not a new domain slice.
|
||||
|
||||
## Complexity Tracking
|
||||
## Key Design Decisions
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| None | N/A | The follow-up stays within the existing page, builder, and test surfaces and introduces no new structural violation. |
|
||||
### D-001 — Keep the route and truth model unchanged
|
||||
|
||||
## Proportionality Review
|
||||
This spec modifies the existing `/compare-matrix` route only. No second route, second matrix artifact, or separate dense-report model is created.
|
||||
|
||||
- **Current operator problem**: The matrix already answers the right governance question, but not with enough density or calmness for repeated operator scanning.
|
||||
- **Existing structure is insufficient because**: The current single rendering shape tries to serve both multi-tenant and single-tenant workflows, so supporting context, action repetition, and cell chrome are too heavy in both cases.
|
||||
- **Narrowest correct implementation**: Keep the same route, truth sources, drilldowns, and compare semantics while adding route-local presentation state, denser rendering, and staged filter application.
|
||||
- **Ownership cost created**: Additional view-state logic, a logical contract file, and focused regression coverage for mode resolution, filter workflow, and status visibility.
|
||||
- **Alternative intentionally rejected**: A generalized density framework, a separate dense-report route, or a stored matrix artifact were rejected because the problem is local to the existing matrix surface.
|
||||
- **Release truth**: current-release operator workflow compression
|
||||
### D-002 — Use adaptive presentation, not separate features
|
||||
|
||||
`auto` mode is the canonical default. `dense` and `compact` exist as local operator overrides, but the product concept remains one matrix page with adaptive presentation.
|
||||
|
||||
### D-003 — Keep dense cells state-first
|
||||
|
||||
Dense mode cells must prioritize compare state, trust, freshness, and attention. Detailed reasons and multiple navigation targets become secondary reveals rather than permanent default chrome.
|
||||
|
||||
### D-004 — Treat controls as supporting context
|
||||
|
||||
Filters, legends, and refresh hints remain available but must become visibly subordinate to the matrix body. The page should read as a working surface, not a form-first screen.
|
||||
|
||||
### D-005 — Keep single-tenant mode honest
|
||||
|
||||
If only one visible tenant remains, the operator should see a compact comparison surface rather than an artificially wide matrix. The page should not preserve multi-tenant structure when it no longer helps.
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase A — Presentation Mode Contract
|
||||
|
||||
- Add route-local `auto`, `dense`, and `compact` mode state.
|
||||
- Resolve the active mode from visible tenant count unless manually overridden.
|
||||
- Expose `lastUpdatedAt`, `hasStagedFilterChanges`, and passive auto-refresh state to the page.
|
||||
- Add `auto`, `dense`, and `compact` mode state to the page.
|
||||
- Keep override state local to the route and compatible with existing drilldown URLs.
|
||||
- Reuse the current derived matrix bundle instead of adding a second persisted view model.
|
||||
|
||||
### Phase B — Dense Multi-Tenant Surface
|
||||
|
||||
- Keep the subject column sticky during horizontal scroll.
|
||||
- Condense dense cells to state, trust, freshness, and attention signals.
|
||||
- Move repeated actions into compact secondary affordances without breaking drilldown continuity.
|
||||
- Reduce per-cell chrome and prioritize state/trust/freshness.
|
||||
- Keep the subject axis sticky and readable across horizontal scroll.
|
||||
- Move repeated actions into compact secondary affordances where necessary.
|
||||
|
||||
### Phase C — Compact Single-Tenant Surface
|
||||
|
||||
- Replace pseudo-matrix rendering with a compact subject-result list when only one visible tenant remains.
|
||||
- Remove repeated tenant headers and duplicated secondary metadata.
|
||||
- Preserve subject focus and the existing compare/finding/run destinations.
|
||||
- Replace pseudo-matrix presentation with a shorter, calmer list optimized for one visible tenant.
|
||||
- Remove repeated tenant headers and duplicated labels.
|
||||
- Preserve subject focus and drilldowns.
|
||||
|
||||
### Phase D — Supporting Context Compression
|
||||
|
||||
- Convert heavy matrix filters to staged apply/reset behavior.
|
||||
- Replace the current long policy-type control with a more compact selector.
|
||||
- Group or collapse legends.
|
||||
- Separate blocking refresh from passive auto-refresh and last-updated status.
|
||||
- Convert heavy filters to an apply/reset workflow.
|
||||
- Compress legends into grouped or collapsible supporting blocks.
|
||||
- Clarify background polling, manual refresh, and last-updated status without using blocking loading surfaces.
|
||||
|
||||
### Phase E — Verification
|
||||
|
||||
- Extend focused feature coverage for mode resolution, staged filter behavior, and support-surface state.
|
||||
- Extend browser smoke coverage for one dense-mode path and one compact-mode path.
|
||||
- Keep existing Spec 190 matrix truth, drilldown continuity, and RBAC guarantees green.
|
||||
- Extend feature coverage for mode selection and density rules.
|
||||
- Extend browser coverage for one dense-mode path and one compact-mode path.
|
||||
- Keep existing Spec 190 truth and RBAC guarantees intact.
|
||||
|
||||
## Risk Assessment
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
|------|--------|------------|------------|
|
||||
| Dense mode becomes another framework | Medium | Low | Keep presentation logic local to the matrix page and avoid generalized shared abstractions. |
|
||||
| Compact mode hides too much follow-up value | Medium | Medium | Preserve one clear primary drilldown per subject and keep existing follow-up destinations intact. |
|
||||
| Staged filtering feels slow or unclear | Medium | Medium | Show explicit staged/applied state and keep reset obvious. |
|
||||
| Manual override confuses operators | Low | Medium | Keep `auto` as the default and surface the resolved mode clearly. |
|
||||
| Last-updated and auto-refresh cues drift out of sync | Medium | Low | Derive both cues from the same rendered matrix payload and active-run state. |
|
||||
| Dense mode becomes another framework | Medium | Low | Keep presentation logic local to the matrix page and view. |
|
||||
| Compact mode hides too much drilldown value | Medium | Medium | Keep one clear follow-up path per subject and preserve existing drilldowns. |
|
||||
| Apply/reset feels stale compared with live filters | Medium | Medium | Make staged filter state obvious and keep reset immediate. |
|
||||
| Manual override confuses operators | Low | Medium | Keep `auto` as default and label override state clearly. |
|
||||
|
||||
## Test Strategy
|
||||
|
||||
- Extend `BaselineCompareMatrixPageTest` for requested vs resolved mode, active filter application, compact vs dense rendering, and non-blocking refresh cues.
|
||||
- Extend `BaselineCompareMatrixBuilderTest` for any new derived presentation metadata required by the page.
|
||||
- Keep `ActionSurfaceContractTest` green so calmer actions do not regress the surface contract.
|
||||
- Extend `Spec190BaselineCompareMatrixSmokeTest` to prove one dense-mode and one compact-mode operator path on the Livewire page.
|
||||
- Run the focused Sail verification pack from `quickstart.md` and re-run `update-agent-context.sh copilot` after the plan is finalized.
|
||||
- Extend feature tests for mode resolution based on visible tenant count.
|
||||
- Add assertions for dense multi-tenant sticky subject behavior and reduced visible action noise.
|
||||
- Add assertions for compact single-tenant rendering and shorter supporting chrome.
|
||||
- Add coverage for explicit filter apply/reset behavior, grouped legends, and page-level last-updated messaging.
|
||||
- Reuse existing browser smoke coverage and extend it for one dense path plus one compact-mode path.
|
||||
- Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` and the focused matrix-related Pest suite before sign-off.
|
||||
@ -1,70 +0,0 @@
|
||||
# Quickstart: Baseline Compare Matrix: High-Density Operator Mode
|
||||
|
||||
## Goal
|
||||
|
||||
Turn the existing baseline compare matrix into a denser operator surface without changing its underlying compare truth. Multi-tenant use should favor high-density cross-tenant scanning, while single-tenant use should collapse into a calmer compact comparison view.
|
||||
|
||||
## Implementation Sequence
|
||||
|
||||
1. Add page-level presentation state.
|
||||
- Add `auto`, `dense`, and `compact` route-local mode state.
|
||||
- Resolve the active mode from visible tenant count unless the operator explicitly overrides it.
|
||||
- Expose `lastUpdatedAt`, staged-filter state, and passive auto-refresh state on the page.
|
||||
|
||||
2. Build the dense multi-tenant rendering contract.
|
||||
- Keep the subject column sticky.
|
||||
- Reduce dense-cell chrome to state, trust, freshness, and attention.
|
||||
- Move repeated follow-up links into compact secondary affordances.
|
||||
|
||||
3. Build the compact single-tenant rendering contract.
|
||||
- Replace the pseudo-matrix layout with a compact subject-result list.
|
||||
- Remove repeated tenant headers and repeated metadata blocks.
|
||||
- Preserve subject focus and existing drilldowns.
|
||||
|
||||
4. Compress supporting context.
|
||||
- Convert heavy filters to staged apply/reset semantics.
|
||||
- Replace the current long policy-type list with a more compact operator-first control.
|
||||
- Group or collapse legends so they remain available without dominating the page.
|
||||
- Separate blocking refresh from passive auto-refresh and last-updated status.
|
||||
|
||||
5. Extend regression coverage.
|
||||
- Cover mode resolution, dense multi-tenant layout, compact single-tenant layout, staged filters, and non-blocking refresh cues.
|
||||
- Keep existing Spec 190 matrix truth, drilldown continuity, and RBAC guarantees green.
|
||||
|
||||
## Suggested Test Files
|
||||
|
||||
- `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`
|
||||
- `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php`
|
||||
- `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
|
||||
- `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`
|
||||
|
||||
## Minimum Verification Commands
|
||||
|
||||
Run all commands through Sail from `apps/platform`.
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareMatrixPageTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php
|
||||
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||
```
|
||||
|
||||
## Manual Acceptance Checklist
|
||||
|
||||
1. Open a baseline profile whose matrix has multiple visible tenants and confirm `auto` resolves to dense mode.
|
||||
2. Verify the first subject column remains visible while horizontally scrolling dense mode.
|
||||
3. Confirm dense cells foreground compare state, trust, freshness, and attention before links or long prose.
|
||||
4. Open a matrix that resolves to one visible tenant and confirm `auto` resolves to compact mode instead of a one-column matrix.
|
||||
5. Change heavy filters and confirm the page stages those changes until the operator applies them.
|
||||
6. Confirm active filter count and filter summary reflect the applied state clearly.
|
||||
7. Confirm legends are still understandable but no longer dominate the top of the page.
|
||||
8. Trigger or observe queued/running compare work and confirm passive auto-refresh does not look like a permanent blocking load.
|
||||
9. Confirm the page shows when the current matrix payload was last updated.
|
||||
10. Verify tenant compare, finding, and run drilldowns still preserve the existing matrix context.
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
- No migration is expected.
|
||||
- No new asset registration is expected.
|
||||
- No queue topology change is expected because compare execution semantics stay unchanged.
|
||||
@ -1,111 +0,0 @@
|
||||
# Research: Baseline Compare Matrix: High-Density Operator Mode
|
||||
|
||||
## Decision: Keep the existing matrix route and truth model, and change presentation only
|
||||
|
||||
### Rationale
|
||||
|
||||
Spec 190 already established the correct workspace route, the correct baseline reference model, and the correct visible-set-only compare truth. The operator-density follow-up should stay on `/admin/baseline-profiles/{record}/compare-matrix` and must not introduce a second route, a second report artifact, or a second source of matrix truth.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Add a separate `dense report` page: rejected because it would duplicate the same baseline-scoped workflow on a second route.
|
||||
- Add a stored matrix snapshot: rejected because the operator problem is scan efficiency, not missing persistence.
|
||||
|
||||
## Decision: Resolve presentation mode from visible tenant count, with a local override only
|
||||
|
||||
### Rationale
|
||||
|
||||
The core operator split is real: one visible tenant is a compact review problem, while several visible tenants create a cross-tenant scan problem. The narrowest implementation is one requested mode (`auto`, `dense`, or `compact`) and one resolved mode at render time. `auto` should remain the default, while manual override stays local to the matrix route and must not become stored user preference or domain truth.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Separate feature flags or separate navigation entries for each mode: rejected because the matrix should remain one operator surface.
|
||||
- Persist mode preference per user: rejected because the current need is local workflow control, not profile-level personalization.
|
||||
|
||||
## Decision: Dense mode must be state-first, not action-first
|
||||
|
||||
### Rationale
|
||||
|
||||
In multi-tenant reading, the primary questions are where drift exists, how severe it is, whether the signal is trustworthy, and what deserves follow-up next. Dense cells should therefore foreground compare state, trust, freshness, and attention, while detailed reasons and repeated links move into compact secondary affordances.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Keep the current repeated open-link pattern in every cell: rejected because repeated actions visually outrank the state being scanned.
|
||||
- Remove cell-level follow-up completely: rejected because the matrix must remain a decision surface, not a dead-end report.
|
||||
|
||||
## Decision: Single-tenant mode should be a compact compare list, not a one-column matrix
|
||||
|
||||
### Rationale
|
||||
|
||||
Once only one visible tenant remains, the value of cross-tenant columns disappears. The surface should switch to a shorter subject-result list that reuses the same truth but removes repeated tenant headers, empty width, and oversized cell chrome.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Reuse dense mode even for one tenant: rejected because it preserves the wrong reading model.
|
||||
- Route single-tenant viewing away to the tenant compare page: rejected because the operator still started from the workspace baseline matrix context and should not lose that context automatically.
|
||||
|
||||
## Decision: Heavy filters should use staged apply/reset semantics
|
||||
|
||||
### Rationale
|
||||
|
||||
The current matrix is dense enough that chatty recomputation on every multi-select click works against operator flow. Policy types and other heavy matrix filters should stage changes locally, show that staged state clearly, and apply them deliberately. This improves calmness and makes the surface feel less like a form page.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Keep all filters live: rejected because heavy multi-select controls create noisy redraw behavior.
|
||||
- Convert every filter to manual apply: rejected because lightweight interactions such as mode switching or focused-subject clearing should remain immediate.
|
||||
|
||||
## Decision: Replace the long policy-type checkbox stack with a more compact operator-first selector
|
||||
|
||||
### Rationale
|
||||
|
||||
The policy-type filter is the most visually expensive control on the page. The follow-up spec should use a denser selection pattern such as searchable multi-select, type-to-find, or another compact control that exposes the same filter truth without the current long vertical list.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Keep the long checkbox list and only restyle it: rejected because vertical space is the actual product problem.
|
||||
- Hide policy type filtering behind a modal by default: rejected because the filter remains core enough to deserve immediate access.
|
||||
|
||||
## Decision: Legends should become grouped support context, optionally collapsible
|
||||
|
||||
### Rationale
|
||||
|
||||
State, freshness, and trust legends remain semantically valuable, especially for onboarding or occasional operators, but they should no longer compete with the matrix for top-of-screen attention. Grouped, compact legend blocks are the narrowest way to preserve semantics while reducing dominance.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Remove legends entirely: rejected because trust and freshness semantics still need an on-page reference.
|
||||
- Leave three separate full-width legend sections: rejected because they displace the primary working surface.
|
||||
|
||||
## Decision: Separate loading, auto-refresh, and last-updated cues
|
||||
|
||||
### Rationale
|
||||
|
||||
Spec 190 already exposed the risk of background polling reading like permanent blocking load. This follow-up should make three states explicit: active loading for user-triggered refresh, passive auto-refresh while queued or running compare work exists, and last-updated time for the currently rendered matrix.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Reuse one generic refresh chip for all states: rejected because operators cannot tell whether the page is blocked or simply polling.
|
||||
- Hide refresh state entirely: rejected because operator trust depends on understanding when the matrix is current.
|
||||
|
||||
## Decision: Reuse the existing drilldown and visible-set semantics without change
|
||||
|
||||
### Rationale
|
||||
|
||||
This spec is a presentation refactor, not a navigation or authorization redesign. The existing tenant compare, finding, run-detail, and canonical-navigation context from Spec 190 remain correct and should carry forward unchanged.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Introduce a dense-mode-specific drilldown model: rejected because it would create new behavior where existing follow-up paths are already sufficient.
|
||||
- Add aggregated hidden-tenant remainder summaries: rejected because visible-set-only semantics explicitly avoid hidden-tenant leakage.
|
||||
|
||||
## Decision: Validate primarily with focused page, builder, guard, and browser coverage
|
||||
|
||||
### Rationale
|
||||
|
||||
The highest-risk changes are mode resolution, dense-cell hierarchy, compact single-tenant rendering, filter apply behavior, and non-blocking refresh cues. These are best covered with focused feature tests plus one browser smoke path for the interactive Livewire surface.
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
- Browser-test every combination exhaustively: rejected because most of the behavior is deterministic and cheaper to validate through feature tests.
|
||||
- Limit validation to visual inspection: rejected because mode resolution and filter workflow are important enough to guard in CI.
|
||||
@ -1,7 +1,7 @@
|
||||
# Tasks: Baseline Compare Matrix: High-Density Operator Mode
|
||||
|
||||
**Input**: Design documents from `/specs/191-baseline-compare-operator-mode/`
|
||||
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`
|
||||
**Prerequisites**: `plan.md`, `spec.md`
|
||||
|
||||
**Tests**: Tests are REQUIRED. Extend Pest feature coverage and browser smoke coverage around the existing matrix route.
|
||||
**Operations**: This feature reuses existing `baseline_compare` run truth only. No new `OperationRun` type, no new run-summary contract, and no new notification channel should be introduced.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user