feat: add coverage v2 operator surface
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m15s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m15s
This commit is contained in:
parent
8cbf1f7fe3
commit
d1f7fbd4c6
@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Pages\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
||||||
|
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
|
||||||
|
use App\Support\Auth\Capabilities;
|
||||||
|
use App\Support\Navigation\NavigationScope;
|
||||||
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class CoverageV2Readiness extends Page
|
||||||
|
{
|
||||||
|
use ResolvesPanelTenantContext;
|
||||||
|
|
||||||
|
protected static bool $isDiscovered = false;
|
||||||
|
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 4;
|
||||||
|
|
||||||
|
protected static string|UnitEnum|null $navigationGroup = 'Inventory';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Coverage v2';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'workspaces/{workspace}/environments/{environment}/tenant-configuration/coverage-v2';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Coverage v2 Readiness';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.tenant-configuration.coverage-v2-readiness';
|
||||||
|
|
||||||
|
#[Locked]
|
||||||
|
public ?int $environmentId = null;
|
||||||
|
|
||||||
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||||
|
{
|
||||||
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
|
||||||
|
->exempt(ActionSurfaceSlot::ListHeader, 'Coverage v2 Readiness is read-only and exposes no page header actions.')
|
||||||
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::PrimaryLinkColumn->value)
|
||||||
|
->withPrimaryLinkColumnReason('Only the primary name/resource column opens the read-only inspect slide-over; full-row click would conflict with dense registry comparison columns.')
|
||||||
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The surface uses primary link columns for read-only inspect details and no secondary row menu.')
|
||||||
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Coverage v2 Readiness does not expose bulk actions.')
|
||||||
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Each table has a scoped no-data state for empty Coverage v2 registry or resource rows.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
$environment = static::resolveTenantContextForCurrentPanel();
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app(ManagedEnvironmentAccessScopeResolver::class)
|
||||||
|
->decision($user, $environment, Capabilities::EVIDENCE_VIEW)
|
||||||
|
->allowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function shouldRegisterNavigation(): bool
|
||||||
|
{
|
||||||
|
return NavigationScope::shouldRegisterEnvironmentNavigation()
|
||||||
|
&& parent::shouldRegisterNavigation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(Workspace|int|string|null $workspace = null, ManagedEnvironment|int|string|null $environment = null): void
|
||||||
|
{
|
||||||
|
$resolvedEnvironment = $environment instanceof ManagedEnvironment
|
||||||
|
? $environment
|
||||||
|
: static::resolveTenantContextForCurrentPanel();
|
||||||
|
|
||||||
|
if (! $resolvedEnvironment instanceof ManagedEnvironment || ! is_numeric($resolvedEnvironment->workspace_id)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($workspace instanceof Workspace && (int) $workspace->getKey() !== (int) $resolvedEnvironment->workspace_id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((is_int($workspace) || is_string($workspace)) && is_numeric($workspace) && (int) $workspace !== (int) $resolvedEnvironment->workspace_id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decision = app(ManagedEnvironmentAccessScopeResolver::class)
|
||||||
|
->decision($user, $resolvedEnvironment, Capabilities::EVIDENCE_VIEW);
|
||||||
|
|
||||||
|
if (! $decision->allowed()) {
|
||||||
|
abort($decision->denialHttpStatus ?? 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->environmentId = (int) $resolvedEnvironment->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string|Htmlable
|
||||||
|
{
|
||||||
|
return 'Coverage v2 Readiness';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function environment(): ManagedEnvironment
|
||||||
|
{
|
||||||
|
$environment = ManagedEnvironment::query()
|
||||||
|
->with('workspace')
|
||||||
|
->whereKey($this->environmentId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $environment instanceof ManagedEnvironment) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function readinessSummary(): array
|
||||||
|
{
|
||||||
|
return app(CoverageV2ReadinessReadModel::class)
|
||||||
|
->summary($this->environment());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $parameters
|
||||||
|
*/
|
||||||
|
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
|
||||||
|
{
|
||||||
|
$panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin';
|
||||||
|
|
||||||
|
if ($panelId !== 'admin') {
|
||||||
|
return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
$environment = static::resolveUrlEnvironment($parameters, $tenant);
|
||||||
|
|
||||||
|
if (! $environment instanceof ManagedEnvironment) {
|
||||||
|
return url('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
$workspace = static::resolveUrlWorkspace($environment, $parameters);
|
||||||
|
|
||||||
|
if (! $workspace instanceof Workspace && ! is_int($workspace) && ! is_string($workspace)) {
|
||||||
|
return url('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameters['environment'] ??= $environment;
|
||||||
|
$parameters['workspace'] ??= $workspace;
|
||||||
|
unset($parameters['tenant']);
|
||||||
|
|
||||||
|
return parent::getUrl($parameters, $isAbsolute, $panelId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $parameters
|
||||||
|
*/
|
||||||
|
private static function resolveUrlEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment
|
||||||
|
{
|
||||||
|
$parameterTenant = $parameters['tenant'] ?? $parameters['environment'] ?? null;
|
||||||
|
|
||||||
|
if ($parameterTenant instanceof ManagedEnvironment) {
|
||||||
|
return $parameterTenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tenant instanceof ManagedEnvironment) {
|
||||||
|
return $tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::resolveTenantContextForCurrentPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $parameters
|
||||||
|
*/
|
||||||
|
private static function resolveUrlWorkspace(ManagedEnvironment $environment, array $parameters): Workspace|string|int|null
|
||||||
|
{
|
||||||
|
$workspace = $parameters['workspace'] ?? null;
|
||||||
|
|
||||||
|
if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) {
|
||||||
|
return $workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workspace = $environment->workspace;
|
||||||
|
|
||||||
|
if ($workspace instanceof Workspace) {
|
||||||
|
return $workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $environment->workspace()->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Widgets\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\TenantConfigurationResource;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
||||||
|
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
|
||||||
|
use App\Support\Auth\Capabilities;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
|
use App\Support\Filament\TablePaginationProfiles;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Support\Enums\FontFamily;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Widgets\TableWidget;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
|
|
||||||
|
class CoverageV2ResourceInstancesTable extends TableWidget
|
||||||
|
{
|
||||||
|
protected static bool $isLazy = false;
|
||||||
|
|
||||||
|
protected int|string|array $columnSpan = 'full';
|
||||||
|
|
||||||
|
#[Locked]
|
||||||
|
public ?int $environmentId = null;
|
||||||
|
|
||||||
|
public function mount(?int $environmentId = null): void
|
||||||
|
{
|
||||||
|
$this->environmentId = $environmentId;
|
||||||
|
$this->authorizeEnvironment();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->heading('Resource instances')
|
||||||
|
->deferLoading(false)
|
||||||
|
->query(fn (): Builder => app(CoverageV2ReadinessReadModel::class)->resourceInstanceQuery($this->environment()))
|
||||||
|
->searchable()
|
||||||
|
->searchPlaceholder('Search resources')
|
||||||
|
->paginated(TablePaginationProfiles::productSurface())
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('source_display_name')
|
||||||
|
->label('Resource')
|
||||||
|
->searchable()
|
||||||
|
->sortable()
|
||||||
|
->description(fn (TenantConfigurationResource $record): string => (string) $record->canonical_resource_key)
|
||||||
|
->limit(48)
|
||||||
|
->tooltip(fn (TenantConfigurationResource $record): string => (string) ($record->source_display_name ?: $record->canonical_resource_key))
|
||||||
|
->action($this->inspectAction()),
|
||||||
|
TextColumn::make('resourceType.display_name')
|
||||||
|
->label('Resource type')
|
||||||
|
->searchable()
|
||||||
|
->sortable()
|
||||||
|
->description(fn (TenantConfigurationResource $record): string => (string) $record->canonical_type)
|
||||||
|
->wrap(),
|
||||||
|
TextColumn::make('providerConnection.display_name')
|
||||||
|
->label('Provider connection')
|
||||||
|
->sortable()
|
||||||
|
->placeholder('Unassigned provider connection')
|
||||||
|
->limit(32)
|
||||||
|
->tooltip(fn (?string $state): ?string => filled($state) ? $state : null)
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
TextColumn::make('coverage_level')
|
||||||
|
->label('Coverage level')
|
||||||
|
->state(fn (TenantConfigurationResource $record): ?string => $record->latestEvidence?->coverage_level?->value)
|
||||||
|
->badge()
|
||||||
|
->placeholder('Not captured')
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2CoverageLevel))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2CoverageLevel))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2CoverageLevel))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2CoverageLevel)),
|
||||||
|
TextColumn::make('latest_evidence_state')
|
||||||
|
->label('Evidence state')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2EvidenceState))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2EvidenceState))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2EvidenceState))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2EvidenceState)),
|
||||||
|
TextColumn::make('latest_identity_state')
|
||||||
|
->label('Identity state')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2IdentityState))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2IdentityState))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2IdentityState))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2IdentityState)),
|
||||||
|
TextColumn::make('latest_claim_state')
|
||||||
|
->label('Claim state')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2ClaimState))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2ClaimState))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2ClaimState))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2ClaimState)),
|
||||||
|
TextColumn::make('latest_captured_at')
|
||||||
|
->label('Last captured')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->placeholder('Not captured'),
|
||||||
|
TextColumn::make('latest_payload_hash')
|
||||||
|
->label('Evidence hash')
|
||||||
|
->state(fn (TenantConfigurationResource $record): ?string => $record->latestEvidence?->payload_hash ?: $record->latest_payload_hash)
|
||||||
|
->copyable()
|
||||||
|
->limit(16)
|
||||||
|
->fontFamily(FontFamily::Mono)
|
||||||
|
->placeholder('No hash')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
SelectFilter::make('resource_type_id')
|
||||||
|
->label('Resource type')
|
||||||
|
->options(fn (): array => \App\Models\TenantConfigurationResourceType::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('display_name')
|
||||||
|
->pluck('display_name', 'id')
|
||||||
|
->mapWithKeys(fn (string $label, int|string $id): array => [(string) $id => $label])
|
||||||
|
->all()),
|
||||||
|
SelectFilter::make('provider_connection_id')
|
||||||
|
->label('Provider connection')
|
||||||
|
->options(fn (): array => app(CoverageV2ReadinessReadModel::class)->providerConnectionOptions($this->environment())),
|
||||||
|
SelectFilter::make('coverage_level')
|
||||||
|
->label('Coverage level')
|
||||||
|
->options(CoverageV2ReadinessReadModel::coverageLevelOptions())
|
||||||
|
->query(fn (Builder $query, array $data): Builder => filled($data['value'] ?? null)
|
||||||
|
? $query->whereHas('latestEvidence', fn (Builder $latestEvidence): Builder => $latestEvidence->where('coverage_level', $data['value']))
|
||||||
|
: $query),
|
||||||
|
SelectFilter::make('latest_evidence_state')
|
||||||
|
->label('Evidence state')
|
||||||
|
->options(CoverageV2ReadinessReadModel::evidenceStateOptions()),
|
||||||
|
SelectFilter::make('latest_identity_state')
|
||||||
|
->label('Identity state')
|
||||||
|
->options(CoverageV2ReadinessReadModel::identityStateOptions()),
|
||||||
|
SelectFilter::make('latest_claim_state')
|
||||||
|
->label('Claim state')
|
||||||
|
->options(CoverageV2ReadinessReadModel::claimStateOptions()),
|
||||||
|
SelectFilter::make('source_class')
|
||||||
|
->label('Source class')
|
||||||
|
->options(CoverageV2ReadinessReadModel::sourceClassOptions()),
|
||||||
|
])
|
||||||
|
->recordUrl(null)
|
||||||
|
->bulkActions([])
|
||||||
|
->emptyStateHeading('No captured Coverage v2 resources')
|
||||||
|
->emptyStateDescription('No Coverage v2 resource rows match this environment and provider scope. Capture prerequisites must be satisfied before this page can support activation-readiness review.')
|
||||||
|
->emptyStateIcon('heroicon-o-shield-exclamation');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function environment(): ManagedEnvironment
|
||||||
|
{
|
||||||
|
$environment = ManagedEnvironment::query()
|
||||||
|
->whereKey($this->environmentId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $environment instanceof ManagedEnvironment) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function authorizeEnvironment(): void
|
||||||
|
{
|
||||||
|
$environment = $this->environment();
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decision = app(ManagedEnvironmentAccessScopeResolver::class)
|
||||||
|
->decision($user, $environment, Capabilities::EVIDENCE_VIEW);
|
||||||
|
|
||||||
|
if (! $decision->allowed()) {
|
||||||
|
abort($decision->denialHttpStatus ?? 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function inspectAction(): Action
|
||||||
|
{
|
||||||
|
return Action::make('inspect')
|
||||||
|
->label('Inspect')
|
||||||
|
->icon('heroicon-o-eye')
|
||||||
|
->color('gray')
|
||||||
|
->slideOver()
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Close')
|
||||||
|
->modalHeading(fn (TenantConfigurationResource $record): string => (string) ($record->source_display_name ?: $record->canonical_resource_key))
|
||||||
|
->modalContent(fn (TenantConfigurationResource $record): View => view('filament.modals.tenant-configuration.coverage-v2-resource-inspect', [
|
||||||
|
'details' => app(CoverageV2ReadinessReadModel::class)->inspectDetails($record, $this->environment(), auth()->user()),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Widgets\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
|
use App\Support\Filament\TablePaginationProfiles;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Support\Enums\FontFamily;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Widgets\TableWidget;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class CoverageV2ResourceTypesTable extends TableWidget
|
||||||
|
{
|
||||||
|
protected static bool $isLazy = false;
|
||||||
|
|
||||||
|
protected int|string|array $columnSpan = 'full';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->heading('Resource type registry')
|
||||||
|
->deferLoading(false)
|
||||||
|
->query(fn (): Builder => app(CoverageV2ReadinessReadModel::class)->resourceTypeQuery())
|
||||||
|
->searchable()
|
||||||
|
->searchPlaceholder('Search resource types')
|
||||||
|
->paginated(TablePaginationProfiles::productSurface())
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('display_name')
|
||||||
|
->label('Name')
|
||||||
|
->searchable()
|
||||||
|
->sortable()
|
||||||
|
->wrap()
|
||||||
|
->action($this->inspectAction()),
|
||||||
|
TextColumn::make('canonical_type')
|
||||||
|
->label('Canonical type')
|
||||||
|
->searchable()
|
||||||
|
->sortable()
|
||||||
|
->copyable()
|
||||||
|
->wrap()
|
||||||
|
->fontFamily(FontFamily::Mono)
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
TextColumn::make('workload')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(fn (mixed $state): string => $this->workloadLabel($state))
|
||||||
|
->color('info')
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
TextColumn::make('source_class')
|
||||||
|
->label('Source class')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2SourceClass))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2SourceClass))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2SourceClass))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2SourceClass)),
|
||||||
|
TextColumn::make('support_state')
|
||||||
|
->label('Support')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2SupportState))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2SupportState))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2SupportState))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2SupportState)),
|
||||||
|
TextColumn::make('default_coverage_level')
|
||||||
|
->label('Default coverage')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2CoverageLevel))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2CoverageLevel))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2CoverageLevel))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2CoverageLevel)),
|
||||||
|
TextColumn::make('supported_scope')
|
||||||
|
->label('Supported scope')
|
||||||
|
->state(fn (TenantConfigurationResourceType $record): string => app(CoverageV2ReadinessReadModel::class)->scopeInclusionLabel(
|
||||||
|
$record,
|
||||||
|
$this->selectedSupportedScopeKey(),
|
||||||
|
))
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state): string => $state === 'Included' ? 'success' : 'gray')
|
||||||
|
->icon(fn (string $state): string => $state === 'Included' ? 'heroicon-m-check-circle' : 'heroicon-m-minus-circle'),
|
||||||
|
IconColumn::make('beta_experimental')
|
||||||
|
->label('Beta')
|
||||||
|
->state(fn (TenantConfigurationResourceType $record): bool => $record->source_class === SourceClass::GraphBetaExperimental)
|
||||||
|
->boolean()
|
||||||
|
->alignCenter()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
IconColumn::make('graph_fallback')
|
||||||
|
->label('Fallback')
|
||||||
|
->state(fn (TenantConfigurationResourceType $record): bool => $record->source_class === SourceClass::GraphV1Fallback)
|
||||||
|
->boolean()
|
||||||
|
->alignCenter()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
TextColumn::make('default_claim_state')
|
||||||
|
->label('Claim behavior')
|
||||||
|
->badge()
|
||||||
|
->sortable()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2ClaimState))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2ClaimState))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2ClaimState))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2ClaimState)),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
SelectFilter::make('workload')
|
||||||
|
->options(CoverageV2ReadinessReadModel::workloadOptions()),
|
||||||
|
SelectFilter::make('source_class')
|
||||||
|
->label('Source class')
|
||||||
|
->options(CoverageV2ReadinessReadModel::sourceClassOptions()),
|
||||||
|
SelectFilter::make('support_state')
|
||||||
|
->label('Support')
|
||||||
|
->options(CoverageV2ReadinessReadModel::supportStateOptions()),
|
||||||
|
SelectFilter::make('supported_scope')
|
||||||
|
->label('Supported scope')
|
||||||
|
->options(fn (): array => app(CoverageV2ReadinessReadModel::class)->supportedScopeOptions())
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
$scopeKey = $data['value'] ?? null;
|
||||||
|
|
||||||
|
if (! is_string($scopeKey) || $scopeKey === '') {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$canonicalTypes = app(CoverageV2ReadinessReadModel::class)
|
||||||
|
->includedCanonicalTypesForScope($scopeKey);
|
||||||
|
|
||||||
|
return $canonicalTypes === []
|
||||||
|
? $query->whereRaw('1 = 0')
|
||||||
|
: $query->whereIn('canonical_type', $canonicalTypes);
|
||||||
|
}),
|
||||||
|
SelectFilter::make('beta_experimental')
|
||||||
|
->label('Beta')
|
||||||
|
->options(['yes' => 'Beta experimental', 'no' => 'Not beta'])
|
||||||
|
->query(fn (Builder $query, array $data): Builder => match ($data['value'] ?? null) {
|
||||||
|
'yes' => $query->where('source_class', SourceClass::GraphBetaExperimental->value),
|
||||||
|
'no' => $query->where('source_class', '!=', SourceClass::GraphBetaExperimental->value),
|
||||||
|
default => $query,
|
||||||
|
}),
|
||||||
|
SelectFilter::make('default_claim_state')
|
||||||
|
->label('Claim behavior')
|
||||||
|
->options(CoverageV2ReadinessReadModel::claimStateOptions()),
|
||||||
|
])
|
||||||
|
->recordUrl(null)
|
||||||
|
->bulkActions([])
|
||||||
|
->emptyStateHeading('No Coverage v2 resource types')
|
||||||
|
->emptyStateDescription('No active Coverage v2 registry rows are available. The registry is required before internal readiness can be reviewed.')
|
||||||
|
->emptyStateIcon('heroicon-o-shield-exclamation');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function selectedSupportedScopeKey(): ?string
|
||||||
|
{
|
||||||
|
$value = $this->tableFilters['supported_scope']['value'] ?? null;
|
||||||
|
|
||||||
|
return is_string($value) && $value !== ''
|
||||||
|
? $value
|
||||||
|
: app(CoverageV2ReadinessReadModel::class)->defaultScopeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function workloadLabel(mixed $state): string
|
||||||
|
{
|
||||||
|
$value = $state instanceof \BackedEnum ? (string) $state->value : (string) $state;
|
||||||
|
|
||||||
|
return $value === 'intune' ? 'Intune' : str($value)->replace('_', ' ')->headline()->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function inspectAction(): Action
|
||||||
|
{
|
||||||
|
return Action::make('inspect')
|
||||||
|
->label('Inspect')
|
||||||
|
->icon('heroicon-o-eye')
|
||||||
|
->color('gray')
|
||||||
|
->slideOver()
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Close')
|
||||||
|
->modalHeading(fn (TenantConfigurationResourceType $record): string => (string) $record->display_name)
|
||||||
|
->modalContent(fn (TenantConfigurationResourceType $record): View => view('filament.modals.tenant-configuration.coverage-v2-resource-type-inspect', [
|
||||||
|
'details' => app(CoverageV2ReadinessReadModel::class)->resourceTypeInspectDetails(
|
||||||
|
$record,
|
||||||
|
$this->selectedSupportedScopeKey(),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@
|
|||||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
use App\Filament\Pages\Settings\WorkspaceSettings;
|
use App\Filament\Pages\Settings\WorkspaceSettings;
|
||||||
|
use App\Filament\Pages\TenantConfiguration\CoverageV2Readiness;
|
||||||
use App\Filament\Resources\AlertDeliveryResource;
|
use App\Filament\Resources\AlertDeliveryResource;
|
||||||
use App\Filament\Resources\AlertDestinationResource;
|
use App\Filament\Resources\AlertDestinationResource;
|
||||||
use App\Filament\Resources\AlertRuleResource;
|
use App\Filament\Resources\AlertRuleResource;
|
||||||
@ -115,6 +116,12 @@ public function panel(Panel $panel): Panel
|
|||||||
->group('Inventory')
|
->group('Inventory')
|
||||||
->sort(3)
|
->sort(3)
|
||||||
->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && InventoryCoverage::canAccess()),
|
->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && InventoryCoverage::canAccess()),
|
||||||
|
NavigationItem::make('Coverage v2')
|
||||||
|
->url(fn (): string => CoverageV2Readiness::getUrl(panel: 'admin'))
|
||||||
|
->icon('heroicon-o-shield-check')
|
||||||
|
->group('Inventory')
|
||||||
|
->sort(4)
|
||||||
|
->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && CoverageV2Readiness::canAccess()),
|
||||||
NavigationItem::make('Groups')
|
NavigationItem::make('Groups')
|
||||||
->url(fn (): string => EntraGroupResource::getUrl(panel: 'admin'))
|
->url(fn (): string => EntraGroupResource::getUrl(panel: 'admin'))
|
||||||
->icon('heroicon-o-user-group')
|
->icon('heroicon-o-user-group')
|
||||||
@ -238,6 +245,7 @@ public function panel(Panel $panel): Panel
|
|||||||
BaselineCompareLanding::class,
|
BaselineCompareLanding::class,
|
||||||
BaselineSubjectResolution::class,
|
BaselineSubjectResolution::class,
|
||||||
InventoryCoverage::class,
|
InventoryCoverage::class,
|
||||||
|
CoverageV2Readiness::class,
|
||||||
EnvironmentRequiredPermissions::class,
|
EnvironmentRequiredPermissions::class,
|
||||||
WorkspaceSettings::class,
|
WorkspaceSettings::class,
|
||||||
CrossEnvironmentComparePage::class,
|
CrossEnvironmentComparePage::class,
|
||||||
|
|||||||
@ -0,0 +1,557 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\TenantConfiguration;
|
||||||
|
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
|
use App\Models\TenantConfigurationResource;
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Models\TenantConfigurationSupportedScope;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use App\Support\TenantConfiguration\Workload;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
final class CoverageV2ReadinessReadModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return Builder<TenantConfigurationResourceType>
|
||||||
|
*/
|
||||||
|
public function resourceTypeQuery(): Builder
|
||||||
|
{
|
||||||
|
return TenantConfigurationResourceType::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('workload')
|
||||||
|
->orderBy('source_class')
|
||||||
|
->orderBy('canonical_type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Builder<TenantConfigurationResource>
|
||||||
|
*/
|
||||||
|
public function resourceInstanceQuery(ManagedEnvironment $environment): Builder
|
||||||
|
{
|
||||||
|
return TenantConfigurationResource::query()
|
||||||
|
->where('workspace_id', (int) $environment->workspace_id)
|
||||||
|
->where('managed_environment_id', (int) $environment->getKey())
|
||||||
|
->with([
|
||||||
|
'resourceType:id,canonical_type,display_name,source_class,workload,support_state,default_coverage_level',
|
||||||
|
'providerConnection:id,workspace_id,managed_environment_id,display_name,provider',
|
||||||
|
'latestEvidence:id,resource_id,operation_run_id,payload_hash,evidence_state,coverage_level,capture_outcome,source_contract_key,source_endpoint,source_version,source_schema_hash,captured_at',
|
||||||
|
'latestEvidence.operationRun:id,workspace_id,managed_environment_id,type,status,outcome,created_at,completed_at',
|
||||||
|
])
|
||||||
|
->latest('latest_captured_at')
|
||||||
|
->latest('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function summary(ManagedEnvironment $environment): array
|
||||||
|
{
|
||||||
|
$resources = $this->resourceInstanceQuery($environment)->get([
|
||||||
|
'id',
|
||||||
|
'workspace_id',
|
||||||
|
'managed_environment_id',
|
||||||
|
'provider_connection_id',
|
||||||
|
'resource_type_id',
|
||||||
|
'source_class',
|
||||||
|
'latest_evidence_state',
|
||||||
|
'latest_identity_state',
|
||||||
|
'latest_claim_state',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resourceTypes = TenantConfigurationResourceType::query()
|
||||||
|
->active()
|
||||||
|
->get(['id', 'source_class']);
|
||||||
|
|
||||||
|
$blockers = $this->activationBlockers($environment);
|
||||||
|
$readinessState = $this->readinessState($resources->count(), $blockers);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'readiness_state' => $readinessState,
|
||||||
|
'readiness_reason' => $this->readinessReason($resources->count(), $blockers),
|
||||||
|
'readiness_next_step' => $this->readinessNextStep($resources->count(), $blockers),
|
||||||
|
'resource_types_total' => $resourceTypes->count(),
|
||||||
|
'resources_total' => $resources->count(),
|
||||||
|
'content_backed_count' => $resources
|
||||||
|
->where('latest_evidence_state', EvidenceState::ContentBacked)
|
||||||
|
->count(),
|
||||||
|
'activation_blocker_count' => $blockers->sum('count'),
|
||||||
|
'identity_conflict_count' => $resources
|
||||||
|
->where('latest_identity_state', IdentityState::IdentityConflict)
|
||||||
|
->count(),
|
||||||
|
'claim_allowed_count' => $resources
|
||||||
|
->where('latest_claim_state', ClaimState::ClaimAllowed)
|
||||||
|
->count(),
|
||||||
|
'claim_limited_count' => $resources
|
||||||
|
->where('latest_claim_state', ClaimState::ClaimLimited)
|
||||||
|
->count(),
|
||||||
|
'claim_blocked_count' => $resources
|
||||||
|
->where('latest_claim_state', ClaimState::ClaimBlocked)
|
||||||
|
->count(),
|
||||||
|
'beta_experimental_count' => $resourceTypes
|
||||||
|
->where('source_class', SourceClass::GraphBetaExperimental)
|
||||||
|
->count(),
|
||||||
|
'graph_fallback_count' => $resourceTypes
|
||||||
|
->where('source_class', SourceClass::GraphV1Fallback)
|
||||||
|
->count(),
|
||||||
|
'top_blockers' => $blockers->take(6)->values()->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, array{
|
||||||
|
* blocker: string,
|
||||||
|
* label: string,
|
||||||
|
* count: int,
|
||||||
|
* priority: int,
|
||||||
|
* example_resource: ?string,
|
||||||
|
* example_type: ?string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public function activationBlockers(ManagedEnvironment $environment): Collection
|
||||||
|
{
|
||||||
|
$groups = [];
|
||||||
|
|
||||||
|
$this->resourceInstanceQuery($environment)
|
||||||
|
->get([
|
||||||
|
'id',
|
||||||
|
'workspace_id',
|
||||||
|
'managed_environment_id',
|
||||||
|
'provider_connection_id',
|
||||||
|
'resource_type_id',
|
||||||
|
'canonical_type',
|
||||||
|
'source_display_name',
|
||||||
|
'source_class',
|
||||||
|
'latest_evidence_state',
|
||||||
|
'latest_identity_state',
|
||||||
|
'latest_claim_state',
|
||||||
|
])
|
||||||
|
->each(function (TenantConfigurationResource $resource) use (&$groups): void {
|
||||||
|
foreach ($this->blockersForResource($resource) as $blocker) {
|
||||||
|
$groups[$blocker] ??= [
|
||||||
|
'blocker' => $blocker,
|
||||||
|
'label' => self::blockerLabel($blocker),
|
||||||
|
'count' => 0,
|
||||||
|
'priority' => self::blockerPriority($blocker),
|
||||||
|
'example_resource' => null,
|
||||||
|
'example_type' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$groups[$blocker]['count']++;
|
||||||
|
$groups[$blocker]['example_resource'] ??= (string) ($resource->source_display_name ?: $resource->canonical_resource_key);
|
||||||
|
$groups[$blocker]['example_type'] ??= (string) $resource->canonical_type;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return collect($groups)
|
||||||
|
->sort(function (array $left, array $right): int {
|
||||||
|
return ($left['priority'] <=> $right['priority'])
|
||||||
|
?: ($right['count'] <=> $left['count'])
|
||||||
|
?: strnatcasecmp((string) $left['blocker'], (string) $right['blocker']);
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function providerConnectionOptions(ManagedEnvironment $environment): array
|
||||||
|
{
|
||||||
|
return ProviderConnection::query()
|
||||||
|
->where('workspace_id', (int) $environment->workspace_id)
|
||||||
|
->where('managed_environment_id', (int) $environment->getKey())
|
||||||
|
->orderBy('display_name')
|
||||||
|
->pluck('display_name', 'id')
|
||||||
|
->mapWithKeys(fn (string $label, int|string $id): array => [(string) $id => $label])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function supportedScopeOptions(): array
|
||||||
|
{
|
||||||
|
return app(SupportedScopeResolver::class)
|
||||||
|
->activeScopes()
|
||||||
|
->mapWithKeys(fn (TenantConfigurationSupportedScope $scope): array => [
|
||||||
|
(string) $scope->scope_key => (string) $scope->display_name,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function includedCanonicalTypesForScope(string $scopeKey): array
|
||||||
|
{
|
||||||
|
$scope = app(SupportedScopeResolver::class)->findActive($scopeKey);
|
||||||
|
|
||||||
|
if (! $scope instanceof TenantConfigurationSupportedScope) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resolved = app(SupportedScopeResolver::class)->resolveDefinition(
|
||||||
|
$scope,
|
||||||
|
TenantConfigurationResourceType::query()
|
||||||
|
->active()
|
||||||
|
->get(['canonical_type', 'source_class']),
|
||||||
|
);
|
||||||
|
} catch (UnexpectedValueException) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolved['included_resource_types'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeInclusionLabel(TenantConfigurationResourceType $resourceType, ?string $scopeKey = null): string
|
||||||
|
{
|
||||||
|
$scopeKey = $scopeKey ?: $this->defaultScopeKey();
|
||||||
|
|
||||||
|
if (! is_string($scopeKey) || $scopeKey === '') {
|
||||||
|
return 'No active scope';
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array((string) $resourceType->canonical_type, $this->includedCanonicalTypesForScope($scopeKey), true)
|
||||||
|
? 'Included'
|
||||||
|
: 'Not included';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultScopeKey(): ?string
|
||||||
|
{
|
||||||
|
$scope = app(SupportedScopeResolver::class)
|
||||||
|
->activeScopes()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $scope instanceof TenantConfigurationSupportedScope
|
||||||
|
? (string) $scope->scope_key
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function inspectDetails(TenantConfigurationResource $resource, ManagedEnvironment $environment, ?User $user): array
|
||||||
|
{
|
||||||
|
$resource->loadMissing([
|
||||||
|
'resourceType:id,canonical_type,display_name,source_class,workload,support_state,default_coverage_level',
|
||||||
|
'providerConnection:id,workspace_id,managed_environment_id,display_name,provider',
|
||||||
|
'latestEvidence:id,resource_id,operation_run_id,payload_hash,evidence_state,coverage_level,capture_outcome,source_contract_key,source_endpoint,source_version,source_schema_hash,captured_at',
|
||||||
|
'latestEvidence.operationRun:id,workspace_id,managed_environment_id,type,status,outcome,created_at,completed_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(int) $resource->workspace_id !== (int) $environment->workspace_id
|
||||||
|
|| (int) $resource->managed_environment_id !== (int) $environment->getKey()
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$run = $resource->latestEvidence?->operationRun;
|
||||||
|
$runUrl = $run !== null && $user instanceof User && Gate::forUser($user)->allows('view', $run)
|
||||||
|
? OperationRunLinks::view($run, $environment)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'resource' => (string) ($resource->source_display_name ?: $resource->canonical_resource_key),
|
||||||
|
'canonical_resource_key' => (string) $resource->canonical_resource_key,
|
||||||
|
'canonical_type' => (string) $resource->canonical_type,
|
||||||
|
'resource_type' => (string) ($resource->resourceType?->display_name ?: $resource->canonical_type),
|
||||||
|
'provider_connection' => (string) ($resource->providerConnection?->display_name ?: 'Unassigned provider connection'),
|
||||||
|
'coverage_level' => $resource->latestEvidence?->coverage_level?->value,
|
||||||
|
'evidence_state' => $resource->latest_evidence_state?->value,
|
||||||
|
'identity_state' => $resource->latest_identity_state?->value,
|
||||||
|
'claim_state' => $resource->latest_claim_state?->value,
|
||||||
|
'source_class' => $resource->source_class?->value,
|
||||||
|
'evidence_hash' => $resource->latestEvidence?->payload_hash ?: $resource->latest_payload_hash,
|
||||||
|
'last_captured' => $resource->latest_captured_at?->toDayDateTimeString(),
|
||||||
|
'source_contract_key' => $resource->latestEvidence?->source_contract_key,
|
||||||
|
'source_endpoint' => $resource->latestEvidence?->source_endpoint,
|
||||||
|
'source_version' => $resource->latestEvidence?->source_version,
|
||||||
|
'source_schema_hash' => $resource->latestEvidence?->source_schema_hash,
|
||||||
|
'capture_outcome' => $resource->latestEvidence?->capture_outcome?->value,
|
||||||
|
'identity_reason_code' => $this->safeIdentityReasonCode($resource),
|
||||||
|
'operation_run_url' => $runUrl,
|
||||||
|
'operation_run_label' => $run !== null ? 'Operation #'.$run->getKey() : null,
|
||||||
|
'blockers' => collect($this->blockersForResource($resource))
|
||||||
|
->map(fn (string $blocker): string => self::blockerLabel($blocker))
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function resourceTypeInspectDetails(TenantConfigurationResourceType $resourceType, ?string $scopeKey = null): array
|
||||||
|
{
|
||||||
|
$scopeKey = $scopeKey ?: $this->defaultScopeKey();
|
||||||
|
$scope = is_string($scopeKey) && $scopeKey !== ''
|
||||||
|
? app(SupportedScopeResolver::class)->findActive($scopeKey)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => (string) $resourceType->display_name,
|
||||||
|
'canonical_type' => (string) $resourceType->canonical_type,
|
||||||
|
'workload' => self::humanize(self::safeStateValue($resourceType->workload)),
|
||||||
|
'resource_class' => self::humanize(self::safeStateValue($resourceType->resource_class)),
|
||||||
|
'source_class' => self::safeStateValue($resourceType->source_class),
|
||||||
|
'support_state' => self::safeStateValue($resourceType->support_state),
|
||||||
|
'default_coverage_level' => self::safeStateValue($resourceType->default_coverage_level),
|
||||||
|
'default_evidence_state' => self::safeStateValue($resourceType->default_evidence_state),
|
||||||
|
'default_identity_state' => self::safeStateValue($resourceType->default_identity_state),
|
||||||
|
'default_claim_state' => self::safeStateValue($resourceType->default_claim_state),
|
||||||
|
'restore_tier' => self::humanize(self::safeStateValue($resourceType->restore_tier)),
|
||||||
|
'supported_scope' => $this->scopeInclusionLabel($resourceType, $scopeKey),
|
||||||
|
'scope' => $scope instanceof TenantConfigurationSupportedScope ? (string) $scope->display_name : null,
|
||||||
|
'scope_key' => $scopeKey,
|
||||||
|
'allows_beta_claims' => (bool) $resourceType->allows_beta_claims,
|
||||||
|
'allows_graph_fallback_claims' => (bool) $resourceType->allows_graph_fallback_claims,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function coverageLevelOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(CoverageLevel::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function evidenceStateOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(EvidenceState::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function identityStateOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(IdentityState::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function claimStateOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(ClaimState::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function sourceClassOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(SourceClass::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function supportStateOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(SupportState::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function workloadOptions(): array
|
||||||
|
{
|
||||||
|
return self::enumOptions(Workload::cases());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, \BackedEnum> $cases
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private static function enumOptions(array $cases): array
|
||||||
|
{
|
||||||
|
return collect($cases)
|
||||||
|
->mapWithKeys(fn (\BackedEnum $case): array => [
|
||||||
|
(string) $case->value => self::humanize((string) $case->value),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function humanize(string $value): string
|
||||||
|
{
|
||||||
|
return str($value)->replace('_', ' ')->headline()->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function safeStateValue(mixed $state): string
|
||||||
|
{
|
||||||
|
return $state instanceof \BackedEnum ? (string) $state->value : (string) $state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function blockersForResource(TenantConfigurationResource $resource): array
|
||||||
|
{
|
||||||
|
$blockers = [];
|
||||||
|
|
||||||
|
$identityState = $resource->latest_identity_state instanceof IdentityState
|
||||||
|
? $resource->latest_identity_state
|
||||||
|
: IdentityState::tryFrom((string) $resource->latest_identity_state);
|
||||||
|
|
||||||
|
$evidenceState = $resource->latest_evidence_state instanceof EvidenceState
|
||||||
|
? $resource->latest_evidence_state
|
||||||
|
: EvidenceState::tryFrom((string) $resource->latest_evidence_state);
|
||||||
|
|
||||||
|
$claimState = $resource->latest_claim_state instanceof ClaimState
|
||||||
|
? $resource->latest_claim_state
|
||||||
|
: ClaimState::tryFrom((string) $resource->latest_claim_state);
|
||||||
|
|
||||||
|
$sourceClass = $resource->source_class instanceof SourceClass
|
||||||
|
? $resource->source_class
|
||||||
|
: SourceClass::tryFrom((string) $resource->source_class);
|
||||||
|
|
||||||
|
if (in_array($identityState, [
|
||||||
|
IdentityState::IdentityConflict,
|
||||||
|
IdentityState::MissingExternalId,
|
||||||
|
IdentityState::UnsupportedIdentity,
|
||||||
|
], true)) {
|
||||||
|
$blockers[] = $identityState->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($claimState === ClaimState::ClaimBlocked) {
|
||||||
|
$blockers[] = ClaimState::ClaimBlocked->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($evidenceState, [
|
||||||
|
EvidenceState::NotCaptured,
|
||||||
|
EvidenceState::PermissionBlocked,
|
||||||
|
EvidenceState::SourceUnavailable,
|
||||||
|
EvidenceState::SchemaUnknown,
|
||||||
|
EvidenceState::CaptureFailed,
|
||||||
|
], true)) {
|
||||||
|
$blockers[] = $evidenceState->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceClass === SourceClass::GraphBetaExperimental) {
|
||||||
|
$blockers[] = 'beta_experimental';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($blockers));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readinessState(int $resourceCount, Collection $blockers): string
|
||||||
|
{
|
||||||
|
if ($resourceCount === 0) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasHardBlocker = $blockers->contains(fn (array $blocker): bool => in_array($blocker['blocker'], [
|
||||||
|
IdentityState::IdentityConflict->value,
|
||||||
|
IdentityState::MissingExternalId->value,
|
||||||
|
IdentityState::UnsupportedIdentity->value,
|
||||||
|
ClaimState::ClaimBlocked->value,
|
||||||
|
], true));
|
||||||
|
|
||||||
|
if ($hasHardBlocker) {
|
||||||
|
return 'blocked';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $blockers->isNotEmpty() ? 'needs_attention' : 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readinessReason(int $resourceCount, Collection $blockers): string
|
||||||
|
{
|
||||||
|
if ($resourceCount === 0) {
|
||||||
|
return 'No Coverage v2 resource rows exist for this managed environment.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$topBlocker = $blockers->first();
|
||||||
|
|
||||||
|
if (is_array($topBlocker)) {
|
||||||
|
return sprintf(
|
||||||
|
'%s is the highest-priority activation blocker.',
|
||||||
|
(string) ($topBlocker['label'] ?? 'Coverage v2 readiness'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Captured Coverage v2 resources have no activation blockers.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readinessNextStep(int $resourceCount, Collection $blockers): string
|
||||||
|
{
|
||||||
|
if ($resourceCount === 0) {
|
||||||
|
return 'Review capture prerequisites before using Coverage v2 as activation proof.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$topBlocker = $blockers->first();
|
||||||
|
|
||||||
|
if (is_array($topBlocker)) {
|
||||||
|
$example = $topBlocker['example_resource'] ?? $topBlocker['example_type'] ?? null;
|
||||||
|
|
||||||
|
if (is_string($example) && $example !== '') {
|
||||||
|
return sprintf('Inspect %s and resolve the blocker before cutover planning.', $example);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Inspect the top blocker group before cutover planning.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Review the read-only details before cutover planning.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function blockerLabel(string $blocker): string
|
||||||
|
{
|
||||||
|
return match ($blocker) {
|
||||||
|
'identity_conflict' => 'Identity conflict',
|
||||||
|
'missing_external_id' => 'Missing external ID',
|
||||||
|
'unsupported_identity' => 'Unsupported identity',
|
||||||
|
'claim_blocked' => 'Claim blocked',
|
||||||
|
'permission_blocked' => 'Permission blocked',
|
||||||
|
'source_unavailable' => 'Source unavailable',
|
||||||
|
'schema_unknown' => 'Schema unknown',
|
||||||
|
'capture_failed' => 'Capture failed',
|
||||||
|
'not_captured' => 'Not captured',
|
||||||
|
'beta_experimental' => 'Beta experimental',
|
||||||
|
default => self::humanize($blocker),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function blockerPriority(string $blocker): int
|
||||||
|
{
|
||||||
|
return match ($blocker) {
|
||||||
|
'identity_conflict' => 10,
|
||||||
|
'missing_external_id' => 11,
|
||||||
|
'unsupported_identity' => 12,
|
||||||
|
'claim_blocked' => 20,
|
||||||
|
'permission_blocked' => 30,
|
||||||
|
'source_unavailable' => 31,
|
||||||
|
'schema_unknown' => 32,
|
||||||
|
'capture_failed' => 33,
|
||||||
|
'not_captured' => 34,
|
||||||
|
'beta_experimental' => 90,
|
||||||
|
default => 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function safeIdentityReasonCode(TenantConfigurationResource $resource): ?string
|
||||||
|
{
|
||||||
|
$diagnostics = is_array($resource->identity_diagnostics) ? $resource->identity_diagnostics : [];
|
||||||
|
$reasonCode = $diagnostics['reason_code'] ?? null;
|
||||||
|
|
||||||
|
return is_string($reasonCode) && $reasonCode !== '' ? $reasonCode : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,6 +75,13 @@ final class BadgeCatalog
|
|||||||
BadgeDomain::BaselineCompareMatrixState->value => Domains\BaselineCompareMatrixStateBadge::class,
|
BadgeDomain::BaselineCompareMatrixState->value => Domains\BaselineCompareMatrixStateBadge::class,
|
||||||
BadgeDomain::BaselineCompareMatrixFreshness->value => Domains\BaselineCompareMatrixFreshnessBadge::class,
|
BadgeDomain::BaselineCompareMatrixFreshness->value => Domains\BaselineCompareMatrixFreshnessBadge::class,
|
||||||
BadgeDomain::BaselineCompareMatrixTrust->value => Domains\BaselineCompareMatrixTrustBadge::class,
|
BadgeDomain::BaselineCompareMatrixTrust->value => Domains\BaselineCompareMatrixTrustBadge::class,
|
||||||
|
BadgeDomain::CoverageV2Readiness->value => Domains\CoverageV2ReadinessBadge::class,
|
||||||
|
BadgeDomain::CoverageV2CoverageLevel->value => Domains\CoverageV2CoverageLevelBadge::class,
|
||||||
|
BadgeDomain::CoverageV2EvidenceState->value => Domains\CoverageV2EvidenceStateBadge::class,
|
||||||
|
BadgeDomain::CoverageV2IdentityState->value => Domains\CoverageV2IdentityStateBadge::class,
|
||||||
|
BadgeDomain::CoverageV2ClaimState->value => Domains\CoverageV2ClaimStateBadge::class,
|
||||||
|
BadgeDomain::CoverageV2SupportState->value => Domains\CoverageV2SupportStateBadge::class,
|
||||||
|
BadgeDomain::CoverageV2SourceClass->value => Domains\CoverageV2SourceClassBadge::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -66,4 +66,11 @@ enum BadgeDomain: string
|
|||||||
case BaselineCompareMatrixState = 'baseline_compare_matrix_state';
|
case BaselineCompareMatrixState = 'baseline_compare_matrix_state';
|
||||||
case BaselineCompareMatrixFreshness = 'baseline_compare_matrix_freshness';
|
case BaselineCompareMatrixFreshness = 'baseline_compare_matrix_freshness';
|
||||||
case BaselineCompareMatrixTrust = 'baseline_compare_matrix_trust';
|
case BaselineCompareMatrixTrust = 'baseline_compare_matrix_trust';
|
||||||
|
case CoverageV2Readiness = 'coverage_v2_readiness';
|
||||||
|
case CoverageV2CoverageLevel = 'coverage_v2_coverage_level';
|
||||||
|
case CoverageV2EvidenceState = 'coverage_v2_evidence_state';
|
||||||
|
case CoverageV2IdentityState = 'coverage_v2_identity_state';
|
||||||
|
case CoverageV2ClaimState = 'coverage_v2_claim_state';
|
||||||
|
case CoverageV2SupportState = 'coverage_v2_support_state';
|
||||||
|
case CoverageV2SourceClass = 'coverage_v2_source_class';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2ClaimStateBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'claim_allowed' => new BadgeSpec('Claim allowed', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'claim_limited' => new BadgeSpec('Claim limited', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'claim_blocked' => new BadgeSpec('Claim blocked', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'internal_only' => new BadgeSpec('Internal only', 'gray', 'heroicon-m-lock-closed'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2CoverageLevelBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'detected' => new BadgeSpec('Detected', 'gray', 'heroicon-m-eye'),
|
||||||
|
'content_backed' => new BadgeSpec('Content backed', 'info', 'heroicon-m-document-check'),
|
||||||
|
'comparable' => new BadgeSpec('Comparable', 'info', 'heroicon-m-arrows-right-left'),
|
||||||
|
'renderable' => new BadgeSpec('Renderable', 'primary', 'heroicon-m-document-text'),
|
||||||
|
'restorable' => new BadgeSpec('Restorable', 'success', 'heroicon-m-arrow-uturn-left'),
|
||||||
|
'certified' => new BadgeSpec('Certified', 'success', 'heroicon-m-check-badge'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2EvidenceStateBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'not_captured' => new BadgeSpec('Not captured', 'gray', 'heroicon-m-clock'),
|
||||||
|
'captured' => new BadgeSpec('Captured', 'info', 'heroicon-m-archive-box'),
|
||||||
|
'content_backed' => new BadgeSpec('Content backed', 'success', 'heroicon-m-document-check'),
|
||||||
|
'permission_blocked' => new BadgeSpec('Permission blocked', 'danger', 'heroicon-m-no-symbol'),
|
||||||
|
'source_unavailable' => new BadgeSpec('Source unavailable', 'warning', 'heroicon-m-signal-slash'),
|
||||||
|
'schema_unknown' => new BadgeSpec('Schema unknown', 'warning', 'heroicon-m-question-mark-circle'),
|
||||||
|
'capture_failed' => new BadgeSpec('Capture failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2IdentityStateBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'stable' => new BadgeSpec('Stable', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'derived' => new BadgeSpec('Derived', 'warning', 'heroicon-m-link'),
|
||||||
|
'identity_conflict' => new BadgeSpec('Identity conflict', 'danger', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'missing_external_id' => new BadgeSpec('Missing external ID', 'danger', 'heroicon-m-identification'),
|
||||||
|
'unsupported_identity' => new BadgeSpec('Unsupported identity', 'danger', 'heroicon-m-no-symbol'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2ReadinessBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'ready' => new BadgeSpec('Ready', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'needs_attention' => new BadgeSpec('Needs attention', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'blocked' => new BadgeSpec('Blocked', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'unknown' => new BadgeSpec('Unknown', 'gray', 'heroicon-m-question-mark-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2SourceClassBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'tcm' => new BadgeSpec('TCM', 'success', 'heroicon-m-shield-check'),
|
||||||
|
'graph_v1_fallback' => new BadgeSpec('Graph v1 fallback', 'warning', 'heroicon-m-arrow-path'),
|
||||||
|
'graph_beta_experimental' => new BadgeSpec('Graph beta experimental', 'warning', 'heroicon-m-beaker'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class CoverageV2SupportStateBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'supported' => new BadgeSpec('Supported', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'fallback_supported' => new BadgeSpec('Fallback supported', 'warning', 'heroicon-m-arrow-path'),
|
||||||
|
'experimental' => new BadgeSpec('Experimental', 'warning', 'heroicon-m-beaker'),
|
||||||
|
'unsupported' => new BadgeSpec('Unsupported', 'danger', 'heroicon-m-no-symbol'),
|
||||||
|
'out_of_scope' => new BadgeSpec('Out of scope', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
@php
|
||||||
|
$badge = function (\App\Support\Badges\BadgeDomain $domain, ?string $state): ?\App\Support\Badges\BadgeSpec {
|
||||||
|
return filled($state) ? \App\Support\Badges\BadgeRenderer::spec($domain, $state) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$badges = [
|
||||||
|
'Coverage' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2CoverageLevel, $details['coverage_level'] ?? null),
|
||||||
|
'Evidence' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2EvidenceState, $details['evidence_state'] ?? null),
|
||||||
|
'Identity' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2IdentityState, $details['identity_state'] ?? null),
|
||||||
|
'Claim' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2ClaimState, $details['claim_state'] ?? null),
|
||||||
|
'Source' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2SourceClass, $details['source_class'] ?? null),
|
||||||
|
];
|
||||||
|
|
||||||
|
$safeFields = [
|
||||||
|
'Resource type' => $details['resource_type'] ?? null,
|
||||||
|
'Canonical type' => $details['canonical_type'] ?? null,
|
||||||
|
'Canonical key' => $details['canonical_resource_key'] ?? null,
|
||||||
|
'Provider connection' => $details['provider_connection'] ?? null,
|
||||||
|
'Evidence hash' => $details['evidence_hash'] ?? null,
|
||||||
|
'Last captured' => $details['last_captured'] ?? null,
|
||||||
|
'Source contract' => $details['source_contract_key'] ?? null,
|
||||||
|
'Source version' => $details['source_version'] ?? null,
|
||||||
|
'Source schema hash' => $details['source_schema_hash'] ?? null,
|
||||||
|
'Capture outcome' => $details['capture_outcome'] ?? null,
|
||||||
|
'Identity reason' => $details['identity_reason_code'] ?? null,
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach ($badges as $label => $spec)
|
||||||
|
@if ($spec)
|
||||||
|
<x-filament::badge :color="$spec->color" :icon="$spec->icon">
|
||||||
|
{{ $label }}: {{ $spec->label }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (($details['blockers'] ?? []) !== [])
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-sm font-medium text-gray-950 dark:text-white">
|
||||||
|
Activation blockers
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach ($details['blockers'] as $blocker)
|
||||||
|
<x-filament::badge color="warning" icon="heroicon-m-exclamation-triangle">
|
||||||
|
{{ $blocker }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<dl class="grid gap-3 sm:grid-cols-2">
|
||||||
|
@foreach ($safeFields as $label => $value)
|
||||||
|
@if (filled($value))
|
||||||
|
<div class="min-w-0 border-l border-gray-200 pl-3 dark:border-white/10">
|
||||||
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $label }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 break-words text-sm text-gray-950 dark:text-white">
|
||||||
|
{{ $value }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
@if (filled($details['operation_run_url'] ?? null))
|
||||||
|
<x-filament::link :href="$details['operation_run_url']" icon="heroicon-o-arrow-top-right-on-square">
|
||||||
|
{{ $details['operation_run_label'] ?? 'Open operation' }}
|
||||||
|
</x-filament::link>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
@php
|
||||||
|
$badge = function (\App\Support\Badges\BadgeDomain $domain, ?string $state): ?\App\Support\Badges\BadgeSpec {
|
||||||
|
return filled($state) ? \App\Support\Badges\BadgeRenderer::spec($domain, $state) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$badges = [
|
||||||
|
'Coverage' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2CoverageLevel, $details['default_coverage_level'] ?? null),
|
||||||
|
'Evidence' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2EvidenceState, $details['default_evidence_state'] ?? null),
|
||||||
|
'Identity' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2IdentityState, $details['default_identity_state'] ?? null),
|
||||||
|
'Claim' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2ClaimState, $details['default_claim_state'] ?? null),
|
||||||
|
'Support' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2SupportState, $details['support_state'] ?? null),
|
||||||
|
'Source' => $badge(\App\Support\Badges\BadgeDomain::CoverageV2SourceClass, $details['source_class'] ?? null),
|
||||||
|
];
|
||||||
|
|
||||||
|
$safeFields = [
|
||||||
|
'Canonical type' => $details['canonical_type'] ?? null,
|
||||||
|
'Workload' => $details['workload'] ?? null,
|
||||||
|
'Resource class' => $details['resource_class'] ?? null,
|
||||||
|
'Scope' => $details['scope'] ?? null,
|
||||||
|
'Supported scope' => $details['supported_scope'] ?? null,
|
||||||
|
'Scope key' => $details['scope_key'] ?? null,
|
||||||
|
'Restore tier' => $details['restore_tier'] ?? null,
|
||||||
|
'Beta claims' => ($details['allows_beta_claims'] ?? false) ? 'Allowed' : 'Blocked',
|
||||||
|
'Graph fallback claims' => ($details['allows_graph_fallback_claims'] ?? false) ? 'Allowed' : 'Blocked',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach ($badges as $label => $spec)
|
||||||
|
@if ($spec)
|
||||||
|
<x-filament::badge :color="$spec->color" :icon="$spec->icon">
|
||||||
|
{{ $label }}: {{ $spec->label }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="grid gap-3 sm:grid-cols-2">
|
||||||
|
@foreach ($safeFields as $label => $value)
|
||||||
|
@if (filled($value))
|
||||||
|
<div class="min-w-0 border-l border-gray-200 pl-3 dark:border-white/10">
|
||||||
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $label }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 break-words text-sm text-gray-950 dark:text-white">
|
||||||
|
{{ $value }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
@php
|
||||||
|
$environment = $this->environment();
|
||||||
|
$summary = $this->readinessSummary();
|
||||||
|
$readiness = \App\Support\Badges\BadgeRenderer::spec(
|
||||||
|
\App\Support\Badges\BadgeDomain::CoverageV2Readiness,
|
||||||
|
$summary['readiness_state'] ?? 'unknown',
|
||||||
|
);
|
||||||
|
$summaryCounts = [
|
||||||
|
'Resource types' => $summary['resource_types_total'] ?? 0,
|
||||||
|
'Resources' => $summary['resources_total'] ?? 0,
|
||||||
|
'Content backed' => $summary['content_backed_count'] ?? 0,
|
||||||
|
'Activation blockers' => $summary['activation_blocker_count'] ?? 0,
|
||||||
|
];
|
||||||
|
$hasResourceInstances = ((int) ($summary['resources_total'] ?? 0)) > 0;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="min-w-0 space-y-5">
|
||||||
|
<div class="flex flex-col gap-4 border-l-4 border-gray-300 pl-4 dark:border-white/20 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div class="min-w-0 space-y-1">
|
||||||
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||||
|
Activation readiness
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Internal operator-only view for {{ $environment->name }} in {{ $environment->workspace?->name ?? 'Workspace' }}. No customer-facing Coverage v2 proof is activated.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-filament::badge :color="$readiness->color" :icon="$readiness->icon">
|
||||||
|
{{ $readiness->label }}
|
||||||
|
</x-filament::badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="grid gap-3 md:grid-cols-2">
|
||||||
|
<div class="min-w-0 border-l border-gray-200 pl-3 dark:border-white/10">
|
||||||
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Reason
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 break-words text-sm text-gray-950 dark:text-white">
|
||||||
|
{{ $summary['readiness_reason'] ?? 'Coverage v2 readiness has not been evaluated.' }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 border-l border-gray-200 pl-3 dark:border-white/10">
|
||||||
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Next step
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 break-words text-sm text-gray-950 dark:text-white">
|
||||||
|
{{ $summary['readiness_next_step'] ?? 'Inspect Coverage v2 readiness details before cutover planning.' }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
@foreach ($summaryCounts as $label => $value)
|
||||||
|
<div class="min-w-0 border-l border-gray-200 pl-3 dark:border-white/10">
|
||||||
|
<div class="break-words text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $label }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-xl font-semibold text-gray-950 dark:text-white">
|
||||||
|
{{ $value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (($summary['top_blockers'] ?? []) !== [])
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
Top activation blockers
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach ($summary['top_blockers'] as $blocker)
|
||||||
|
<x-filament::badge color="warning" icon="heroicon-m-exclamation-triangle">
|
||||||
|
{{ $blocker['label'] }}: {{ $blocker['count'] }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
@if ($hasResourceInstances)
|
||||||
|
<div class="min-w-0 overflow-x-auto pb-1">
|
||||||
|
@livewire(\App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceTypesTable::class, [], key('coverage-v2-resource-types-' . ($this->environmentId ?? 'none')))
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 overflow-x-auto pb-1">
|
||||||
|
@livewire(\App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceInstancesTable::class, [
|
||||||
|
'environmentId' => $this->environmentId,
|
||||||
|
], key('coverage-v2-resource-instances-' . ($this->environmentId ?? 'none')))
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="min-w-0 overflow-x-auto pb-1">
|
||||||
|
@livewire(\App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceInstancesTable::class, [
|
||||||
|
'environmentId' => $this->environmentId,
|
||||||
|
], key('coverage-v2-resource-instances-' . ($this->environmentId ?? 'none')))
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 overflow-x-auto pb-1">
|
||||||
|
@livewire(\App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceTypesTable::class, [], key('coverage-v2-resource-types-' . ($this->environmentId ?? 'none')))
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@ -0,0 +1,240 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\TenantConfiguration\CoverageV2Readiness;
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
|
use App\Models\TenantConfigurationResource;
|
||||||
|
use App\Models\TenantConfigurationResourceEvidence;
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Models\TenantConfigurationSupportedScope;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\OperationRunOutcome;
|
||||||
|
use App\Support\OperationRunStatus;
|
||||||
|
use App\Support\OperationRunType;
|
||||||
|
use App\Support\TenantConfiguration\CaptureOutcome;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
pest()->browser()->timeout(60_000);
|
||||||
|
|
||||||
|
it('Spec418 smokes the Coverage v2 operator surface without exposing raw evidence payloads', function (): void {
|
||||||
|
[$user, $environment] = spec418CoverageV2BrowserFixture();
|
||||||
|
spec418AuthenticateCoverageV2Browser($this, $user, $environment);
|
||||||
|
|
||||||
|
$page = visit(CoverageV2Readiness::getUrl(tenant: $environment, panel: 'admin'))
|
||||||
|
->resize(1440, 1100)
|
||||||
|
->waitForText('Coverage v2 Readiness')
|
||||||
|
->waitForText('Spec418 Browser conflicting assignment')
|
||||||
|
->assertSee('Resource type registry')
|
||||||
|
->assertSee('Resource instances')
|
||||||
|
->assertSee('Reason')
|
||||||
|
->assertSee('Identity conflict is the highest-priority activation blocker.')
|
||||||
|
->assertSee('Next step')
|
||||||
|
->assertSee('Inspect Spec418 Browser conflicting assignment and resolve the blocker before cutover planning.')
|
||||||
|
->assertSee('Coverage level')
|
||||||
|
->assertSee('Evidence state')
|
||||||
|
->assertSee('Identity state')
|
||||||
|
->assertSee('Claim state')
|
||||||
|
->assertSee('Source class')
|
||||||
|
->assertSee('Supported scope')
|
||||||
|
->assertSee('Top activation blockers')
|
||||||
|
->assertSee('Identity conflict')
|
||||||
|
->assertSee('Permission blocked')
|
||||||
|
->assertSee('Claim blocked')
|
||||||
|
->assertSee('Spec418 Browser captured assignment')
|
||||||
|
->assertDontSee('raw-response-secret')
|
||||||
|
->assertDontSee('normalized-secret')
|
||||||
|
->assertDontSee('permission-secret')
|
||||||
|
->assertDontSee('customer-ready')
|
||||||
|
->assertDontSee('Evidence gaps')
|
||||||
|
->assertScript('typeof window.Livewire !== "undefined"', true)
|
||||||
|
->assertScript('(() => document.querySelectorAll("table tbody tr").length > 0)()', true)
|
||||||
|
->assertScript("(() => performance.getEntriesByType('resource').filter((entry) => /graph\\.microsoft\\.com|\\/tcm\\b|provider-remote/i.test(entry.name)).length)()", 0)
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs();
|
||||||
|
|
||||||
|
$page->script(<<<'JS'
|
||||||
|
(() => {
|
||||||
|
const rows = Array.from(document.querySelectorAll('table tbody tr'));
|
||||||
|
const row = rows.find((candidate) => candidate.textContent.includes('Spec418 Browser conflicting assignment'));
|
||||||
|
const inspect = Array.from(row?.querySelectorAll('button, a') ?? [])
|
||||||
|
.find((element) => element.textContent.includes('Spec418 Browser conflicting assignment'));
|
||||||
|
|
||||||
|
inspect?.click();
|
||||||
|
})()
|
||||||
|
JS);
|
||||||
|
|
||||||
|
$page
|
||||||
|
->waitForText('Activation blockers')
|
||||||
|
->assertSee('Coverage: Detected')
|
||||||
|
->assertSee('Evidence: Permission blocked')
|
||||||
|
->assertSee('Identity: Identity conflict')
|
||||||
|
->assertSee('Spec418 Browser Microsoft provider')
|
||||||
|
->assertSee('same_scope_derived_identity_collision')
|
||||||
|
->assertSee('spec418-browser-schema-hash')
|
||||||
|
->assertSee('Operation #')
|
||||||
|
->assertDontSee('raw-response-secret')
|
||||||
|
->assertDontSee('normalized-secret')
|
||||||
|
->assertDontSee('permission-secret')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs()
|
||||||
|
->screenshot(true, 'spec418-coverage-v2-operator-surface-readiness');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: User, 1: ManagedEnvironment}
|
||||||
|
*/
|
||||||
|
function spec418CoverageV2BrowserFixture(): array
|
||||||
|
{
|
||||||
|
$environment = ManagedEnvironment::factory()->active()->create([
|
||||||
|
'name' => 'Spec418 Browser Environment',
|
||||||
|
'external_id' => 'spec418-browser-environment',
|
||||||
|
]);
|
||||||
|
|
||||||
|
[$user, $environment] = createUserWithTenant(
|
||||||
|
tenant: $environment,
|
||||||
|
role: 'owner',
|
||||||
|
workspaceRole: 'owner',
|
||||||
|
clearCapabilityCaches: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
$connection = ProviderConnection::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'display_name' => 'Spec418 Browser Microsoft provider',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contentType = TenantConfigurationResourceType::factory()->create([
|
||||||
|
'canonical_type' => 'spec418BrowserContentType',
|
||||||
|
'display_name' => 'Spec418 Browser content type',
|
||||||
|
'source_class' => SourceClass::Tcm->value,
|
||||||
|
'support_state' => SupportState::Supported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimAllowed->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$blockedType = TenantConfigurationResourceType::factory()->create([
|
||||||
|
'canonical_type' => 'spec418BrowserBlockedType',
|
||||||
|
'display_name' => 'Spec418 Browser blocked type',
|
||||||
|
'source_class' => SourceClass::GraphV1Fallback->value,
|
||||||
|
'support_state' => SupportState::FallbackSupported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimLimited->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
TenantConfigurationSupportedScope::factory()->create([
|
||||||
|
'scope_key' => 'spec418_browser_scope',
|
||||||
|
'display_name' => 'Spec418 Browser scope',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'included_resource_types' => [$contentType->canonical_type],
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'allow_beta' => false,
|
||||||
|
'customer_claims_allowed' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contentResource = TenantConfigurationResource::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'resource_type_id' => (int) $contentType->getKey(),
|
||||||
|
'canonical_type' => $contentType->canonical_type,
|
||||||
|
'canonical_resource_key' => 'spec418-browser-content-key',
|
||||||
|
'source_display_name' => 'Spec418 Browser captured assignment',
|
||||||
|
'source_class' => SourceClass::Tcm->value,
|
||||||
|
'latest_evidence_state' => EvidenceState::ContentBacked->value,
|
||||||
|
'latest_identity_state' => IdentityState::Stable->value,
|
||||||
|
'latest_claim_state' => ClaimState::ClaimAllowed->value,
|
||||||
|
'latest_captured_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$blockedResource = TenantConfigurationResource::factory()
|
||||||
|
->identityConflict()
|
||||||
|
->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'resource_type_id' => (int) $blockedType->getKey(),
|
||||||
|
'canonical_type' => $blockedType->canonical_type,
|
||||||
|
'canonical_resource_key' => 'spec418-browser-blocked-key',
|
||||||
|
'source_display_name' => 'Spec418 Browser conflicting assignment',
|
||||||
|
'source_class' => SourceClass::GraphV1Fallback->value,
|
||||||
|
'latest_evidence_state' => EvidenceState::PermissionBlocked->value,
|
||||||
|
'latest_captured_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
spec418AttachCoverageV2Evidence($contentResource, CoverageLevel::ContentBacked, CaptureOutcome::Captured, str_repeat('c', 64));
|
||||||
|
spec418AttachCoverageV2Evidence($blockedResource, CoverageLevel::Detected, CaptureOutcome::BlockedPermission, str_repeat('d', 64));
|
||||||
|
|
||||||
|
return [$user, $environment->refresh()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function spec418AttachCoverageV2Evidence(
|
||||||
|
TenantConfigurationResource $resource,
|
||||||
|
CoverageLevel $coverageLevel,
|
||||||
|
CaptureOutcome $captureOutcome,
|
||||||
|
string $payloadHash,
|
||||||
|
): void {
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'workspace_id' => (int) $resource->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $resource->managed_environment_id,
|
||||||
|
'type' => OperationRunType::TenantConfigurationCapture->value,
|
||||||
|
'status' => OperationRunStatus::Completed->value,
|
||||||
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$evidence = TenantConfigurationResourceEvidence::factory()->create([
|
||||||
|
'resource_id' => (int) $resource->getKey(),
|
||||||
|
'workspace_id' => (int) $resource->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $resource->managed_environment_id,
|
||||||
|
'provider_connection_id' => (int) $resource->provider_connection_id,
|
||||||
|
'resource_type_id' => (int) $resource->resource_type_id,
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
'payload_hash' => $payloadHash,
|
||||||
|
'raw_payload' => ['secret' => 'raw-response-secret'],
|
||||||
|
'normalized_payload' => ['secret' => 'normalized-secret'],
|
||||||
|
'permission_context' => ['token' => 'permission-secret'],
|
||||||
|
'evidence_state' => $resource->latest_evidence_state->value,
|
||||||
|
'coverage_level' => $coverageLevel->value,
|
||||||
|
'capture_outcome' => $captureOutcome->value,
|
||||||
|
'source_contract_key' => 'spec418.browser.contract',
|
||||||
|
'source_version' => 'v1.0',
|
||||||
|
'source_schema_hash' => 'spec418-browser-schema-hash',
|
||||||
|
'captured_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resource->forceFill([
|
||||||
|
'latest_evidence_id' => (int) $evidence->getKey(),
|
||||||
|
'latest_payload_hash' => $payloadHash,
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
function spec418AuthenticateCoverageV2Browser(
|
||||||
|
mixed $test,
|
||||||
|
User $user,
|
||||||
|
ManagedEnvironment $environment,
|
||||||
|
): void {
|
||||||
|
$workspaceId = (int) $environment->workspace_id;
|
||||||
|
|
||||||
|
$test->actingAs($user)->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
||||||
|
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
||||||
|
(string) $workspaceId => (int) $environment->getKey(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||||
|
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||||
|
(string) $workspaceId => (int) $environment->getKey(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -0,0 +1,468 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\TenantConfiguration\CoverageV2Readiness;
|
||||||
|
use App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceInstancesTable;
|
||||||
|
use App\Filament\Widgets\TenantConfiguration\CoverageV2ResourceTypesTable;
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
|
use App\Models\TenantConfigurationResource;
|
||||||
|
use App\Models\TenantConfigurationResourceEvidence;
|
||||||
|
use App\Models\TenantConfigurationResourceType;
|
||||||
|
use App\Models\TenantConfigurationSupportedScope;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WorkspaceMembership;
|
||||||
|
use App\Services\Auth\ManagedEnvironmentAccessDecision;
|
||||||
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
||||||
|
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
|
use App\Support\OperationRunOutcome;
|
||||||
|
use App\Support\OperationRunStatus;
|
||||||
|
use App\Support\OperationRunType;
|
||||||
|
use App\Support\TenantConfiguration\CaptureOutcome;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
function coverageV2ActingAs(User $user, ManagedEnvironment $environment): void
|
||||||
|
{
|
||||||
|
test()->actingAs($user);
|
||||||
|
$environment->makeCurrent();
|
||||||
|
Filament::setTenant($environment, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* contentType: TenantConfigurationResourceType,
|
||||||
|
* blockedType: TenantConfigurationResourceType,
|
||||||
|
* betaType: TenantConfigurationResourceType,
|
||||||
|
* connection: ProviderConnection,
|
||||||
|
* contentResource: TenantConfigurationResource,
|
||||||
|
* blockedResource: TenantConfigurationResource,
|
||||||
|
* betaResource: TenantConfigurationResource
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
function seedCoverageV2ReadinessScenario(ManagedEnvironment $environment): array
|
||||||
|
{
|
||||||
|
$connection = ProviderConnection::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'display_name' => 'Spec 418 Microsoft provider',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contentType = TenantConfigurationResourceType::factory()->create([
|
||||||
|
'canonical_type' => 'spec418ContentType',
|
||||||
|
'display_name' => 'Spec 418 content type',
|
||||||
|
'source_class' => SourceClass::Tcm->value,
|
||||||
|
'support_state' => SupportState::Supported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimAllowed->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$blockedType = TenantConfigurationResourceType::factory()->create([
|
||||||
|
'canonical_type' => 'spec418BlockedType',
|
||||||
|
'display_name' => 'Spec 418 blocked type',
|
||||||
|
'source_class' => SourceClass::GraphV1Fallback->value,
|
||||||
|
'support_state' => SupportState::FallbackSupported->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimLimited->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$betaType = TenantConfigurationResourceType::factory()->create([
|
||||||
|
'canonical_type' => 'spec418BetaType',
|
||||||
|
'display_name' => 'Spec 418 beta type',
|
||||||
|
'source_class' => SourceClass::GraphBetaExperimental->value,
|
||||||
|
'support_state' => SupportState::Experimental->value,
|
||||||
|
'default_coverage_level' => CoverageLevel::Detected->value,
|
||||||
|
'default_claim_state' => ClaimState::ClaimBlocked->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
TenantConfigurationSupportedScope::factory()->create([
|
||||||
|
'scope_key' => 'spec418_scope',
|
||||||
|
'display_name' => 'Spec 418 scope',
|
||||||
|
'minimum_coverage_level' => CoverageLevel::ContentBacked->value,
|
||||||
|
'included_resource_types' => [$contentType->canonical_type],
|
||||||
|
'allow_graph_fallback' => false,
|
||||||
|
'allow_beta' => false,
|
||||||
|
'customer_claims_allowed' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$contentResource = TenantConfigurationResource::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'resource_type_id' => (int) $contentType->getKey(),
|
||||||
|
'canonical_type' => $contentType->canonical_type,
|
||||||
|
'source_display_name' => 'Spec 418 captured assignment filter',
|
||||||
|
'source_class' => SourceClass::Tcm->value,
|
||||||
|
'latest_evidence_state' => EvidenceState::ContentBacked->value,
|
||||||
|
'latest_identity_state' => IdentityState::Stable->value,
|
||||||
|
'latest_claim_state' => ClaimState::ClaimAllowed->value,
|
||||||
|
'latest_captured_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$blockedResource = TenantConfigurationResource::factory()
|
||||||
|
->identityConflict()
|
||||||
|
->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'resource_type_id' => (int) $blockedType->getKey(),
|
||||||
|
'canonical_type' => $blockedType->canonical_type,
|
||||||
|
'source_display_name' => 'Spec 418 conflicting assignment filter',
|
||||||
|
'source_class' => SourceClass::GraphV1Fallback->value,
|
||||||
|
'latest_evidence_state' => EvidenceState::PermissionBlocked->value,
|
||||||
|
'latest_captured_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$betaResource = TenantConfigurationResource::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'resource_type_id' => (int) $betaType->getKey(),
|
||||||
|
'canonical_type' => $betaType->canonical_type,
|
||||||
|
'source_display_name' => 'Spec 418 beta resource',
|
||||||
|
'source_class' => SourceClass::GraphBetaExperimental->value,
|
||||||
|
'latest_evidence_state' => EvidenceState::NotCaptured->value,
|
||||||
|
'latest_identity_state' => IdentityState::Stable->value,
|
||||||
|
'latest_claim_state' => ClaimState::ClaimBlocked->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$contentResource, $blockedResource] as $resource) {
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'type' => OperationRunType::TenantConfigurationCapture->value,
|
||||||
|
'status' => OperationRunStatus::Completed->value,
|
||||||
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$evidence = TenantConfigurationResourceEvidence::factory()->create([
|
||||||
|
'resource_id' => (int) $resource->getKey(),
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'resource_type_id' => (int) $resource->resource_type_id,
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
'payload_hash' => $resource->is($blockedResource)
|
||||||
|
? str_repeat('b', 64)
|
||||||
|
: str_repeat('a', 64),
|
||||||
|
'raw_payload' => ['secret' => 'raw-response-secret'],
|
||||||
|
'normalized_payload' => ['secret' => 'normalized-secret'],
|
||||||
|
'permission_context' => ['token' => 'permission-secret'],
|
||||||
|
'evidence_state' => $resource->latest_evidence_state->value,
|
||||||
|
'coverage_level' => $resource->is($blockedResource)
|
||||||
|
? CoverageLevel::Detected->value
|
||||||
|
: CoverageLevel::ContentBacked->value,
|
||||||
|
'capture_outcome' => $resource->is($blockedResource)
|
||||||
|
? CaptureOutcome::BlockedPermission->value
|
||||||
|
: CaptureOutcome::Captured->value,
|
||||||
|
'source_contract_key' => 'spec418.contract',
|
||||||
|
'source_version' => 'v1.0',
|
||||||
|
'source_schema_hash' => 'spec418-schema-hash',
|
||||||
|
'captured_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resource->forceFill([
|
||||||
|
'latest_evidence_id' => (int) $evidence->getKey(),
|
||||||
|
'latest_payload_hash' => (string) $evidence->payload_hash,
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'contentType' => $contentType,
|
||||||
|
'blockedType' => $blockedType,
|
||||||
|
'betaType' => $betaType,
|
||||||
|
'connection' => $connection,
|
||||||
|
'contentResource' => $contentResource->refresh(),
|
||||||
|
'blockedResource' => $blockedResource->refresh(),
|
||||||
|
'betaResource' => $betaResource->refresh(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders the read-only Coverage v2 readiness surface with scoped summary and no raw payloads', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
seedCoverageV2ReadinessScenario($environment);
|
||||||
|
$foreignEnvironment = ManagedEnvironment::factory()->create(['workspace_id' => (int) $environment->workspace_id]);
|
||||||
|
ProviderConnection::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'managed_environment_id' => (int) $foreignEnvironment->getKey(),
|
||||||
|
'display_name' => 'Foreign provider must not render',
|
||||||
|
]);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
bindFailHardGraphClient();
|
||||||
|
|
||||||
|
$response = assertNoOutboundHttp(fn () => $this->get(CoverageV2Readiness::getUrl(tenant: $environment)));
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertSee('Coverage v2 Readiness')
|
||||||
|
->assertSee('Blocked')
|
||||||
|
->assertSee('Activation readiness')
|
||||||
|
->assertSee('Reason')
|
||||||
|
->assertSee('Identity conflict is the highest-priority activation blocker.')
|
||||||
|
->assertSee('Next step')
|
||||||
|
->assertSee('Inspect Spec 418 conflicting assignment filter and resolve the blocker before cutover planning.')
|
||||||
|
->assertSee('Resource types')
|
||||||
|
->assertSee('Activation blockers')
|
||||||
|
->assertSee('Identity conflict')
|
||||||
|
->assertSee('Claim blocked')
|
||||||
|
->assertSee('Spec 418 content type')
|
||||||
|
->assertSee('Spec 418 conflicting assignment filter')
|
||||||
|
->assertDontSee('raw-response-secret')
|
||||||
|
->assertDontSee('normalized-secret')
|
||||||
|
->assertDontSee('permission-secret')
|
||||||
|
->assertDontSee('Foreign provider must not render')
|
||||||
|
->assertDontSee('customer-ready')
|
||||||
|
->assertDontSee('Evidence gaps');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('explains unknown readiness when no Coverage v2 resources are captured', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
$response = $this->get(CoverageV2Readiness::getUrl(tenant: $environment));
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertSee('Unknown')
|
||||||
|
->assertSee('Activation readiness')
|
||||||
|
->assertSee('Reason')
|
||||||
|
->assertSee('No Coverage v2 resource rows exist for this managed environment.')
|
||||||
|
->assertSee('Next step')
|
||||||
|
->assertSee('Review capture prerequisites before using Coverage v2 as activation proof.')
|
||||||
|
->assertSee('No captured Coverage v2 resources');
|
||||||
|
|
||||||
|
expect($response->getContent())
|
||||||
|
->toContain('No captured Coverage v2 resources')
|
||||||
|
->and(strpos((string) $response->getContent(), 'No captured Coverage v2 resources'))
|
||||||
|
->toBeLessThan(strpos((string) $response->getContent(), 'Resource type registry'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives summary counts and top blockers from Coverage v2 state only', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
seedCoverageV2ReadinessScenario($environment);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
$readModel = app(CoverageV2ReadinessReadModel::class);
|
||||||
|
$summary = $readModel->summary($environment);
|
||||||
|
$blockers = $readModel->activationBlockers($environment)->pluck('blocker')->all();
|
||||||
|
|
||||||
|
expect($summary)
|
||||||
|
->toMatchArray([
|
||||||
|
'readiness_state' => 'blocked',
|
||||||
|
'resources_total' => 3,
|
||||||
|
'content_backed_count' => 1,
|
||||||
|
'activation_blocker_count' => 6,
|
||||||
|
'identity_conflict_count' => 1,
|
||||||
|
'claim_allowed_count' => 1,
|
||||||
|
'claim_blocked_count' => 2,
|
||||||
|
])
|
||||||
|
->and($summary['beta_experimental_count'])
|
||||||
|
->toBeGreaterThanOrEqual(1)
|
||||||
|
->and($summary['graph_fallback_count'])
|
||||||
|
->toBeGreaterThanOrEqual(1)
|
||||||
|
->and(array_slice($blockers, 0, 5))
|
||||||
|
->toBe([
|
||||||
|
'identity_conflict',
|
||||||
|
'claim_blocked',
|
||||||
|
'permission_blocked',
|
||||||
|
'not_captured',
|
||||||
|
'beta_experimental',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders resource type registry filters and supported-scope inclusion', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
$scenario = seedCoverageV2ReadinessScenario($environment);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(CoverageV2ResourceTypesTable::class)
|
||||||
|
->assertTableColumnExists('display_name')
|
||||||
|
->assertTableColumnExists('canonical_type')
|
||||||
|
->assertTableColumnExists('source_class')
|
||||||
|
->assertTableColumnExists('support_state')
|
||||||
|
->assertTableColumnExists('default_coverage_level')
|
||||||
|
->assertTableColumnExists('supported_scope')
|
||||||
|
->assertTableColumnExists('default_claim_state')
|
||||||
|
->assertTableActionExists('inspect', fn (Action $action): bool => $action->getLabel() === 'Inspect' && ! $action->isConfirmationRequired(), $scenario['contentType'])
|
||||||
|
->searchTable('Spec 418')
|
||||||
|
->assertCanSeeTableRecords([$scenario['contentType'], $scenario['blockedType'], $scenario['betaType']])
|
||||||
|
->filterTable('supported_scope', 'spec418_scope')
|
||||||
|
->assertCanSeeTableRecords([$scenario['contentType']])
|
||||||
|
->assertCanNotSeeTableRecords([$scenario['blockedType'], $scenario['betaType']])
|
||||||
|
->removeTableFilters()
|
||||||
|
->filterTable('beta_experimental', 'yes')
|
||||||
|
->assertCanSeeTableRecords([$scenario['betaType']])
|
||||||
|
->assertCanNotSeeTableRecords([$scenario['contentType'], $scenario['blockedType']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders resource instance states, filters, and one read-only inspect action', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
$scenario = seedCoverageV2ReadinessScenario($environment);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(CoverageV2ResourceInstancesTable::class, ['environmentId' => (int) $environment->getKey()])
|
||||||
|
->assertTableColumnExists('source_display_name')
|
||||||
|
->assertTableColumnExists('resourceType.display_name')
|
||||||
|
->assertTableColumnExists('providerConnection.display_name')
|
||||||
|
->assertTableColumnExists('coverage_level')
|
||||||
|
->assertTableColumnExists('latest_evidence_state')
|
||||||
|
->assertTableColumnExists('latest_identity_state')
|
||||||
|
->assertTableColumnExists('latest_claim_state')
|
||||||
|
->assertTableColumnExists('latest_payload_hash')
|
||||||
|
->assertCanSeeTableRecords([$scenario['contentResource'], $scenario['blockedResource']])
|
||||||
|
->assertTableColumnFormattedStateSet('latest_claim_state', 'Claim blocked', $scenario['blockedResource'])
|
||||||
|
->assertTableColumnFormattedStateSet('latest_identity_state', 'Identity conflict', $scenario['blockedResource'])
|
||||||
|
->assertTableActionExists('inspect', fn (Action $action): bool => $action->getLabel() === 'Inspect' && ! $action->isConfirmationRequired(), $scenario['blockedResource'])
|
||||||
|
->filterTable('latest_identity_state', IdentityState::IdentityConflict->value)
|
||||||
|
->assertCanSeeTableRecords([$scenario['blockedResource']])
|
||||||
|
->assertCanNotSeeTableRecords([$scenario['contentResource']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redacts raw evidence payload fields from inspect disclosure', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
$scenario = seedCoverageV2ReadinessScenario($environment);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
$details = app(CoverageV2ReadinessReadModel::class)
|
||||||
|
->inspectDetails($scenario['blockedResource'], $environment, $user);
|
||||||
|
$run = $scenario['blockedResource']->latestEvidence?->operationRun;
|
||||||
|
$outsider = User::factory()->create();
|
||||||
|
$outsiderDetails = app(CoverageV2ReadinessReadModel::class)
|
||||||
|
->inspectDetails($scenario['blockedResource'], $environment, $outsider);
|
||||||
|
|
||||||
|
$html = view('filament.modals.tenant-configuration.coverage-v2-resource-inspect', [
|
||||||
|
'details' => $details,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
expect($details)
|
||||||
|
->toHaveKey('identity_reason_code', 'same_scope_derived_identity_collision')
|
||||||
|
->toHaveKey('operation_run_url', OperationRunLinks::view($run, $environment))
|
||||||
|
->not->toHaveKeys(['raw_payload', 'normalized_payload', 'permission_context'])
|
||||||
|
->and($outsiderDetails['operation_run_url'] ?? null)
|
||||||
|
->toBeNull()
|
||||||
|
->and($html)
|
||||||
|
->toContain('Identity conflict')
|
||||||
|
->toContain('same_scope_derived_identity_collision')
|
||||||
|
->toContain('spec418-schema-hash')
|
||||||
|
->not->toContain('raw-response-secret')
|
||||||
|
->not->toContain('normalized-secret')
|
||||||
|
->not->toContain('permission-secret')
|
||||||
|
->not->toContain('raw_payload')
|
||||||
|
->not->toContain('normalized_payload')
|
||||||
|
->not->toContain('permission_context');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redacts raw registry metadata fields from resource type inspect disclosure', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
$scenario = seedCoverageV2ReadinessScenario($environment);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
$details = app(CoverageV2ReadinessReadModel::class)
|
||||||
|
->resourceTypeInspectDetails($scenario['contentType'], 'spec418_scope');
|
||||||
|
|
||||||
|
$html = view('filament.modals.tenant-configuration.coverage-v2-resource-type-inspect', [
|
||||||
|
'details' => $details,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
expect($details)
|
||||||
|
->toHaveKey('canonical_type', 'spec418ContentType')
|
||||||
|
->not->toHaveKeys(['metadata', 'raw_payload', 'normalized_payload', 'permission_context'])
|
||||||
|
->and($html)
|
||||||
|
->toContain('Spec 418 scope')
|
||||||
|
->toContain('Content backed')
|
||||||
|
->toContain('Claim allowed')
|
||||||
|
->not->toContain('raw-response-secret')
|
||||||
|
->not->toContain('normalized-secret')
|
||||||
|
->not->toContain('permission-secret')
|
||||||
|
->not->toContain('metadata')
|
||||||
|
->not->toContain('raw_payload')
|
||||||
|
->not->toContain('normalized_payload')
|
||||||
|
->not->toContain('permission_context');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when the actor is not a member of the requested workspace', function (): void {
|
||||||
|
[$owner, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
seedCoverageV2ReadinessScenario($environment);
|
||||||
|
$outsider = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($outsider)
|
||||||
|
->get(CoverageV2Readiness::getUrl(tenant: $environment))
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when the actor belongs to the workspace but not the requested environment', function (): void {
|
||||||
|
[$owner, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
seedCoverageV2ReadinessScenario($environment);
|
||||||
|
$otherEnvironment = ManagedEnvironment::factory()->create(['workspace_id' => (int) $environment->workspace_id]);
|
||||||
|
$outsider = User::factory()->create();
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => (int) $environment->workspace_id,
|
||||||
|
'user_id' => (int) $outsider->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('managed_environment_memberships')->insert([
|
||||||
|
'id' => (string) Str::uuid(),
|
||||||
|
'managed_environment_id' => (int) $otherEnvironment->getKey(),
|
||||||
|
'user_id' => (int) $outsider->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
'source' => 'manual',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
coverageV2ActingAs($outsider, $environment);
|
||||||
|
|
||||||
|
$this->get(CoverageV2Readiness::getUrl(tenant: $environment))
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 403 when environment scope is valid but the capability is denied', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
seedCoverageV2ReadinessScenario($environment);
|
||||||
|
coverageV2ActingAs($user, $environment);
|
||||||
|
|
||||||
|
app()->instance(ManagedEnvironmentAccessScopeResolver::class, new class
|
||||||
|
{
|
||||||
|
public function decision(User $user, ManagedEnvironment $environment, ?string $requiredCapability = null): ManagedEnvironmentAccessDecision
|
||||||
|
{
|
||||||
|
return new ManagedEnvironmentAccessDecision(
|
||||||
|
workspaceId: (int) $environment->workspace_id,
|
||||||
|
managedEnvironmentId: (int) $environment->getKey(),
|
||||||
|
userId: (int) $user->getKey(),
|
||||||
|
workspaceMember: true,
|
||||||
|
workspaceRole: 'owner',
|
||||||
|
explicitScopeRowsPresent: false,
|
||||||
|
managedEnvironmentAllowed: true,
|
||||||
|
failedBoundary: 'capability',
|
||||||
|
requiredCapability: $requiredCapability,
|
||||||
|
capabilityAllowed: false,
|
||||||
|
denialHttpStatus: 403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->get(CoverageV2Readiness::getUrl(tenant: $environment))
|
||||||
|
->assertForbidden();
|
||||||
|
} finally {
|
||||||
|
app()->forgetInstance(ManagedEnvironmentAccessScopeResolver::class);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
it('keeps the Coverage v2 readiness render path free of remote clients and capture actions', function (): void {
|
||||||
|
$files = [
|
||||||
|
app_path('Services/TenantConfiguration/CoverageV2ReadinessReadModel.php'),
|
||||||
|
app_path('Filament/Pages/TenantConfiguration/CoverageV2Readiness.php'),
|
||||||
|
app_path('Filament/Widgets/TenantConfiguration/CoverageV2ResourceTypesTable.php'),
|
||||||
|
app_path('Filament/Widgets/TenantConfiguration/CoverageV2ResourceInstancesTable.php'),
|
||||||
|
resource_path('views/filament/modals/tenant-configuration/coverage-v2-resource-type-inspect.blade.php'),
|
||||||
|
resource_path('views/filament/modals/tenant-configuration/coverage-v2-resource-inspect.blade.php'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$content = collect($files)
|
||||||
|
->map(fn (string $file): string => file_get_contents($file) ?: '')
|
||||||
|
->implode("\n");
|
||||||
|
|
||||||
|
expect($content)
|
||||||
|
->not->toContain('GraphClient')
|
||||||
|
->not->toContain('MicrosoftGraph')
|
||||||
|
->not->toContain('ProviderConnectionResolver')
|
||||||
|
->not->toContain('StartVerification')
|
||||||
|
->not->toContain('StartTenantConfigurationCapture')
|
||||||
|
->not->toContain('TenantConfigurationCaptureJob')
|
||||||
|
->not->toContain('Http::')
|
||||||
|
->not->toContain('tenant_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps Coverage v2 readiness labels canonical and avoids legacy gap taxonomy', function (): void {
|
||||||
|
$files = [
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2ReadinessBadge.php'),
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2CoverageLevelBadge.php'),
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2EvidenceStateBadge.php'),
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2IdentityStateBadge.php'),
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2ClaimStateBadge.php'),
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2SupportStateBadge.php'),
|
||||||
|
app_path('Support/Badges/Domains/CoverageV2SourceClassBadge.php'),
|
||||||
|
app_path('Services/TenantConfiguration/CoverageV2ReadinessReadModel.php'),
|
||||||
|
app_path('Filament/Pages/TenantConfiguration/CoverageV2Readiness.php'),
|
||||||
|
app_path('Filament/Widgets/TenantConfiguration/CoverageV2ResourceTypesTable.php'),
|
||||||
|
app_path('Filament/Widgets/TenantConfiguration/CoverageV2ResourceInstancesTable.php'),
|
||||||
|
resource_path('views/filament/pages/tenant-configuration/coverage-v2-readiness.blade.php'),
|
||||||
|
resource_path('views/filament/modals/tenant-configuration/coverage-v2-resource-type-inspect.blade.php'),
|
||||||
|
resource_path('views/filament/modals/tenant-configuration/coverage-v2-resource-inspect.blade.php'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$content = collect($files)
|
||||||
|
->map(fn (string $file): string => file_get_contents($file) ?: '')
|
||||||
|
->implode("\n");
|
||||||
|
|
||||||
|
expect($content)
|
||||||
|
->toContain('Claim allowed')
|
||||||
|
->toContain('Claim limited')
|
||||||
|
->toContain('Claim blocked')
|
||||||
|
->toContain('Internal only')
|
||||||
|
->not->toContain('Evidence gaps')
|
||||||
|
->not->toContain('Raw gaps')
|
||||||
|
->not->toContain('Primary gaps')
|
||||||
|
->not->toContain('Partially complete')
|
||||||
|
->not->toContain('Incomplete result')
|
||||||
|
->not->toContain('ambiguous_match')
|
||||||
|
->not->toContain('policy_record_missing')
|
||||||
|
->not->toContain('foundation_not_policy_backed')
|
||||||
|
->not->toContain('meta_fallback')
|
||||||
|
->not->toContain('customer-ready')
|
||||||
|
->not->toContain('customer ready');
|
||||||
|
});
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\TenantConfiguration\ClaimState;
|
||||||
|
use App\Support\TenantConfiguration\CoverageLevel;
|
||||||
|
use App\Support\TenantConfiguration\EvidenceState;
|
||||||
|
use App\Support\TenantConfiguration\IdentityState;
|
||||||
|
use App\Support\TenantConfiguration\SourceClass;
|
||||||
|
use App\Support\TenantConfiguration\SupportState;
|
||||||
|
|
||||||
|
it('maps Coverage v2 readiness and diagnostic states through the badge catalog', function (): void {
|
||||||
|
expect(BadgeCatalog::spec(BadgeDomain::CoverageV2Readiness, 'ready')->label)->toBe('Ready')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2Readiness, 'needs_attention')->label)->toBe('Needs attention')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2Readiness, 'blocked')->label)->toBe('Blocked')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2CoverageLevel, CoverageLevel::ContentBacked)->label)->toBe('Content backed')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2EvidenceState, EvidenceState::PermissionBlocked)->label)->toBe('Permission blocked')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2IdentityState, IdentityState::IdentityConflict)->label)->toBe('Identity conflict')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2ClaimState, ClaimState::ClaimAllowed)->label)->toBe('Claim allowed')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2ClaimState, ClaimState::ClaimLimited)->label)->toBe('Claim limited')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2ClaimState, ClaimState::ClaimBlocked)->label)->toBe('Claim blocked')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2ClaimState, ClaimState::InternalOnly)->label)->toBe('Internal only')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2SupportState, SupportState::FallbackSupported)->label)->toBe('Fallback supported')
|
||||||
|
->and(BadgeCatalog::spec(BadgeDomain::CoverageV2SourceClass, SourceClass::GraphV1Fallback)->label)->toBe('Graph v1 fallback');
|
||||||
|
});
|
||||||
@ -6,12 +6,12 @@ ## Summary
|
|||||||
|
|
||||||
| Metric | Count | Notes |
|
| Metric | Count | Notes |
|
||||||
| --- | ---: | --- |
|
| --- | ---: | --- |
|
||||||
| UI route/page inventory rows | 101 | Includes dynamic route families and utility/auth endpoints. |
|
| UI route/page inventory rows | 102 | Includes dynamic route families and utility/auth endpoints. Spec 418 adds the internal Coverage v2 Readiness route. |
|
||||||
| Unique page reports | 22 | `page-reports/*.md`; some inventory rows intentionally share existing reports where routes resolve to the same surface. |
|
| Unique page reports | 22 | `page-reports/*.md`; some inventory rows intentionally share existing reports where routes resolve to the same surface. |
|
||||||
| Desktop screenshots | 22 | Route-inventory-linked desktop evidence, including strategic runtime captures, blocker evidence screenshots, the Spec 366 rendered-report capture, and Spec 396 system-panel focused proof. Spec 397 adds focused textual browser proof without durable screenshots. |
|
| Desktop screenshots | 22 | Route-inventory-linked desktop evidence, including strategic runtime captures, blocker evidence screenshots, the Spec 366 rendered-report capture, and Spec 396 system-panel focused proof. Spec 397 adds focused textual browser proof without durable screenshots. |
|
||||||
| Tablet screenshots | 0 | Deferred to later strategic mockup/implementation specs. |
|
| Tablet screenshots | 0 | Deferred to later strategic mockup/implementation specs. |
|
||||||
| Mobile screenshots | 3 | Spec 366 adds mobile-ish rendered-report evidence for the customer technical profile; Spec 384 adds a narrow baseline subject resolution smoke capture; Spec 386 adds a narrow publication-resolution smoke capture. |
|
| Mobile screenshots | 3 | Spec 366 adds mobile-ish rendered-report evidence for the customer technical profile; Spec 384 adds a narrow baseline subject resolution smoke capture; Spec 386 adds a narrow publication-resolution smoke capture. |
|
||||||
| Strategic Surface rows | 47 | Individual target treatment or explicit product decision required. |
|
| Strategic Surface rows | 48 | Individual target treatment or explicit product decision required. |
|
||||||
| Domain Pattern Surface rows | 45 | Can be handled through grouped pattern specs unless later evidence raises risk. |
|
| Domain Pattern Surface rows | 45 | Can be handled through grouped pattern specs unless later evidence raises risk. |
|
||||||
| Design-System Cleanup Surface rows | 7 | Tables/forms/states/copy cleanup, no individual target mockup expected by default. |
|
| Design-System Cleanup Surface rows | 7 | Tables/forms/states/copy cleanup, no individual target mockup expected by default. |
|
||||||
| Internal / Deprecated / Hidden rows | 1 | Local-only smoke login routes. |
|
| Internal / Deprecated / Hidden rows | 1 | Local-only smoke login routes. |
|
||||||
@ -51,7 +51,7 @@ ## Coverage By Area
|
|||||||
| Platform/system | 14 | Spec 396 adds focused browser proof for `/system`, `/system/login`, `/system/ops/runs`, and `/system/security/access-logs`; remaining directory, control, detail, and repair surfaces stay route-discovered or follow-up. |
|
| Platform/system | 14 | Spec 396 adds focused browser proof for `/system`, `/system/login`, `/system/ops/runs`, and `/system/security/access-logs`; remaining directory, control, detail, and repair surfaces stay route-discovered or follow-up. |
|
||||||
| Governance | 13 | Strong browser coverage for inbox, decisions, exceptions, baselines; Spec 397 adds baseline snapshot detail receipt proof; baseline profile/detail and compare routes remain broader follow-up. |
|
| Governance | 13 | Strong browser coverage for inbox, decisions, exceptions, baselines; Spec 397 adds baseline snapshot detail receipt proof; baseline profile/detail and compare routes remain broader follow-up. |
|
||||||
| Monitoring | 9 | Operations hub and alert delivery landing captured; Spec 399 caps the Operations Hub hot table and removes default raw run identifiers/technical wording while record details and config forms remain pattern/manual review. |
|
| Monitoring | 9 | Operations hub and alert delivery landing captured; Spec 399 caps the Operations Hub hot table and removes default raw run identifiers/technical wording while record details and config forms remain pattern/manual review. |
|
||||||
| Inventory | 8 | Route-discovered only; coverage, policy version detail, and raw-data exposure need later review. |
|
| Inventory | 9 | Route-discovered only; Coverage v2 Readiness is now registered as an internal read-only readiness surface, while policy version detail and raw-data exposure still need later review. |
|
||||||
| Evidence / audit | 8 | Audit log captured; Spec 397 adds receipt-reduction proof for Evidence Snapshot, Baseline Snapshot, and Stored Report detail surfaces while Evidence Overview remains follow-up. |
|
| Evidence / audit | 8 | Audit log captured; Spec 397 adds receipt-reduction proof for Evidence Snapshot, Baseline Snapshot, and Stored Report detail surfaces while Evidence Overview remains follow-up. |
|
||||||
| Reviews | 8 | Review register, customer workspace, review pack detail, rendered-report, and the Spec 386 publication-resolution workflow now have bounded browser evidence; Spec 397 reduces Review Pack receipt internals while deeper evidence/report surfaces still remain open elsewhere. |
|
| Reviews | 8 | Review register, customer workspace, review pack detail, rendered-report, and the Spec 386 publication-resolution workflow now have bounded browser evidence; Spec 397 reduces Review Pack receipt internals while deeper evidence/report surfaces still remain open elsewhere. |
|
||||||
| Backup / restore | 6 | High-risk area; Spec 371 adds seeded browser proof for Backup Sets list/detail. Spec 390 adds Restore create/view readiness guidance; Spec 397 verifies completed Restore Run detail receipt reduction while Restore list and broader failure/conflict browser coverage remain unresolved. |
|
| Backup / restore | 6 | High-risk area; Spec 371 adds seeded browser proof for Backup Sets list/detail. Spec 390 adds Restore create/view readiness guidance; Spec 397 verifies completed Restore Run detail receipt reduction while Restore list and broader failure/conflict browser coverage remain unresolved. |
|
||||||
@ -75,7 +75,7 @@ ## Coverage By Primary Archetype
|
|||||||
| Settings / Admin | 13 | RBAC, entitlement, lifecycle, and dangerous setting changes need confirmation and authorization review. |
|
| Settings / Admin | 13 | RBAC, entitlement, lifecycle, and dangerous setting changes need confirmation and authorization review. |
|
||||||
| Evidence / Audit | 10 | Must keep proof, timestamps, source, and raw details clearly separated. |
|
| Evidence / Audit | 10 | Must keep proof, timestamps, source, and raw details clearly separated. |
|
||||||
| Operations / Monitoring | 9 | Needs consistent run status, retry/rerun semantics, and diagnostic hierarchy. |
|
| Operations / Monitoring | 9 | Needs consistent run status, retry/rerun semantics, and diagnostic hierarchy. |
|
||||||
| Inventory | 8 | Needs raw provider payload disclosure rules and confidence/status language. |
|
| Inventory | 9 | Needs raw provider payload disclosure rules and confidence/status language; Spec 418 adds internal Coverage v2 readiness diagnostics without raw payload display. |
|
||||||
| Drift / Diff | 9 | Needs assignment, comparison, subject-resolution, snapshot, and evidence-gap hierarchy. |
|
| Drift / Diff | 9 | Needs assignment, comparison, subject-resolution, snapshot, and evidence-gap hierarchy. |
|
||||||
| Provider / Integration | 7 | Consent, credentials, permissions, and disconnect states require high trust clarity. |
|
| Provider / Integration | 7 | Consent, credentials, permissions, and disconnect states require high trust clarity. |
|
||||||
| Reviews | 8 | Customer/auditor language, export context, proof links, and source-owned publication resolution are central. |
|
| Reviews | 8 | Customer/auditor language, export context, proof links, and source-owned publication resolution are central. |
|
||||||
@ -93,7 +93,7 @@ ## Coverage By Design Depth
|
|||||||
|
|
||||||
| Design Depth | Rows | Gate Treatment |
|
| Design Depth | Rows | Gate Treatment |
|
||||||
| --- | ---: | --- |
|
| --- | ---: | --- |
|
||||||
| Strategic Surface | 47 | Requires individual target artifact or explicit product decision before substantive UI implementation. |
|
| Strategic Surface | 48 | Requires individual target artifact or explicit product decision before substantive UI implementation. |
|
||||||
| Domain Pattern Surface | 45 | Can be handled by grouped pattern specs and shared components. |
|
| Domain Pattern Surface | 45 | Can be handled by grouped pattern specs and shared components. |
|
||||||
| Design-System Cleanup Surface | 7 | Table/form/action/state cleanup can be folded into implementation waves. |
|
| Design-System Cleanup Surface | 7 | Table/form/action/state cleanup can be folded into implementation waves. |
|
||||||
| Manual Review Required | 1 | Must not be treated as product-ready until route/auth state is confirmed. |
|
| Manual Review Required | 1 | Must not be treated as product-ready until route/auth state is confirmed. |
|
||||||
|
|||||||
@ -72,6 +72,7 @@ # Route Inventory
|
|||||||
| UI-100 | `/admin/workspaces/{workspace}/environments/{environment}/baseline-subject-resolution` | page | Baseline Subject Resolution | Governance | environment-bound | browser-verified | workspace + environment entitlement; view requires baseline view capability, mutations require baseline manage capability | Drift / Diff | Evidence / Audit | Strategic Surface | browser-verified | [desktop](../../specs/384-baseline-subject-resolution-ui/artifacts/screenshots/spec384-01-baseline-subject-resolution.png) | [report](page-reports/ui-100-baseline-subject-resolution.md) | Focused operator worklist for persisted baseline subject identity and coverage decisions; reachable from Baseline Compare and Operation detail only when actionable outcomes exist. |
|
| UI-100 | `/admin/workspaces/{workspace}/environments/{environment}/baseline-subject-resolution` | page | Baseline Subject Resolution | Governance | environment-bound | browser-verified | workspace + environment entitlement; view requires baseline view capability, mutations require baseline manage capability | Drift / Diff | Evidence / Audit | Strategic Surface | browser-verified | [desktop](../../specs/384-baseline-subject-resolution-ui/artifacts/screenshots/spec384-01-baseline-subject-resolution.png) | [report](page-reports/ui-100-baseline-subject-resolution.md) | Focused operator worklist for persisted baseline subject identity and coverage decisions; reachable from Baseline Compare and Operation detail only when actionable outcomes exist. |
|
||||||
| UI-062 | `/admin/workspaces/{workspace}/environments/{environment}/inventory` | cluster | Inventory Cluster | Inventory | environment-bound | route exists | environment entitlement | Inventory | Workspace / Tenant Context | Domain Pattern Surface | repo-verified | - | - | Cluster landing/navigation surface. |
|
| UI-062 | `/admin/workspaces/{workspace}/environments/{environment}/inventory` | cluster | Inventory Cluster | Inventory | environment-bound | route exists | environment entitlement | Inventory | Workspace / Tenant Context | Domain Pattern Surface | repo-verified | - | - | Cluster landing/navigation surface. |
|
||||||
| UI-063 | `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` | page | Inventory Coverage | Inventory | environment-bound | route exists | environment entitlement | Inventory | Evidence / Audit | Strategic Surface | repo-verified | - | - | Coverage truth page; strategic because it gates evidence confidence. |
|
| UI-063 | `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` | page | Inventory Coverage | Inventory | environment-bound | route exists | environment entitlement | Inventory | Evidence / Audit | Strategic Surface | repo-verified | - | - | Coverage truth page; strategic because it gates evidence confidence. |
|
||||||
|
| UI-102 | `/admin/workspaces/{workspace}/environments/{environment}/tenant-configuration/coverage-v2` | page | Coverage v2 Readiness | Inventory | environment-bound | route exists | workspace + environment entitlement plus `evidence.view` capability | Inventory | Provider / Integration | Strategic Surface | repo-verified | - | - | Spec 418 internal read-only Coverage v2 readiness surface; no customer output, no raw payloads, no mutation actions, primary-link Inspect disclosure model, and DB-only render path. |
|
||||||
| UI-064 | `/admin/workspaces/{workspace}/environments/{environment}/inventory-items` | resource | Inventory Items | Inventory | environment-bound | route exists | environment entitlement | Inventory | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Core observed-state list. |
|
| UI-064 | `/admin/workspaces/{workspace}/environments/{environment}/inventory-items` | resource | Inventory Items | Inventory | environment-bound | route exists | environment entitlement | Inventory | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Core observed-state list. |
|
||||||
| UI-065 | `/admin/workspaces/{workspace}/environments/{environment}/inventory-items/{record}` | resource | Inventory Item Detail | Inventory | environment record | route exists | environment + record entitlement | Inventory | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Detail report should distinguish raw provider payload from decision content. |
|
| UI-065 | `/admin/workspaces/{workspace}/environments/{environment}/inventory-items/{record}` | resource | Inventory Item Detail | Inventory | environment record | route exists | environment + record entitlement | Inventory | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Detail report should distinguish raw provider payload from decision content. |
|
||||||
| UI-066 | `/admin/workspaces/{workspace}/environments/{environment}/policies` | resource | Policies | Inventory | environment-bound | route exists | environment entitlement | Inventory | Drift / Diff | Domain Pattern Surface | repo-verified | - | - | Intune policy inventory list. |
|
| UI-066 | `/admin/workspaces/{workspace}/environments/{environment}/policies` | resource | Policies | Inventory | environment-bound | route exists | environment entitlement | Inventory | Drift / Diff | Domain Pattern Surface | repo-verified | - | - | Intune policy inventory list. |
|
||||||
|
|||||||
@ -0,0 +1,118 @@
|
|||||||
|
# Requirements Checklist: Spec 418 - Coverage v2 Operator Surface
|
||||||
|
|
||||||
|
## Candidate And Dependencies
|
||||||
|
|
||||||
|
- [x] Candidate is user-provided, not auto-selected from an empty active candidate queue.
|
||||||
|
- [x] Spec 414 is completed/validated dependency context only.
|
||||||
|
- [x] Spec 415 is completed/validated dependency context only.
|
||||||
|
- [x] Spec 417 is completed/validated dependency context only.
|
||||||
|
- [x] No existing `418-coverage-v2-operator-surface` spec directory was found before creation.
|
||||||
|
- [x] Scope is limited to one internal operator readiness surface.
|
||||||
|
- [x] No application implementation was performed during preparation.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- [x] Spec 418 depends on Coverage v2 kernel/capture/identity.
|
||||||
|
- [x] Spec 418 adds one operator-only read surface.
|
||||||
|
- [x] Spec 418 does not activate customer-facing Coverage v2 truth.
|
||||||
|
- [x] Spec 418 does not convert Evidence Overview, Review Packs, Reports, Restore, Baseline Compare, or Customer Review Workspace.
|
||||||
|
- [x] Spec 418 does not add capture/start actions.
|
||||||
|
- [x] Deferred Coverage v2 cutover/removal and customer activation are listed as follow-up work.
|
||||||
|
|
||||||
|
## Product Surface
|
||||||
|
|
||||||
|
- [x] Product Surface Impact is declared.
|
||||||
|
- [x] Surface is Secondary Context Surface.
|
||||||
|
- [x] Surface is Read-only Registry / Report Surface.
|
||||||
|
- [x] Surface is Native Surface unless implementation documents an approved exception.
|
||||||
|
- [x] Inspect/open model uses a linked primary column instead of a duplicate View/Inspect row action.
|
||||||
|
- [x] Primary operator question is explicit.
|
||||||
|
- [x] Default-visible truth is explicit.
|
||||||
|
- [x] Diagnostics are secondary/disclosed.
|
||||||
|
- [x] Raw/support evidence is hidden.
|
||||||
|
- [x] Browser proof is required.
|
||||||
|
- [x] Product Surface table-count exception is documented and internal-only.
|
||||||
|
- [x] Product Surface table-count exception is classified as a PSC Technical Annex surface-budget exception, with UI-EX-001 remaining `none` for native Filament implementation.
|
||||||
|
- [x] Human Product Sanity questions are explicit.
|
||||||
|
- [x] `docs/product/standards/list-surface-review-checklist.md` is required for implementation close-out.
|
||||||
|
|
||||||
|
## Ownership / RBAC
|
||||||
|
|
||||||
|
- [x] No `tenant_id` internal ownership.
|
||||||
|
- [x] Surface scopes by workspace and managed environment.
|
||||||
|
- [x] Provider connection filters are same-scope.
|
||||||
|
- [x] Non-member gets 404.
|
||||||
|
- [x] No environment entitlement gets 404.
|
||||||
|
- [x] Member without capability gets 403.
|
||||||
|
- [x] Authorized actor can view.
|
||||||
|
- [x] Workspace-wide aggregation, if implemented, is limited to entitled environments.
|
||||||
|
|
||||||
|
## Data / Render
|
||||||
|
|
||||||
|
- [x] Page render is DB-only.
|
||||||
|
- [x] No Graph/TCM/provider calls during render.
|
||||||
|
- [x] No capture action.
|
||||||
|
- [x] No remote calls in table columns, badges, filters, or diagnostics.
|
||||||
|
- [x] No persisted UI-only summary table unless the spec is amended with proportionality proof.
|
||||||
|
- [x] Narrow indexes are allowed only with documented query path.
|
||||||
|
- [x] Top activation blocker ordering is deterministic.
|
||||||
|
|
||||||
|
## Vocabulary
|
||||||
|
|
||||||
|
- [x] Shows Coverage level.
|
||||||
|
- [x] Shows Evidence state.
|
||||||
|
- [x] Shows Identity state.
|
||||||
|
- [x] Shows Claim state.
|
||||||
|
- [x] Shows Source class.
|
||||||
|
- [x] Shows Supported scope.
|
||||||
|
- [x] Status-like rendered values use `BadgeCatalog`/`BadgeRenderer` or a central BadgeDomain mapping.
|
||||||
|
- [x] Does not show Evidence gaps.
|
||||||
|
- [x] Does not show Raw gaps.
|
||||||
|
- [x] Does not show Primary gaps.
|
||||||
|
- [x] Does not show policy_record_missing.
|
||||||
|
- [x] Does not show foundation_not_policy_backed.
|
||||||
|
- [x] Does not show meta_fallback.
|
||||||
|
- [x] Does not show ambiguous_match.
|
||||||
|
- [x] Does not show old v1 gap reason codes as active UI truth.
|
||||||
|
|
||||||
|
## Claim Safety
|
||||||
|
|
||||||
|
- [x] No unscoped 100% claim.
|
||||||
|
- [x] No broad Microsoft 365 coverage claim.
|
||||||
|
- [x] No certified claim unless exact internal guard allows and the label remains internal.
|
||||||
|
- [x] No restore-ready claim.
|
||||||
|
- [x] No customer-ready proof claim.
|
||||||
|
- [x] Claim state labels are internal/operator-facing.
|
||||||
|
|
||||||
|
## Redaction
|
||||||
|
|
||||||
|
- [x] Raw payload hidden.
|
||||||
|
- [x] Normalized payload hidden by default.
|
||||||
|
- [x] Permission context raw JSON hidden.
|
||||||
|
- [x] Tokens, secrets, authorization headers, cookies, private keys, certificates, raw provider responses, stack traces, and PII absent.
|
||||||
|
- [x] OperationRun diagnostics are secondary and authorized.
|
||||||
|
- [x] Evidence hash is allowed if safe.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- [x] Unit tests cover read model, summary, blockers, display mapping, and no-old-label emissions.
|
||||||
|
- [x] Feature tests cover authorization, render, redaction, no-legacy, no-remote, OperationRun links, and provider scope.
|
||||||
|
- [x] Browser smoke covers rendered UI.
|
||||||
|
- [x] No real Graph/TCM/provider calls are allowed.
|
||||||
|
- [x] Test lane impact is documented.
|
||||||
|
|
||||||
|
## Spec Readiness Gate
|
||||||
|
|
||||||
|
- [x] `spec.md` exists.
|
||||||
|
- [x] `plan.md` exists.
|
||||||
|
- [x] `tasks.md` exists.
|
||||||
|
- [x] Requirements are bounded and testable.
|
||||||
|
- [x] Plan identifies likely affected repo surfaces.
|
||||||
|
- [x] Tasks are ordered, small, verifiable, and include validation.
|
||||||
|
- [x] Product Surface, RBAC, workspace/provider isolation, OperationRun, evidence, provider boundary, no-legacy, and test governance are addressed.
|
||||||
|
- [x] No open question blocks safe implementation.
|
||||||
|
|
||||||
|
## Gate Results
|
||||||
|
|
||||||
|
- [x] Candidate Selection Gate: PASS.
|
||||||
|
- [x] Spec Readiness Gate: PASS for preparation; implementation must still follow `tasks.md`.
|
||||||
168
specs/418-coverage-v2-operator-surface/implementation-report.md
Normal file
168
specs/418-coverage-v2-operator-surface/implementation-report.md
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Implementation Report: Spec 418 - Coverage v2 Operator Surface
|
||||||
|
|
||||||
|
**Date**: 2026-06-26
|
||||||
|
**Branch**: `418-coverage-v2-operator-surface`
|
||||||
|
**Base HEAD**: `8cbf1f7f feat: implement canonical identity engine (#484)`
|
||||||
|
**Initial dirty state**: active spec directory `specs/418-coverage-v2-operator-surface/` was untracked; no unrelated dirty runtime files were present.
|
||||||
|
**Final dirty state**: implementation files, tests, UI audit docs, tasks, and this report are dirty/untracked for this feature package.
|
||||||
|
|
||||||
|
## Gates
|
||||||
|
|
||||||
|
- **Activated skills / gates**: `spec-kit-implementation-loop`, `spec-readiness-gate`, `workspace-scope-safety`, `rbac-action-safety`, `operation-run-truth`, `evidence-anchor-contract`, `provider-freshness-semantics`, `product-surface-gate`, `filament-livewire-v5-change-loop`, `tcm-cutover-guard`, `browser-readonly-audit`, `pest-testing`, `browsertest`.
|
||||||
|
- **Hard-gate result**: PASS. No stop condition was hit.
|
||||||
|
- **Dependency reports**: present and treated as read-only context only:
|
||||||
|
- `specs/414-tcm-first-coverage-core-cutover/implementation-report.md`
|
||||||
|
- `specs/415-generic-content-backed-capture/implementation-report.md`
|
||||||
|
- `specs/417-canonical-identity-engine/implementation-report.md`
|
||||||
|
- **Historical specs**: no completed historical spec was rewritten or stripped of validation, task, smoke, browser, or review history.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Added a DB-only Coverage v2 readiness read model, central badge mappings, and a native Filament operator surface.
|
||||||
|
|
||||||
|
- Route: repo-equivalent internal route `/admin/workspaces/{workspace}/environments/{environment}/tenant-configuration/coverage-v2`.
|
||||||
|
- Page: `apps/platform/app/Filament/Pages/TenantConfiguration/CoverageV2Readiness.php`.
|
||||||
|
- Tables/widgets: native `TableWidget` resource type registry and environment-scoped resource instance table.
|
||||||
|
- Detail model: linked primary columns open one read-only `Inspect` slide-over model for resource types and resource instances; no separate row action column.
|
||||||
|
- Productization follow-up: readiness summary now exposes one explicit reason and one next step; secondary technical table columns are available through native Filament column toggles instead of default-visible density.
|
||||||
|
- Navigation: secondary Inventory entry `Coverage v2`; does not replace Evidence Overview, Baseline Compare, Customer Review Workspace, Review Packs, Reports, or Restore surfaces.
|
||||||
|
- Read model: `apps/platform/app/Services/TenantConfiguration/CoverageV2ReadinessReadModel.php`.
|
||||||
|
- No migration, no persisted summary, no fallback reader, no v1 adapter, no `tenant_id` ownership.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
- Runtime: `AdminPanelProvider`, Coverage v2 page/widgets/read model, Blade page/modal.
|
||||||
|
- Badges: `BadgeDomain`, `BadgeCatalog`, and Coverage v2 badge mappers for readiness, coverage, evidence, identity, claim, support, and source class.
|
||||||
|
- Tests: one unit badge test, two feature test files, one browser smoke.
|
||||||
|
- Product audit: `docs/ui-ux-enterprise-audit/route-inventory.md`, `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`.
|
||||||
|
- Spec close-out: `tasks.md`, `implementation-report.md`.
|
||||||
|
|
||||||
|
## Product Surface
|
||||||
|
|
||||||
|
- **No-legacy posture**: canonical Coverage v2 internal readiness surface; no compatibility exception.
|
||||||
|
- **Product Surface Impact**: new internal operator page, navigation entry, two native read-only tables, one primary-link read-only inspect slide-over model.
|
||||||
|
- **UI Surface Impact**: route inventory updated as `UI-102`; design coverage matrix counts updated.
|
||||||
|
- **Page archetype**: Technical Annex Page / Read-only Registry Report.
|
||||||
|
- **Surface budget**: approved Product Surface Contract Technical Annex exception for summary plus two native tables. The two-table view is required to compare registry denominator truth with concrete environment evidence.
|
||||||
|
- **UI-EX-001**: none. Implementation stayed native Filament.
|
||||||
|
- **Canonical status vocabulary**: readiness uses `Ready`, `Needs attention`, `Blocked`, `Unknown`; Coverage v2 diagnostic dimensions use internal labels such as `Claim allowed`, `Claim limited`, `Claim blocked`, `Internal only`.
|
||||||
|
- **Technical Annex / deep-link demotion**: OperationRun links, evidence hash, source contract state, provider provenance, identity reason code, and source class are secondary diagnostics. Raw payloads and raw provider responses are not rendered.
|
||||||
|
- **Product Surface exceptions**: PSC Technical Annex surface-budget exception only.
|
||||||
|
- **List surface review**: PASS. Tables have scoped empty states, primary-link inspect columns instead of duplicate row/view actions, no bulk actions, no destructive actions, and diagnostics are disclosed through one inspect slide-over model.
|
||||||
|
- **Visible complexity outcome**: reduced for operators by replacing scattered DB/test/report inspection with one bounded read-only surface, adding explicit readiness reason/next-step text, and demoting secondary technical columns from the default table view through native Filament column toggles.
|
||||||
|
|
||||||
|
## UI Action Matrix
|
||||||
|
|
||||||
|
| Slot | Result |
|
||||||
|
|---|---|
|
||||||
|
| Header actions | none |
|
||||||
|
| Row primary action | linked primary columns open the read-only `Inspect` slide-over model for resource types and resource instances |
|
||||||
|
| Row URL | none; primary link columns are used because full-row click conflicts with dense comparison tables |
|
||||||
|
| More menu | none |
|
||||||
|
| Bulk actions | none |
|
||||||
|
| Destructive/high-impact actions | none |
|
||||||
|
| Remote/capture/sync/restore/export/publish actions | none |
|
||||||
|
| OperationRun link | secondary diagnostic link only when `Gate::allows('view', $run)` |
|
||||||
|
|
||||||
|
## Authorization And Scope
|
||||||
|
|
||||||
|
- Uses `Capabilities::EVIDENCE_VIEW`; no new capability was required.
|
||||||
|
- Workspace/non-member and environment-entitlement failures return 404 through existing scope helpers.
|
||||||
|
- Capability denial returns 403.
|
||||||
|
- Instance query is scoped by `workspace_id` and `managed_environment_id`.
|
||||||
|
- Provider connection filter options are scoped to the same workspace and managed environment.
|
||||||
|
- No workspace-wide aggregation was implemented.
|
||||||
|
|
||||||
|
## Redaction And Safety
|
||||||
|
|
||||||
|
- Raw payload, normalized payload, permission context JSON, secrets, tokens, raw provider responses, exception dumps, and stack traces are excluded from selected columns and rendered views.
|
||||||
|
- Read model selects safe latest-evidence fields only.
|
||||||
|
- Old labels and reason codes are not active UI truth: `Evidence gaps`, `Raw gaps`, `Primary gaps`, `ambiguous_match`, `policy_record_missing`, `foundation_not_policy_backed`, `meta_fallback`.
|
||||||
|
- Static guard confirmed the render path does not register Graph/TCM/provider clients or capture/start actions.
|
||||||
|
- No destructive action was added.
|
||||||
|
|
||||||
|
## Browser Proof
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: PASS, `1 passed`, `42 assertions`, duration `4.95s`.
|
||||||
|
|
||||||
|
Proof covered: authorized route load, Livewire presence, no JavaScript errors, no console logs, readiness labels, explicit reason and next step, resource type/instance tables, inspect slide-over, authorized OperationRun diagnostic link, provider provenance, identity reason code, source schema hash, and absence of raw secrets/customer-ready wording.
|
||||||
|
|
||||||
|
Integrated Browser follow-up smoke:
|
||||||
|
|
||||||
|
- Result: PASS after applying pending local migrations for Specs 414/415/417.
|
||||||
|
- Route: `/admin/workspaces/3/environments/3/tenant-configuration/coverage-v2`.
|
||||||
|
- Context: authenticated admin browser session, workspace `wp`, managed environment `YPTW2`.
|
||||||
|
- Steps: reloaded route, verified readiness summary and full status labels, created a temporary same-scope Coverage v2 resource/evidence fixture, opened the resource instance inspect slide-over, verified provider provenance, evidence hash, source schema hash, and OperationRun diagnostic link, then removed the temporary fixture.
|
||||||
|
- Safety checks: no JavaScript console warnings/errors, no 500/SQLSTATE output, no Graph/TCM/provider-remote resource requests during render, no raw/normalized payload, permission context, token/secret sentinel, legacy v1 gap label, or customer-ready/certified wording in the page or inspect dialog.
|
||||||
|
- Clean-up: temporary local smoke resource, evidence row, and OperationRun were deleted; final reload returned to the empty resource-instance state without errors.
|
||||||
|
|
||||||
|
Screenshot artifact:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/tests/Browser/Screenshots/spec418-coverage-v2-operator-surface-readiness.png
|
||||||
|
```
|
||||||
|
|
||||||
|
## Human Product Sanity
|
||||||
|
|
||||||
|
- Can an operator understand readiness? PASS: summary status, reason, next step, counts, and top blockers are visible first.
|
||||||
|
- Are blockers grouped by actionable v2 states? PASS: identity, claim, evidence, source, and beta/fallback blockers are grouped deterministically.
|
||||||
|
- Does the page avoid technical object hub behavior? PASS: secondary navigation, bounded internal route, no mutation actions, and secondary technical table columns are demoted through native column toggles.
|
||||||
|
- Are raw/support details hidden by default? PASS: raw evidence fields are neither selected nor rendered.
|
||||||
|
- Is there exactly one inspect model? PASS: one read-only slide-over model reached from primary link columns; no row URL/action-column/bulk/menu duplication.
|
||||||
|
- Are old gap labels absent? PASS: feature/browser/static guard tests assert absence.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail bin pint app/Filament/Pages/TenantConfiguration/CoverageV2Readiness.php app/Filament/Widgets/TenantConfiguration/CoverageV2ResourceTypesTable.php app/Filament/Widgets/TenantConfiguration/CoverageV2ResourceInstancesTable.php app/Services/TenantConfiguration/CoverageV2ReadinessReadModel.php tests/Feature/TenantConfiguration/CoverageV2ReadinessGuardTest.php tests/Feature/Filament/CoverageV2ReadinessPageTest.php tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php tests/Unit/TenantConfiguration/CoverageV2ReadinessBadgeTest.php --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: PASS, fixed import/spacing in `CoverageV2ResourceInstancesTable.php`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/TenantConfiguration/CoverageV2ReadinessBadgeTest.php tests/Feature/TenantConfiguration/CoverageV2ReadinessGuardTest.php tests/Feature/Filament/CoverageV2ReadinessPageTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: PASS, `13 passed`, `155 assertions`, duration `6.52s`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: PASS, `1 passed`, `42 assertions`, duration `4.95s`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --filter=ActionSurface
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: Coverage v2 Action Surface guard PASS; full filtered run FAILS on four pre-existing non-Spec-418 failures (`FindingResource` primary drilldown, Operations URL nav context, Required Permissions copy, Provider Connection required-permissions action).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: PASS.
|
||||||
|
|
||||||
|
Static guard sweep: PASS. Expected raw-payload terms appear only as negative test fixtures/assertions, not runtime render code.
|
||||||
|
|
||||||
|
PostgreSQL lane: N/A. No migrations, indexes, constraints, or query-shape persistence changes were added.
|
||||||
|
|
||||||
|
## Filament / Livewire / Deployment
|
||||||
|
|
||||||
|
- **Livewire v4.0+ compliance**: PASS. Existing app uses Livewire v4; no Livewire v3 APIs introduced.
|
||||||
|
- **Provider registration location**: unchanged. Laravel provider registration remains in `apps/platform/bootstrap/providers.php`; page registration was added to `apps/platform/app/Providers/Filament/AdminPanelProvider.php`.
|
||||||
|
- **Global search**: N/A. No Filament Resource was added; no global-searchable resource exists for this surface.
|
||||||
|
- **Destructive actions**: none. The only registered action is read-only inspect behind primary link columns and does not mutate data.
|
||||||
|
- **Asset strategy**: no new assets; no additional `filament:assets` deployment requirement beyond existing deployment process.
|
||||||
|
- **Runtime impact**: no env vars, no queues, no scheduler, no storage/volume changes, no migrations.
|
||||||
|
- **Dokploy/Staging impact**: deploy code only; validate page on staging before any future customer/cutover activation work.
|
||||||
|
|
||||||
|
## Deferred Work
|
||||||
|
|
||||||
|
Customer-facing Coverage v2 proof, Evidence Overview conversion, Baseline Compare conversion, Review Pack/report output, Restore Readiness conversion, certification, capture/start actions, and legacy cutover/removal remain out of scope for later specs.
|
||||||
239
specs/418-coverage-v2-operator-surface/plan.md
Normal file
239
specs/418-coverage-v2-operator-surface/plan.md
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# Implementation Plan: Spec 418 - Coverage v2 Operator Surface
|
||||||
|
|
||||||
|
**Branch**: `418-coverage-v2-operator-surface` | **Date**: 2026-06-26 | **Spec**: `specs/418-coverage-v2-operator-surface/spec.md`
|
||||||
|
**Input**: Feature specification from `/specs/418-coverage-v2-operator-surface/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add one internal Coverage v2 Readiness surface for operators to inspect activation readiness without activating customer-facing proof. The implementation should build a thin DB-only read model over existing Coverage v2 resource types, supported scopes, resources, evidence, identity state, provider connection provenance, and Claim Guard state; then render it with Filament-native summary, tables, filters, badges, and disclosed diagnostics. It must remain read-only, no-legacy, no-raw-payload, no-remote-render, workspace/environment/provider-scoped, and browser-proven.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.x, Laravel 12.x
|
||||||
|
**Primary Dependencies**: Filament v5, Livewire v4, existing TenantConfiguration models/services, existing RBAC/capability helpers, existing OperationRun link helper
|
||||||
|
**Storage**: PostgreSQL via existing Coverage v2 tables. No new persisted summary table by default.
|
||||||
|
**Testing**: Pest 4 / PHPUnit 12 via Sail
|
||||||
|
**Validation Lanes**: fast-feedback, confidence, browser; pgsql only if indexes/constraints/migrations change
|
||||||
|
**Target Platform**: Laravel Sail locally, Dokploy/container deployment for staging/production
|
||||||
|
**Project Type**: Laravel monolith under `apps/platform`
|
||||||
|
**Performance Goals**: page render is DB-only, bounded queries, no remote calls, paginated/filterable tables
|
||||||
|
**Constraints**: read-only; no Graph/TCM/provider calls during render; no customer proof; no raw payloads; no v1/v2 adapter; no `tenant_id` ownership; no start/capture/restore/report actions
|
||||||
|
**Scale/Scope**: initial Coverage v2 registry and captured resources from Specs 414, 415, and 417; future customer/cutover work is separate
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: new rendered operator surface.
|
||||||
|
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: Coverage v2 Readiness page at `/admin/tenant-configuration/coverage-v2` or repo-equivalent; admin panel page registration/navigation; summary/widgets/tables/diagnostics; workspace/environment/provider filter state.
|
||||||
|
- **No-impact class, if applicable**: N/A.
|
||||||
|
- **Native vs custom classification summary**: Native Surface. Use Filament-native page/table/widget/infolist/section/action slide-over primitives where possible.
|
||||||
|
- **Shared-family relevance**: navigation, status messaging, evidence diagnostics, OperationRun links, provider provenance.
|
||||||
|
- **State layers in scope**: page, query/filter, table, read-only inspect/disclosure detail. No shell rewrite.
|
||||||
|
- **Audience modes in scope**: operator/MSP and support/platform diagnostics. Customer/read-only output is explicitly out of scope.
|
||||||
|
- **Decision/diagnostic/raw hierarchy plan**: readiness summary and critical v2 states visible first; diagnostics disclosed second; raw/support evidence hidden and not added unless existing safe route already exists.
|
||||||
|
- **Raw/support gating plan**: raw payloads, normalized payloads, permission context raw JSON, secrets, and provider response dumps are never displayed.
|
||||||
|
- **One-primary-action / duplicate-truth control**: one dominant action is inspect/open read-only detail. No export/capture/start/publish action. Avoid duplicate blocker summaries.
|
||||||
|
- **Handling modes by drift class or surface**: hard-stop if customer output, remote work, raw payload display, v1 adapter, old labels, or mutations appear.
|
||||||
|
- **Repository-signal treatment**: review-mandatory for route/navigation/UI registry updates; exception-required for Product Surface two-table Technical Annex budget.
|
||||||
|
- **Special surface test profiles**: standard-native-filament plus focused browser smoke.
|
||||||
|
- **Required tests or manual smoke**: unit read model/mappers, feature auth/redaction/no-legacy/no-remote/scope, browser smoke.
|
||||||
|
- **Exception path and spread control**: one Product Surface exception for two native tables on an internal Technical Annex page. No broad UI framework exception.
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||||
|
- **UI/Productization coverage decision**: reachable UI added. Update `docs/ui-ux-enterprise-audit/route-inventory.md` and `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`, or document a bounded registry exception.
|
||||||
|
- **Coverage artifacts to update**: route inventory and design coverage matrix expected.
|
||||||
|
- **List surface review**: apply `docs/product/standards/list-surface-review-checklist.md` for the new Read-only Registry / Report tables and record the result in the implementation report.
|
||||||
|
- **No-impact rationale**: N/A.
|
||||||
|
- **Navigation / Filament provider-panel handling**: register the page in the admin panel; any navigation entry must be secondary and must not dominate primary navigation. Provider registration remains `apps/platform/bootstrap/providers.php`.
|
||||||
|
- **Screenshot or page-report need**: no full page report required unless implementation adds custom layout or deviates from native Filament. Focused browser proof is required.
|
||||||
|
|
||||||
|
## Product Surface Contract Plan
|
||||||
|
|
||||||
|
- **Product Surface Contract reference**: `docs/product/standards/product-surface-contract.md`.
|
||||||
|
- **No-legacy posture**: canonical Coverage v2 internal readiness; no compatibility exception.
|
||||||
|
- **Page archetype and surface budget plan**: Technical Annex Page / Read-only Registry Report. Surface budget exception for two native tables is documented because registry support and concrete instance evidence must be visible together for internal cutover readiness.
|
||||||
|
- **Technical Annex and deep-link demotion plan**: OperationRun links, evidence hashes, source metadata, source contract state, identity diagnostics, and provider provenance are secondary diagnostics. Raw payloads and raw provider output forbidden.
|
||||||
|
- **Canonical status vocabulary plan**: top-level readiness uses `Ready`, `Needs attention`, `Blocked`, `Unknown`; existing Coverage v2 enum values display as internal diagnostic dimensions with internal/operator labels.
|
||||||
|
- **Product Surface exceptions**: one Product Surface Contract Technical Annex surface-budget exception for summary plus two native tables; no customer-facing exception. UI-EX-001 implementation exception is `none` if the surface remains native Filament. If custom UI becomes necessary, implementation must stop and name a catalogued UI-EX-001 exception type before proceeding.
|
||||||
|
- **Browser verification plan**: focused route smoke, visible required labels, hidden forbidden labels/raw payloads, primary-link inspect slide-over, no console/Livewire/Filament errors, and no remote network calls.
|
||||||
|
- **Human Product Sanity plan**: implementation report answers the six Spec 418 questions.
|
||||||
|
- **Visible complexity outcome target**: neutral to reduced.
|
||||||
|
- **Implementation report target**: `specs/418-coverage-v2-operator-surface/implementation-report.md`.
|
||||||
|
|
||||||
|
## Filament / Livewire / Deployment Posture
|
||||||
|
|
||||||
|
- **Livewire v4 compliance**: Livewire v4.x confirmed. Do not introduce Livewire v3 APIs.
|
||||||
|
- **Panel provider registration location**: Laravel 12 panel providers remain registered in `apps/platform/bootstrap/providers.php`; page registration belongs in `apps/platform/app/Providers/Filament/AdminPanelProvider.php` or repo-equivalent admin panel page discovery.
|
||||||
|
- **Global search posture**: no Filament Resource should be globally searchable for this surface. If a Resource is created instead of a Page, global search must be disabled unless safe View/Edit posture exists.
|
||||||
|
- **Destructive/high-impact action posture**: none. If implementation wants any start/capture/re-evaluate/export/publish/restore action, stop and amend spec/plan/tasks or split a new spec.
|
||||||
|
- **Asset strategy**: no new assets expected; `filament:assets` not required unless implementation registers Filament assets.
|
||||||
|
- **Testing plan**: Livewire/feature tests for page authorization/render if page is a Livewire component; unit tests for read model and display mapping; browser smoke for rendered route.
|
||||||
|
- **Deployment impact**: no env vars, queues, scheduler, storage, or assets by default. Migrations only if narrow indexes are added. No queue worker change because no operations start.
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes.
|
||||||
|
- **Systems touched**: `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, `TenantConfigurationResource`, `TenantConfigurationResourceEvidence`, `ClaimGuard`, `OperationRunLinks`, ProviderConnection scope, admin panel registration, UI audit registry, tests.
|
||||||
|
- **Shared abstractions reused**: existing Coverage v2 models/enums/services; existing OperationRun URL helper; existing workspace/environment/provider scope helpers; existing RBAC capability resolver/policies; `BadgeCatalog`/`BadgeRenderer` for status-like badges.
|
||||||
|
- **New abstraction introduced? why?**: one thin read model/query service and possibly a non-status display mapper. It is needed to centralize DB-only aggregate logic, blocker grouping, redaction, and no-legacy text display for tests. Status-like rendered badges must use a central BadgeDomain mapping, not page-local status semantics.
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: Existing kernel/capture/identity services own truth but do not provide an operator-ready read model. Existing Evidence Overview/Inventory Coverage pages are v1/customer-adjacent surfaces and must not become v2 dual truth.
|
||||||
|
- **Bounded deviation / spread control**: no new persisted read model, no UI framework, no new state family, no customer output. Keep new support under TenantConfiguration or Filament page-local namespaces.
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: diagnostic links only.
|
||||||
|
- **Central contract reused**: `OperationRunLinks` or current canonical helper.
|
||||||
|
- **Delegated UX behaviors**: URL resolution and authorization only; no queued toast, artifact link, run-enqueued browser event, queued DB notification, or dedupe messaging.
|
||||||
|
- **Surface-owned behavior kept local**: secondary diagnostic link visibility only.
|
||||||
|
- **Queued DB-notification policy**: N/A.
|
||||||
|
- **Terminal notification path**: N/A.
|
||||||
|
- **Exception path**: none.
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes.
|
||||||
|
- **Provider-owned seams**: provider connection provenance, source contract metadata, Graph fallback, beta experimental source class, external provider identifiers if ever disclosed.
|
||||||
|
- **Platform-core seams**: workspace/managed-environment/provider_connection ownership, resource type, supported scope, readiness summary, coverage/evidence/identity/claim states.
|
||||||
|
- **Neutral platform terms / contracts preserved**: provider connection, managed environment, supported scope, source class, activation blocker, readiness.
|
||||||
|
- **Retained provider-specific semantics and why**: Graph/TCM labels appear only as source class or safe metadata because the initial resource types are Intune-backed.
|
||||||
|
- **Bounded extraction or follow-up path**: document-in-feature if any provider-specific display is retained; no new provider framework.
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
- Inventory/evidence truth: PASS. Uses existing Coverage v2 resource/evidence truth, no legacy snapshot promotion.
|
||||||
|
- Read/write separation: PASS. Read-only surface; no mutation actions.
|
||||||
|
- Graph contract path: PASS. No new Graph calls and no render-time provider calls.
|
||||||
|
- Deterministic capabilities: PASS with implementation validation for selected view capability.
|
||||||
|
- RBAC-UX: PASS with required 404/403 tests and server-side page authorization.
|
||||||
|
- Workspace isolation: PASS with required workspace/environment/provider scope tests.
|
||||||
|
- OperationRun: PASS. Diagnostic links only; no OperationRun as default proof.
|
||||||
|
- Evidence/currentness: PASS. Evidence hash allowed; raw evidence hidden; no fallback-to-latest proof.
|
||||||
|
- Customer output: PASS. No customer output, Review Pack, report, restore readiness, or customer-ready wording.
|
||||||
|
- Provider boundary: PASS with provider IDs hidden/default metadata-only.
|
||||||
|
- Product Surface: PASS with one documented Product Surface Contract Technical Annex surface-budget exception. UI-EX-001 implementation exception remains `none` if the surface is native Filament.
|
||||||
|
- Test governance: PASS. Unit/feature/browser lanes named; no broad heavy family.
|
||||||
|
- Proportionality: PASS. Thin derived read model is justified; persisted summary disallowed.
|
||||||
|
- No premature abstraction: PASS. Do not introduce UI framework or broad presenter.
|
||||||
|
- Persisted truth: PASS. No new persisted summary by default.
|
||||||
|
- Behavioral state: PASS. Uses existing v2 state families.
|
||||||
|
- No legacy / pre-production lean: PASS. No adapters, fallback readers, dual writes, old labels, or `tenant_id`.
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: Unit for read model and display mapping; Feature for authorization, rendered content, redaction, no-legacy, no-remote, and scope; Browser for Product Surface smoke.
|
||||||
|
- **Affected validation lanes**: fast-feedback, confidence, browser; pgsql only for indexes/migrations.
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: Unit tests prove computed readiness; feature tests prove security/render boundaries; browser proves the actual Filament/Livewire surface.
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/CoverageV2ReadinessSummaryTest.php tests/Unit/Support/TenantConfiguration/CoverageV2ActivationBlockerGroupingTest.php tests/Unit/Support/TenantConfiguration/CoverageV2ClaimGuardDisplayMapperTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceAuthorizationTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceNoLegacyLabelsTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceRedactionTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: keep Spec 418 fixtures local or opt-in; do not broaden default TenantConfiguration/browser factories.
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no expected default broadening.
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none.
|
||||||
|
- **Surface-class relief / special coverage rule**: standard-native-filament relief for layout; focused browser still required.
|
||||||
|
- **Closing validation and reviewer handoff**: implementation report must record exact commands/results, browser proof, Product Surface exception, and any unrelated failures.
|
||||||
|
- **Budget / baseline / trend follow-up**: none expected; document if browser fixture or feature lane cost expands materially.
|
||||||
|
- **Review-stop questions**: lane fit, hidden fixture cost, no remote render, redaction, old labels absent, customer claims absent, no `tenant_id`.
|
||||||
|
- **Escalation path**: document-in-feature for contained exception; follow-up-spec only if structural persistence or broad governance tests are required.
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||||
|
- **Why no dedicated follow-up spec is needed**: this is the bounded operator-readiness surface; customer/cutover actions are already deferred.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/418-coverage-v2-operator-surface/
|
||||||
|
├── spec.md
|
||||||
|
├── plan.md
|
||||||
|
├── tasks.md
|
||||||
|
├── implementation-report.md
|
||||||
|
└── checklists/
|
||||||
|
└── requirements.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (likely affected in later implementation)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/app/
|
||||||
|
├── Filament/
|
||||||
|
│ ├── Pages/TenantConfiguration/
|
||||||
|
│ │ └── CoverageV2Readiness.php
|
||||||
|
│ └── Widgets/TenantConfiguration/
|
||||||
|
│ ├── CoverageV2ActivationBlockersWidget.php
|
||||||
|
│ ├── CoverageV2ReadinessSummaryWidget.php
|
||||||
|
│ ├── CoverageV2ResourceInstancesTable.php
|
||||||
|
│ └── CoverageV2ResourceTypesTable.php
|
||||||
|
├── Providers/Filament/
|
||||||
|
│ └── AdminPanelProvider.php
|
||||||
|
├── Services/TenantConfiguration/
|
||||||
|
│ └── CoverageV2ReadinessReadModel.php
|
||||||
|
└── Support/TenantConfiguration/
|
||||||
|
└── CoverageV2ClaimGuardDisplayMapper.php
|
||||||
|
|
||||||
|
apps/platform/resources/views/filament/pages/tenant-configuration/
|
||||||
|
└── coverage-v2-readiness.blade.php
|
||||||
|
|
||||||
|
apps/platform/tests/
|
||||||
|
├── Unit/Support/TenantConfiguration/
|
||||||
|
├── Feature/TenantConfiguration/
|
||||||
|
└── Browser/
|
||||||
|
|
||||||
|
docs/ui-ux-enterprise-audit/
|
||||||
|
├── route-inventory.md
|
||||||
|
└── design-coverage-matrix.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Use repo-equivalent names if implementation finds a closer existing TenantConfiguration namespace. Keep the read model/service thin and DB-only. If a single Filament page with table widgets is not the best native pattern, implementation may use an equivalent native Filament page/resource-page structure while preserving the route and Product Surface contract.
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 0 - Preflight
|
||||||
|
|
||||||
|
Capture branch, HEAD, dirty state, activated skills, candidate gate, completed dependency reports, current Coverage v2 model/service names, and hard-gate stop conditions.
|
||||||
|
|
||||||
|
### Phase 1 - Inspect Existing Coverage v2 Read Paths
|
||||||
|
|
||||||
|
Inspect resource type registry, supported scopes, resources, evidence, identity states, Claim Guard, provider connection scope, OperationRun links, authorization helpers, existing Filament pages, and browser fixture patterns. Produce a dependency map in the implementation report.
|
||||||
|
|
||||||
|
### Phase 2 - Product Surface Contract Design
|
||||||
|
|
||||||
|
Lock route, surface role, surface type, native/custom classification, primary operator question, default-visible truth, diagnostics boundary, raw evidence boundary, action model, browser proof criteria, and Human Product Sanity criteria before runtime UI edits.
|
||||||
|
|
||||||
|
### Phase 3 - DB-Only Read Model
|
||||||
|
|
||||||
|
Create the derived read model/query service for readiness summary, resource type table, resource instance table, activation blockers, source class counts, and claim state counts. No remote calls, no persisted summary table.
|
||||||
|
|
||||||
|
### Phase 4 - Filament Native Surface
|
||||||
|
|
||||||
|
Add the read-only admin surface with scope summary, readiness summary, resource type table, resource instance table, activation blockers, diagnostics disclosure, empty states, filters, and one inspect model. Update admin panel/page registration and secondary navigation only if appropriate.
|
||||||
|
|
||||||
|
### Phase 5 - Authorization And Scope
|
||||||
|
|
||||||
|
Enforce workspace membership, managed environment entitlement, selected view capability, provider connection same-scope filtering, and 404/403 semantics.
|
||||||
|
|
||||||
|
### Phase 6 - Redaction, No-Remote, And No-Legacy Guards
|
||||||
|
|
||||||
|
Hide raw/normalized payloads, permission context raw JSON, secrets, raw provider details, old gap labels, customer claims, and unscoped 100% claims. Prove render path is DB-only.
|
||||||
|
|
||||||
|
### Phase 7 - Tests And Browser Smoke
|
||||||
|
|
||||||
|
Add focused unit, feature, and browser tests. Keep fixtures narrow and explicit. Run Pint dirty, focused tests, browser smoke, and `git diff --check`.
|
||||||
|
|
||||||
|
### Phase 8 - Implementation Report
|
||||||
|
|
||||||
|
Write `implementation-report.md` with candidate gate, dirty state before/after, files changed, route, Product Surface classification, UI Action Matrix, browser proof, Human Product Sanity, authorization proof, redaction proof, no remote render proof, no-tenant_id confirmation, no-legacy/no-dual-truth confirmation, tests, deployment impact, and deferred work.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|---|---|---|
|
||||||
|
| Thin read model/display mapper | Operators need one safe readiness answer from several existing v2 tables and services | Direct DB inspection or implementation reports lack RBAC/redaction/browser proof and cannot support Product Surface checks |
|
||||||
|
| Product Surface Contract Technical Annex surface-budget exception | Registry support and concrete instance evidence both block activation and must be visible together | A one-table page hides either denominator readiness or concrete capture/identity/claim blockers |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: v2 readiness cannot be verified safely from one product surface.
|
||||||
|
- **Existing structure is insufficient because**: Specs 414/415/417 created truth but no operator inspection path.
|
||||||
|
- **Narrowest correct implementation**: one derived DB-only read model and one read-only Filament-native surface.
|
||||||
|
- **Ownership cost created**: tests, central badge/status mapping, any non-status display mapping, browser smoke, UI registry updates, list-surface checklist close-out, and Product Surface close-out must be maintained as Coverage v2 evolves.
|
||||||
|
- **Alternative intentionally rejected**: add v2 sections to customer-facing or existing v1 surfaces. Rejected because that creates dual truth and premature customer activation.
|
||||||
|
- **Release truth**: current-release internal readiness inspection; future activation deferred.
|
||||||
410
specs/418-coverage-v2-operator-surface/spec.md
Normal file
410
specs/418-coverage-v2-operator-surface/spec.md
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
# Feature Specification: Spec 418 - Coverage v2 Operator Surface
|
||||||
|
|
||||||
|
**Feature Branch**: `418-coverage-v2-operator-surface`
|
||||||
|
**Created**: 2026-06-26
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User-provided "Spec 418 - Coverage v2 Operator Surface" draft plus repo checks against Specs 414/415/417, roadmap, candidate queue, constitution, Product Surface Contract, TenantPilot agent skill gates, and current TenantConfiguration runtime.
|
||||||
|
|
||||||
|
## Candidate Selection
|
||||||
|
|
||||||
|
- **Selected candidate**: Spec 418 - Coverage v2 Operator Surface.
|
||||||
|
- **Source location**: User-provided prompt in this session.
|
||||||
|
- **Why selected**: The active candidate queue in `docs/product/spec-candidates.md` explicitly has no automatic next-best target, but the user directly promoted a bounded Coverage v2 follow-up. Specs 414, 415, and 417 are implemented/accepted and leave Coverage v2 inactive but inspectable only through database rows, tests, and implementation reports. The next safe step is a read-only internal operator surface that shows readiness and blockers without activating customer-facing proof.
|
||||||
|
- **Roadmap relationship**: Aligns with the roadmap themes for evidence/coverage hardening, provider-boundary discipline, workspace/managed-environment ownership, and no-legacy cutover. This package is not auto-selected from the queue; it is a user-promoted P0 Coverage v2 cutover-readiness slice.
|
||||||
|
- **Close alternatives deferred**: Management-report runtime validation, artifact lifecycle retention, provider readiness productization, cross-domain indicator follow-through, system-panel browser fixture work, and first governed AI consumer remain manual-promotion backlog items. Coverage v2 legacy cutover/removal, Intune core comparable/renderable pack, certification, pilot readiness gate, customer output, Review Pack/report conversion, Evidence Overview conversion, Baseline Compare conversion, and Restore Readiness conversion are deferred to later Coverage v2 specs.
|
||||||
|
- **Related completed-spec guardrail**: `specs/414-tcm-first-coverage-core-cutover/`, `specs/415-generic-content-backed-capture/`, and `specs/417-canonical-identity-engine/` are completed/validated dependency context only. Do not rewrite them, normalize their close-out history, or strip validation/task/browser/review history.
|
||||||
|
- **Prerequisite gate result**: PASS. Spec 414 implementation report confirms resource type registry, supported scopes, Claim Guard, and inactive kernel. Spec 415 implementation report confirms concrete resources/evidence, content-backed capture outcomes, provider scope, and no UI activation. Spec 417 implementation report confirms canonical identity state, Claim Guard identity integration, and no UI/customer activation.
|
||||||
|
- **Smallest viable implementation slice**: Add one internal `/admin` Coverage v2 Readiness surface over existing Coverage v2 tables/services. It is read-only, DB-only during render, environment-scoped for instance rows, and limited to summaries, native tables, filters, inspect/disclosure details, authorized OperationRun diagnostic links, evidence hashes, and activation blocker groups.
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: Coverage v2 exists in persistence and services but operators cannot inspect readiness, supported scope, captured evidence state, identity state, claim state, provider provenance, or activation blockers in one bounded surface.
|
||||||
|
- **Today's failure**: Reviewers must inspect database rows, tests, reports, OperationRun context, and service internals. This makes false activation more likely because claim blockers, identity conflicts, beta/experimental resources, and capture blockers are not operationally visible.
|
||||||
|
- **User-visible improvement**: Operators get one internal read-only place to answer: "What prevents Coverage v2 from becoming active proof?"
|
||||||
|
- **Smallest enterprise-capable version**: One secondary operator page with a derived DB-only read model, readiness summary, resource type table, environment-filtered resource instance table, activation blockers, and disclosed diagnostics. No capture starts, no customer output, no legacy adapters, no raw payload display.
|
||||||
|
- **Explicit non-goals**: No customer-facing Coverage v2 claims, Review Pack/report output, Customer Review Workspace output, Evidence Overview conversion, Baseline Compare conversion, Restore Readiness conversion, capture start actions, TCM/Graph calls, compare/render/restore/certification, legacy runtime removal, v1-to-v2 adapter, dual writes, old snapshot migration, or technical object hub in primary navigation.
|
||||||
|
- **Permanent complexity imported**: One operator page/route, one thin derived read model or repo-equivalent query service, display mapping for existing Coverage v2 states, focused unit/feature/browser tests, and Product Surface close-out. No new persisted summary table, enum/status family, taxonomy table, or compatibility layer.
|
||||||
|
- **Why now**: Specs 414, 415, and 417 have completed the kernel, capture, and identity prerequisites. Before any cutover or customer activation, reviewers need human sanity proof that v2 readiness and blockers are visible and safe.
|
||||||
|
- **Why not local**: A scattered debug route, database query, or implementation report cannot enforce RBAC, redaction, no-legacy labels, provider scope, or Product Surface behavior. A bounded operator surface is the narrowest durable inspection point.
|
||||||
|
- **Approval class**: Core Enterprise.
|
||||||
|
- **Red flags triggered**: New surface; readiness/status presentation; report-like internal surface. Defense: it uses existing Coverage v2 states, adds no persisted UI truth, no customer output, and directly prevents unsafe cutover.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||||
|
- **Decision**: approve, with strict read-only, internal-only, DB-only, no-legacy, no-customer-claim, and no-raw-payload boundaries.
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: workspace + managed environment operator context. Resource type registry may be workspace-wide product metadata; resource instance rows require managed environment scope by default.
|
||||||
|
- **Primary Routes**: Recommended route `/admin/tenant-configuration/coverage-v2`. If repo route conventions require another internal/operator route, use the nearest `/admin` workspace/environment-scoped convention and document the deviation in the implementation report.
|
||||||
|
- **Data Ownership**: Existing Coverage v2 environment-owned rows are scoped by `workspace_id`, `managed_environment_id`, and `provider_connection_id`. Existing resource type/supported scope definition rows remain product/kernel metadata. No internal `tenant_id` ownership is allowed.
|
||||||
|
- **RBAC**: Non-member workspace access returns 404. Workspace member without managed-environment entitlement returns 404. Member with entitlement but without the chosen view capability returns 403. Implementation must verify whether `Capabilities::EVIDENCE_VIEW`, `Capabilities::TENANT_VIEW`, or a new narrow coverage-readiness view capability is the correct repo path; if a new capability is needed, it must be registry-backed and tested.
|
||||||
|
|
||||||
|
For canonical-view specs:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: If a managed environment is active or supplied by `environment_id`, prefilter resource instance rows to that managed environment. If workspace-wide mode is allowed, aggregate only environments the actor is entitled to view. The initial recommended rule is to require a managed environment filter before instance rows render.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Provider connection filters and instance queries must join or scope through workspace + managed environment entitlement and same-scope provider connection. Wrong workspace, wrong environment, wrong provider connection, and guessed IDs must not reveal rows.
|
||||||
|
|
||||||
|
## No Legacy / No Backward Compatibility Constraint *(mandatory)*
|
||||||
|
|
||||||
|
TenantPilot is pre-production unless this spec explicitly records a compatibility exception.
|
||||||
|
|
||||||
|
- **Compatibility posture**: canonical Coverage v2 inspection surface; no compatibility exception.
|
||||||
|
- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: no.
|
||||||
|
- **Why clean replacement is safe now**: Coverage v2 remains inactive and internal. No production/customer data, shared staging migration-relevant data, or external contract requires v1/v2 compatibility. This spec must not read legacy snapshots as v2 proof, translate old gap taxonomy into v2 rows, dual-write v1/v2 data, or add fallback readers.
|
||||||
|
|
||||||
|
## UI Surface Impact *(mandatory - UI-COV-001)*
|
||||||
|
|
||||||
|
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||||
|
|
||||||
|
- [ ] No UI surface impact
|
||||||
|
- [ ] Existing page changed
|
||||||
|
- [x] New page/route added
|
||||||
|
- [x] Navigation changed
|
||||||
|
- [x] Filament panel/provider surface changed
|
||||||
|
- [x] New modal/drawer/wizard/action added
|
||||||
|
- [x] New table/form/state added
|
||||||
|
- [ ] Customer-facing surface changed
|
||||||
|
- [ ] Dangerous action changed
|
||||||
|
- [x] Status/evidence/review presentation changed
|
||||||
|
- [x] Workspace/environment context presentation changed
|
||||||
|
|
||||||
|
## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact"; otherwise write `N/A - no reachable UI surface impact` plus rationale)*
|
||||||
|
|
||||||
|
- **Route/page/surface**: Coverage v2 Readiness at `/admin/tenant-configuration/coverage-v2` or repo-equivalent `/admin` internal operator route.
|
||||||
|
- **Current or new page archetype**: Technical Annex Page with Read-only Registry / Report Surface behavior.
|
||||||
|
- **Design depth**: Internal/Hidden / Domain Pattern Surface. It must not become a primary daily decision surface or technical object hub.
|
||||||
|
- **Repo-truth level**: repo-verified dependencies from Specs 414, 415, and 417; new surface is spec-backed until implemented.
|
||||||
|
- **Existing pattern reused**: Filament-native `Page`, table widgets or native tables, filters, badges, infolists/sections, primary-link column inspect slideovers where needed; existing OperationRun URL helper; existing workspace/environment/provider scope helpers; existing Product Surface Contract.
|
||||||
|
- **List surface review checklist**: `docs/product/standards/list-surface-review-checklist.md` applies because this spec adds Read-only Registry / Report table surfaces. The implementation report must record the checklist result or documented exceptions.
|
||||||
|
- **New pattern required**: none. If implementation cannot express the surface with native Filament/report semantics, stop and document a Product Surface exception before custom Blade.
|
||||||
|
- **Screenshot required**: no standalone screenshot artifact required by prep, but focused browser proof is required in the implementation report.
|
||||||
|
- **Page audit required**: update `docs/ui-ux-enterprise-audit/route-inventory.md` and `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` during implementation, or document a bounded exception if that registry format cannot represent this internal surface.
|
||||||
|
- **Customer-safe review required**: yes, as a negative boundary: customer-facing claims and customer output must remain absent.
|
||||||
|
- **Dangerous-action review required**: no destructive/high-impact actions are allowed.
|
||||||
|
- **Coverage files updated or explicitly not needed**:
|
||||||
|
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
|
||||||
|
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
|
||||||
|
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||||
|
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
|
||||||
|
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
|
||||||
|
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
|
||||||
|
- [ ] `N/A - no reachable UI surface impact`
|
||||||
|
- **No-impact rationale when applicable**: N/A.
|
||||||
|
|
||||||
|
## Product Surface Impact *(mandatory for UI-affecting specs; otherwise write `N/A - no rendered product surface changed` plus rationale)*
|
||||||
|
|
||||||
|
Reference: `docs/product/standards/product-surface-contract.md`.
|
||||||
|
|
||||||
|
- **Product Surface Contract applies?**: yes. This is a rendered readiness/evidence/provider-state operator surface.
|
||||||
|
- **Page archetype**: Technical Annex Page.
|
||||||
|
- **Primary user question**: What prevents Coverage v2 from becoming active proof?
|
||||||
|
- **Primary action**: Inspect/open read-only details. No mutation, export, capture, publish, restore, or customer report action is allowed.
|
||||||
|
- **Surface budget result**: Product Surface Contract Technical Annex surface-budget exception documented. The relaxed budget is summary plus two native table data sets (resource types and resource instances). Reason: operators must compare registry support with concrete environment evidence to answer readiness. This is not customer/product-facing because the route is internal/operator-only, diagnostics/raw proof remain disclosed or hidden, and no customer claim is activated. Follow-up: none if default view remains readiness-first, instance rows require environment scope, diagnostics are disclosed, and no raw/customer proof appears.
|
||||||
|
- **Technical Annex / deep-link demotion**: OperationRun links, evidence hashes, source metadata, provider diagnostics, reason codes, identity fields, source contract state, and missing/present identity fields are secondary diagnostics. Raw payload, normalized payload, permission context raw JSON, tokens, secrets, raw Graph responses, stack traces, and old v1 gap codes are forbidden.
|
||||||
|
- **Canonical status vocabulary**: top-level readiness may use `Ready`, `Needs attention`, `Blocked`, and `Unknown`. Coverage v2 enum values (`coverage_level`, `evidence_state`, `identity_state`, `claim_state`, `source_class`, `support_state`) are internal diagnostic dimensions and must be labeled as internal/operator readiness, not customer proof.
|
||||||
|
- **Visible complexity impact**: neutral to reduced for operators, because scattered database/report/test inspection is compressed into one bounded read-only surface.
|
||||||
|
- **Product Surface exceptions**: one Product Surface Contract Technical Annex surface-budget exception for `Coverage v2 Readiness` as described above. **UI-EX-001 implementation exception**: none when implemented with native Filament tables/infolists/sections/actions. If implementation requires custom UI, stop before building it and name a catalogued UI-EX-001 exception type with proof.
|
||||||
|
|
||||||
|
## Browser Verification Plan *(mandatory)*
|
||||||
|
|
||||||
|
- **Browser proof required?**: yes.
|
||||||
|
- **No-browser rationale**: N/A.
|
||||||
|
- **Focused path when required**: `/admin/tenant-configuration/coverage-v2` or implemented repo-equivalent.
|
||||||
|
- **Primary interaction to execute**: load the page with an authorized operator, apply or confirm managed-environment context, inspect visible summary/table labels, open one diagnostics disclosure or row inspect model if implemented, and verify forbidden labels/raw content are absent.
|
||||||
|
- **Console, Livewire, Filament, network, and 500-error checks**: planned. Browser proof must record no console errors, no Livewire/Filament runtime errors, no 500s, and no network calls to Graph/TCM/provider endpoints during render.
|
||||||
|
- **Full-suite failure triage**: unrelated browser failures may be documented separately only if focused Spec 418 proof is green.
|
||||||
|
|
||||||
|
## Human Product Sanity Check *(mandatory)*
|
||||||
|
|
||||||
|
- **Required?**: yes.
|
||||||
|
- **No-human-sanity rationale**: N/A.
|
||||||
|
- **Reviewer questions**: Can an operator understand why Coverage v2 is or is not activation-ready? Are blockers grouped by actionable v2 states? Does the page avoid noisy technical object hub behavior? Are raw/support details hidden by default? Is there exactly one inspect model? Are old gap labels absent?
|
||||||
|
- **Planned result location**: `specs/418-coverage-v2-operator-surface/implementation-report.md`.
|
||||||
|
|
||||||
|
## Product Surface Merge Gate Checklist *(mandatory)*
|
||||||
|
|
||||||
|
- [x] No-legacy posture or approved exception recorded.
|
||||||
|
- [x] Product Surface Impact is completed or `N/A` is justified.
|
||||||
|
- [x] Browser proof is required and the focused proof plan is specified for implementation close-out.
|
||||||
|
- [x] Human Product Sanity is required and the reviewer questions/result location are specified for implementation close-out.
|
||||||
|
- [x] Product Surface exceptions are documented or `none`.
|
||||||
|
- [x] Implementation report will state Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, visible complexity outcome, and no completed-spec rewrite assertion.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes.
|
||||||
|
- **Interaction class(es)**: status messaging, navigation entry point, action/deep links, evidence diagnostics, provider provenance, read-only report/table surface.
|
||||||
|
- **Systems touched**: TenantConfiguration resource type registry, supported scopes, resources, evidence, Claim Guard, identity state, provider connection provenance, OperationRun links, Filament admin panel registration, UI audit registry.
|
||||||
|
- **Existing pattern(s) to extend**: existing Filament-native page/table/widget patterns, `OperationRunLinks`, workspace/environment/provider scoping helpers, capability/policy helpers, and `BadgeCatalog`/`BadgeRenderer` for status-like badges.
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: `OperationRunLinks` for operation URLs; `WorkspaceContext`/managed environment access helpers; `BadgeCatalog`/`BadgeRenderer` for status-like readiness, coverage, evidence, identity, claim, source, and support values. If TenantConfiguration state badge mapping is missing, implementation must add a narrow central badge/domain mapping with tests. A page-local display mapper may format non-status text only and must not assign colors, icons, or status semantics.
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: Native Filament and existing scope/link helpers are sufficient for the surface. Existing Coverage v2 services provide truth; the missing piece is a derived operator read model and display mapping, not new domain truth.
|
||||||
|
- **Allowed deviation and why**: none for raw UI frameworks, customer output, or remote calls. The only allowed Product Surface exception is the internal Technical Annex two-table layout.
|
||||||
|
- **Consistency impact**: Keep labels aligned with existing Coverage v2 enum values and Product Surface top-level readiness vocabulary. Do not introduce parallel old gap labels.
|
||||||
|
- **Review focus**: Verify native/read-only behavior, one inspect model, no redundant View action, no raw payloads, no OperationRun default proof, no customer-safe claims, and no page-local truth that conflicts with existing v2 services.
|
||||||
|
|
||||||
|
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: yes, diagnostic links only. No start/completion UX is allowed.
|
||||||
|
- **Shared OperationRun UX contract/layer reused**: `OperationRunLinks` or current canonical OperationRun URL helper.
|
||||||
|
- **Delegated start/completion UX behaviors**: none; no queued toast, run-enqueued event, queued DB notification, dedupe messaging, or terminal notification.
|
||||||
|
- **Local surface-owned behavior that remains**: display an authorized secondary diagnostic link only when the actor may view the referenced run.
|
||||||
|
- **Queued DB-notification policy**: N/A.
|
||||||
|
- **Terminal notification path**: N/A.
|
||||||
|
- **Exception required?**: none.
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes.
|
||||||
|
- **Boundary classification**: mixed.
|
||||||
|
- **Seams affected**: provider connection provenance, source class, source contract metadata, provider connection filters, and provider-owned external IDs as hidden/diagnostic metadata.
|
||||||
|
- **Neutral platform terms preserved or introduced**: workspace, managed environment, provider connection, resource type, source class, supported scope, coverage level, evidence state, identity state, claim state, activation blocker.
|
||||||
|
- **Provider-specific semantics retained and why**: Microsoft/Graph/TCM labels may appear only as source class or safe provider metadata because the initial Coverage v2 resource types are Intune-backed.
|
||||||
|
- **Why this does not deepen provider coupling accidentally**: Provider-specific identifiers stay metadata/diagnostics and never become route, ownership, or platform-core identity truth.
|
||||||
|
- **Follow-up path**: none for this slice; future resource packs can add source contracts separately.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Coverage v2 Readiness | yes | Native Filament preferred | status messaging, evidence diagnostics, navigation, OperationRun links | page, query/filter, table/detail/disclosure | PSC Technical Annex surface-budget exception only; UI-EX-001 none if native | Read-only internal operator surface; no customer output |
|
||||||
|
|
||||||
|
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Coverage v2 Readiness | Secondary Context Surface | Review whether Coverage v2 can proceed to the next cutover step | selected workspace/environment, supported scope, readiness counts, critical v2 states, top activation blockers | identity diagnostics, claim guard explanation, capture blocker reason, source contract metadata, evidence hash, authorized OperationRun link | Not primary because it supports readiness inspection and cutover planning, not a daily queue or customer decision | Follows coverage cutover readiness, not storage objects | Replaces scattered DB/test/report inspection with one bounded surface |
|
||||||
|
|
||||||
|
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Coverage v2 Readiness | operator/MSP, support/platform diagnostics only | readiness summary, resource type support, coverage/evidence/identity/claim states, top blockers | reason code, missing/present identity fields, source class, source contract state, provider provenance, evidence hash, OperationRun link | raw payloads and normalized payloads remain hidden; no support/raw route added unless existing safe route exists | Inspect read-only details | raw_payload, normalized_payload, permission_context JSON, provider response dumps, secrets/tokens, old v1 gap codes | top summary states blockers once; tables provide evidence dimensions without restating old gap taxonomy |
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Coverage v2 Readiness | List / Table / Bulk | Read-only Registry / Report Surface | Inspect blocker or resource readiness | linked primary column opens read-only same-page slide-over | linked primary column; full-row click intentionally avoided for dense comparison tables | disclosed diagnostics/slide-over only | none | `/admin/tenant-configuration/coverage-v2` | N/A or same-page slide-over | workspace, managed environment, supported scope, provider connection filter | Coverage v2 Readiness | coverage level, evidence state, identity state, claim state, source class, supported scope | PSC Technical Annex surface-budget exception only; UI-EX-001 none unless custom UI becomes necessary |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Coverage v2 Readiness | TenantPilot operator/reviewer | Determine what blocks Coverage v2 activation | Read-only Registry / Report Surface | What prevents Coverage v2 from becoming active proof? | workspace, managed environment, supported scope, counts, resource type support, instance readiness states, top activation blockers | identity diagnostics, source contract metadata, claim guard explanation, evidence hash, authorized OperationRun link | top readiness plus coverage/evidence/identity/claim/support/source dimensions | none | inspect/open read-only detail | none |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: no. The page derives from existing Coverage v2 tables/services.
|
||||||
|
- **New persisted entity/table/artifact?**: no. New persisted summary tables and UI-only readiness records are disallowed unless implementation proves query cost cannot be solved with existing tables or narrow indexes.
|
||||||
|
- **New abstraction?**: yes, likely one thin derived read model/query service and a narrow non-status display mapper where useful. Status-like rendered badges must use `BadgeCatalog`/`BadgeRenderer` or a new central BadgeDomain mapping with tests, not page-local status semantics.
|
||||||
|
- **New enum/state/reason family?**: no. Use existing `CoverageLevel`, `EvidenceState`, `IdentityState`, `ClaimState`, `SourceClass`, `SupportState`, and existing capture outcomes.
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no.
|
||||||
|
- **Current operator problem**: Operators cannot verify v2 activation readiness or blockers without scattered technical inspection.
|
||||||
|
- **Existing structure is insufficient because**: Kernel/capture/identity services store truth but do not provide a bounded, authorized, redacted, no-legacy operator inspection surface.
|
||||||
|
- **Narrowest correct implementation**: One DB-only read model plus one read-only Filament-native page, scoped and tested.
|
||||||
|
- **Ownership cost**: Maintain the read model, central badge/status mapping, any non-status display mapping, feature/browser tests, scope/redaction/no-legacy guards, and Product Surface close-out as Coverage v2 evolves.
|
||||||
|
- **Alternative intentionally rejected**: Database inspection, implementation reports, or adding v2 claims to existing customer/report surfaces. Rejected because they either lack RBAC/redaction/product proof or would activate dual customer truth prematurely.
|
||||||
|
- **Release truth**: Current-release internal readiness inspection after Specs 414/415/417; future customer activation remains deferred.
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment. Backward compatibility, legacy aliases, migration shims, historical fixtures, v1 adapters, fallback readers, and dual writes are out of scope.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Unit for read model/mapper/blocker grouping; Feature for authorization/render/redaction/no-legacy/no-remote/scope; Browser for focused rendered UI smoke.
|
||||||
|
- **Validation lane(s)**: fast-feedback, confidence, browser. PostgreSQL only if implementation adds/changes database indexes/constraints.
|
||||||
|
- **Why this classification and these lanes are sufficient**: Read model logic is service-level; authorization/redaction/no-remote behavior requires feature tests; rendered Product Surface compliance requires browser proof.
|
||||||
|
- **New or expanded test families**: Focused `Spec418*` unit/feature/browser files only. No broad heavy-governance family.
|
||||||
|
- **Fixture / helper cost impact**: Use existing TenantConfiguration factories and workspace/environment/provider setup. Any browser fixture must be explicit and local to Spec 418.
|
||||||
|
- **Heavy-family visibility / justification**: none.
|
||||||
|
- **Special surface test profile**: standard-native-filament plus focused browser smoke.
|
||||||
|
- **Standard-native relief or required special coverage**: Native Filament relief for layout; browser proof still required because a rendered route changes.
|
||||||
|
- **Reviewer handoff**: Verify no hidden browser/heavy cost, no broad fixture defaults, no remote calls during render, no old labels, no raw payloads, and exact commands in the implementation report.
|
||||||
|
- **Budget / baseline / trend impact**: none expected; document if browser or feature fixture cost materially expands.
|
||||||
|
- **Escalation needed**: document-in-feature for the Product Surface exception; follow-up-spec only if read model/query cost requires structural persistence.
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/CoverageV2ReadinessSummaryTest.php tests/Unit/Support/TenantConfiguration/CoverageV2ActivationBlockerGroupingTest.php tests/Unit/Support/TenantConfiguration/CoverageV2ClaimGuardDisplayMapperTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceAuthorizationTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceNoLegacyLabelsTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceRedactionTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Inspect Coverage v2 Activation Readiness (Priority: P1)
|
||||||
|
|
||||||
|
As an operator or reviewer, I need one internal page that summarizes Coverage v2 readiness for a selected workspace and managed environment so I can decide what blocks the next cutover step.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core value of Spec 418 and the minimum safe inspection path before cutover.
|
||||||
|
|
||||||
|
**Independent Test**: Seed Coverage v2 resource types, resources, evidence, identity states, and claim states. An authorized actor loads the page and sees summary counts and top blocker groups derived from v2 state only.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an authorized actor in a workspace with managed-environment entitlement, **When** the actor opens Coverage v2 Readiness, **Then** the page shows selected workspace, managed environment, supported scope, readiness counts, and top activation blockers.
|
||||||
|
2. **Given** identity conflicts, claim-blocked resources, beta resources, and capture failures exist, **When** the summary is rendered, **Then** blockers are grouped by v2 states such as `identity_conflict`, `claim_blocked`, `beta_experimental`, and `capture_failed`.
|
||||||
|
3. **Given** legacy v1 gap labels exist elsewhere in the codebase, **When** this page renders, **Then** it does not show `Evidence gaps`, `Raw gaps`, `ambiguous_match`, `policy_record_missing`, `foundation_not_policy_backed`, or `meta_fallback`.
|
||||||
|
|
||||||
|
### User Story 2 - Inspect Resource Type Support (Priority: P1)
|
||||||
|
|
||||||
|
As an operator, I need to inspect the Coverage v2 resource type registry and supported scope so I can understand which resource types are included, supported, beta/experimental, fallback-backed, and claimable.
|
||||||
|
|
||||||
|
**Why this priority**: Activation readiness depends on denominator and support posture before inspecting individual resources.
|
||||||
|
|
||||||
|
**Independent Test**: Seed the Spec 414 registry and supported scopes. The resource type table shows required columns and filters without using old gap vocabulary.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** active resource type definitions exist, **When** the resource type table renders, **Then** it shows human name, canonical type, workload, source class, support state, default coverage level, supported-scope inclusion, beta, graph fallback, and claim behavior.
|
||||||
|
2. **Given** a filter is applied by workload, source class, support state, supported scope, beta, or claim behavior, **When** the table reloads, **Then** only matching registry rows are shown.
|
||||||
|
3. **Given** a row detail/inspect model is implemented, **When** an operator opens a row, **Then** there is exactly one inspect model and no redundant View action beside row click or linked primary column.
|
||||||
|
|
||||||
|
### User Story 3 - Inspect Resource Instance State Safely (Priority: P1)
|
||||||
|
|
||||||
|
As an operator, I need environment-scoped resource instance rows with critical truth visible by default so I can see what concrete captured resources block activation.
|
||||||
|
|
||||||
|
**Why this priority**: Resource type readiness without instance/evidence/identity/claim state cannot answer the cutover-readiness question.
|
||||||
|
|
||||||
|
**Independent Test**: Seed concrete resources and evidence for two managed environments and provider connections. An authorized actor sees only same-scope rows and the default visible critical state columns.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** concrete Coverage v2 resources exist for the selected environment, **When** instance rows render, **Then** resource, resource type, provider connection, coverage level, evidence state, identity state, claim state, last captured time, and evidence hash are visible.
|
||||||
|
2. **Given** a provider connection filter is applied, **When** instance rows render, **Then** cross-environment provider connections do not leak rows or labels.
|
||||||
|
3. **Given** raw and normalized payloads exist in evidence storage, **When** the page and diagnostics render, **Then** raw payload, normalized payload, permission context raw JSON, tokens, secrets, authorization headers, and raw Graph response dumps are absent.
|
||||||
|
|
||||||
|
### User Story 4 - Enforce Scope, Claim Safety, And No Remote Render Work (Priority: P1)
|
||||||
|
|
||||||
|
As a security reviewer, I need the surface to enforce workspace/environment/provider isolation, claim-safety language, and DB-only rendering so the inspection page cannot become an activation or data-leak path.
|
||||||
|
|
||||||
|
**Why this priority**: Read-only pages can still leak scope, proof, or remote side effects if authorization and render boundaries are weak.
|
||||||
|
|
||||||
|
**Independent Test**: Feature tests cover non-member 404, no environment entitlement 404, missing capability 403, authorized view, no Graph/TCM calls during render, no customer-ready claims, no unscoped 100% claim, and no `tenant_id` ownership query.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a non-member guesses the route, **When** they request it, **Then** the response is 404.
|
||||||
|
2. **Given** a workspace member lacks entitlement to the selected environment, **When** they request the surface, **Then** the response is 404.
|
||||||
|
3. **Given** a member has environment entitlement but lacks the view capability, **When** they request the surface, **Then** the response is 403.
|
||||||
|
4. **Given** the page renders for an authorized actor, **When** the request completes, **Then** no Graph/TCM/provider client call occurred during render.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- No managed environment selected: the page may show registry/resource type readiness but must require an environment filter before instance rows render, or redirect to the repo-standard environment selection path.
|
||||||
|
- No supported scopes exist: fail closed with `Unknown` or `Blocked` readiness and clear internal copy that supported scope is missing.
|
||||||
|
- No captured resources exist: show `not_captured` blockers and no raw debug output.
|
||||||
|
- Identity conflicts with no latest evidence: show identity and claim blockers without fabricating evidence hash.
|
||||||
|
- OperationRun reference is missing or unauthorized: omit or disable the diagnostic link without exposing the run ID.
|
||||||
|
- Beta/experimental resources are present: group separately and never label them certified or customer-ready.
|
||||||
|
- Graph fallback resources are present: show source class/fallback posture as internal readiness, not equal customer proof.
|
||||||
|
- Empty evidence hash: display as unavailable, not as proof.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-418-001**: The system MUST add exactly one bounded internal Coverage v2 Readiness operator surface.
|
||||||
|
- **FR-418-002**: The surface MUST be read-only and MUST NOT add capture, sync, restore, apply, certify, publish, export, report, review-pack, or manual claim-override actions.
|
||||||
|
- **FR-418-003**: The surface MUST use Coverage v2 data and services only: resource type registry, supported scopes, resources, evidence, identity state, and Claim Guard output.
|
||||||
|
- **FR-418-004**: The surface MUST NOT use legacy snapshots, Coverage v1 gap taxonomy, v1 subject matching, v1-to-v2 adapters, fallback readers, or dual-write paths as v2 proof.
|
||||||
|
- **FR-418-005**: The page render path MUST be DB-only and MUST NOT call Microsoft Graph, TCM, provider gateways, capture services, remote verification services, or remote clients.
|
||||||
|
- **FR-418-006**: The default route SHOULD be `/admin/tenant-configuration/coverage-v2`; any repo-equivalent route deviation MUST be documented in the implementation report.
|
||||||
|
- **FR-418-007**: The surface MUST show selected workspace, managed environment, supported scope, provider connection filter when applicable, source class filter, and last updated or last captured time.
|
||||||
|
- **FR-418-008**: Resource instance rows MUST require managed environment scope unless implementation proves workspace-wide aggregation is safe across only entitled environments.
|
||||||
|
- **FR-418-009**: The readiness summary MUST include `resource_types_total`, `resources_total`, `content_backed_count`, `identity_conflict_count`, `claim_allowed_count`, `claim_limited_count`, `claim_blocked_count`, `beta_experimental_count`, and `graph_fallback_count`.
|
||||||
|
- **FR-418-010**: Summary counts MUST derive from Coverage v2 state and MUST NOT derive from old v1 gap labels or old snapshot classifications.
|
||||||
|
- **FR-418-011**: The resource type table MUST show human name, canonical type, workload, source class, support state, default coverage level, supported-scope inclusion, beta indicator, Graph fallback indicator, and claim behavior.
|
||||||
|
- **FR-418-012**: The resource type table MUST provide filters for workload, source class, support state, supported scope, beta/non-beta, and claim behavior.
|
||||||
|
- **FR-418-013**: The resource instance table or grouped view MUST show resource, resource type, provider connection, coverage level, evidence state, identity state, claim state, last captured time, and evidence hash.
|
||||||
|
- **FR-418-014**: The resource instance table MUST provide filters for resource type, provider connection, coverage level, evidence state, identity state, claim state, and source class.
|
||||||
|
- **FR-418-015**: Critical truth MUST be visible by default: `coverage_level`, `evidence_state`, `identity_state`, and `claim_state`.
|
||||||
|
- **FR-418-016**: Activation blockers MUST be grouped by v2 states, including `identity_conflict`, `missing_external_id`, `unsupported_identity`, `not_captured`, `permission_blocked`, `source_unavailable`, `schema_unknown`, `capture_failed`, `claim_blocked`, and `beta_experimental`. Top blocker ordering MUST be deterministic: identity and claim blockers first, capture/source blockers next, beta/experimental blockers after non-beta blockers, then count descending, then stable blocker key ascending.
|
||||||
|
- **FR-418-017**: Diagnostics MAY show reason code, missing identity fields, present identity fields, source class, source contract state, authorized OperationRun link, evidence hash, and provider connection provenance.
|
||||||
|
- **FR-418-018**: Diagnostics MUST be secondary/disclosed and MUST NOT be primary visual content.
|
||||||
|
- **FR-418-019**: Raw payload, normalized payload, permission context raw JSON, access tokens, refresh tokens, ID tokens, client secrets, passwords, private keys, certificates, authorization headers, cookies, raw exception messages, stack traces, raw Graph responses, and unredacted PII MUST NOT be displayed.
|
||||||
|
- **FR-418-020**: Evidence hash MAY be displayed and copied only if safe and useful.
|
||||||
|
- **FR-418-021**: OperationRun diagnostic links MAY appear only through the canonical helper and only when the actor is authorized to view the run.
|
||||||
|
- **FR-418-022**: OperationRun links MUST be secondary diagnostics and MUST NOT be treated as default proof.
|
||||||
|
- **FR-418-023**: The surface MUST show Claim Guard results only as internal/operator readiness labels: `Claim allowed`, `Claim limited`, `Claim blocked`, or `Internal only`; when rendered as status-like badges, these labels MUST use `BadgeCatalog`/`BadgeRenderer` or a central BadgeDomain mapping, not page-local color/status mapping.
|
||||||
|
- **FR-418-024**: The surface MUST NOT produce or activate customer-facing Coverage v2 claims.
|
||||||
|
- **FR-418-025**: The surface MUST NOT show unscoped `100%` claims. Any 100% statement must be explicitly internal, scoped, and allowed by Claim Guard for the exact selected scope and level.
|
||||||
|
- **FR-418-026**: The surface MUST NOT show customer-facing phrases such as `100% Microsoft 365 coverage`, `Complete tenant coverage`, `Certified coverage`, `Restore-ready`, `Full evidence coverage`, or `Customer-ready proof`.
|
||||||
|
- **FR-418-027**: The default UI MUST NOT show old labels: `Evidence gaps`, `Raw gaps`, `Primary gaps`, `Partially complete`, `Incomplete result`, `ambiguous_match`, `policy_record_missing`, `foundation_not_policy_backed`, or `meta_fallback`.
|
||||||
|
- **FR-418-028**: The surface MUST enforce non-member workspace access as 404.
|
||||||
|
- **FR-418-029**: The surface MUST enforce workspace member without managed-environment entitlement as 404.
|
||||||
|
- **FR-418-030**: The surface MUST enforce member without the required view capability as 403 after membership and environment entitlement are established.
|
||||||
|
- **FR-418-031**: Provider connection filters MUST be same-workspace and same-managed-environment, and MUST NOT reveal cross-environment records or labels.
|
||||||
|
- **FR-418-032**: The implementation MUST NOT introduce `tenant_id` as Coverage v2 ownership truth, compatibility alias, fallback reader, dual-write target, or query scope.
|
||||||
|
- **FR-418-033**: Resource type registry rows MAY be workspace-wide product metadata, but environment-owned evidence/resources MUST be scoped and authorized by workspace and managed environment.
|
||||||
|
- **FR-418-034**: The implementation SHOULD prefer a thin derived read model over persisted summary tables.
|
||||||
|
- **FR-418-035**: New persisted UI-only summary records are disallowed unless the active spec is amended with a proportionality review and query-cost proof.
|
||||||
|
- **FR-418-036**: If query cost is high, implementation MAY add narrow indexes with a documented query path and rollback/forward migration note.
|
||||||
|
- **FR-418-037**: The surface MUST use native Filament components where possible, MUST use central badge/status primitives for status-like values, and MUST NOT ship a fake-native Blade request UI when native table/report semantics fit.
|
||||||
|
- **FR-418-038**: Each table/detail surface MUST have exactly one primary inspect/open model; redundant View actions beside row click or linked primary column are forbidden.
|
||||||
|
- **FR-418-039**: The implementation MUST update or explicitly handle UI audit registry artifacts for the new route according to UI-COV-001 and MUST apply `docs/product/standards/list-surface-review-checklist.md` for the new list/table surfaces.
|
||||||
|
- **FR-418-040**: The implementation report MUST include candidate gate result, dirty state before/after, files changed, route/surface added, Product Surface classification, UI Action Matrix, Human Product Sanity, browser proof, authorization proof, redaction proof, no remote render proof, no-tenant_id confirmation, no-legacy/no-dual-truth confirmation, tests run, and deferred work.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Coverage v2 Readiness | `/admin/tenant-configuration/coverage-v2` or repo-equivalent Filament page | none, except neutral scope/filter reset if repo pattern requires | linked primary column opens read-only same-page slide-over | none by default; optional copy evidence hash only if safe | none | explain missing environment/filter/capture state; optional clear filters only | N/A | N/A | no mutation audit required; diagnostic open audit only if repo pattern requires | read-only internal Technical Annex; two-table Product Surface exception; no separate row action column |
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Tenant Configuration Resource Type**: Existing Coverage v2 resource type definition with canonical type, display name, workload, source class, support state, coverage defaults, claim defaults, beta/fallback flags, and metadata.
|
||||||
|
- **Tenant Configuration Supported Scope**: Existing supported-scope denominator contract from Spec 414.
|
||||||
|
- **Tenant Configuration Resource**: Existing environment-owned concrete Coverage v2 resource observed in a workspace/managed environment/provider connection with latest evidence, identity, claim, and capture metadata.
|
||||||
|
- **Tenant Configuration Resource Evidence**: Existing append-only evidence row with payload hash, coverage/evidence state, capture outcome, captured timestamp, operation run, and redacted source/permission metadata. Raw payloads remain hidden.
|
||||||
|
- **Provider Connection**: Existing same-scope provenance and filter dimension.
|
||||||
|
- **OperationRun**: Existing execution truth for capture operations; may be linked only as secondary authorized diagnostics.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-418-001**: An authorized operator can load one Coverage v2 Readiness surface and identify the top activation blockers without inspecting database rows or implementation reports.
|
||||||
|
- **SC-418-002**: Focused feature tests prove non-member 404, no environment entitlement 404, missing capability 403, and authorized 200 behavior.
|
||||||
|
- **SC-418-003**: Focused tests prove readiness counts derive from Coverage v2 states and no old v1 gap labels are emitted.
|
||||||
|
- **SC-418-004**: Focused tests prove raw payloads, normalized payloads, permission context raw JSON, tokens, secrets, authorization headers, and raw provider responses do not render.
|
||||||
|
- **SC-418-005**: Focused tests or static guards prove no Graph/TCM/provider remote call happens during page render.
|
||||||
|
- **SC-418-006**: Browser smoke proves the rendered page shows Coverage level, Evidence state, Identity state, Claim state, Source class, and Supported scope, and does not show old gap labels, raw payload, or customer-ready coverage claims.
|
||||||
|
- **SC-418-007**: Implementation report records Product Surface classification, browser proof, Human Product Sanity, no-legacy/no-dual-truth, no-tenant_id, redaction, authorization, no remote render, and deployment impact.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- Specs 414, 415, and 417 remain accepted dependency context and their implementation reports are authoritative.
|
||||||
|
- Coverage v2 remains inactive for customer claims through this spec.
|
||||||
|
- The initial resource type registry contains the eight Spec 414/415/417 resource types.
|
||||||
|
- The correct view capability will be confirmed during implementation against current RBAC patterns; no raw role-string checks are allowed.
|
||||||
|
- A native Filament Page plus native tables/widgets/infolists can satisfy the surface without custom product UI.
|
||||||
|
- There is no safe raw evidence route that must be introduced in this spec.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
| Risk | Severity | Mitigation |
|
||||||
|
|---|---:|---|
|
||||||
|
| Surface becomes technical object hub | High | Secondary Context classification; Technical Annex budget exception; readiness question first; diagnostics disclosed |
|
||||||
|
| v2 appears as active customer truth | High | internal/operator language, no customer surfaces, no output/download actions, claim safety tests |
|
||||||
|
| Old gap vocabulary leaks into UI | High | unit/feature/browser assertions |
|
||||||
|
| Raw payloads or secrets leak | High | redaction tests; no raw/default display |
|
||||||
|
| Remote work during render | High | DB-only read model; fake/failing remote client tests or static guard |
|
||||||
|
| Scope leakage across workspace/environment/provider | High | positive and negative authorization/filter tests |
|
||||||
|
| New persisted UI summary bloat | Medium | derived read model only; narrow-index fallback with proof |
|
||||||
|
| Too much browser proof | Medium | focused smoke only |
|
||||||
|
|
||||||
|
## Follow-Up Spec Candidates
|
||||||
|
|
||||||
|
- Spec 419 - Legacy Coverage Cutover and Removal.
|
||||||
|
- Spec 420 - Intune Core Comparable/Renderable Pack.
|
||||||
|
- Spec 421 - Certified Intune Core Coverage Pack.
|
||||||
|
- Spec 422 - Pilot Readiness Gate.
|
||||||
|
- Customer-facing Coverage v2 output.
|
||||||
|
- Evidence Overview v2 conversion.
|
||||||
|
- Review Pack/report v2 conversion.
|
||||||
|
- Restore Readiness v2 conversion.
|
||||||
|
- Capture start action.
|
||||||
|
- Identity re-evaluation action.
|
||||||
|
- Compare/render/restore/certification.
|
||||||
|
- Full v1 runtime removal.
|
||||||
129
specs/418-coverage-v2-operator-surface/tasks.md
Normal file
129
specs/418-coverage-v2-operator-surface/tasks.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Tasks: Spec 418 - Coverage v2 Operator Surface
|
||||||
|
|
||||||
|
**Input**: `specs/418-coverage-v2-operator-surface/spec.md`, `specs/418-coverage-v2-operator-surface/plan.md`, `specs/418-coverage-v2-operator-surface/checklists/requirements.md`
|
||||||
|
**Prerequisites**: completed Specs 414, 415, and 417 as read-only dependency context
|
||||||
|
|
||||||
|
**Tests**: Required. Runtime UI/security behavior must be covered with focused Pest unit, feature, and browser tests. PostgreSQL lane is required only if migrations/indexes/constraints change.
|
||||||
|
|
||||||
|
**Implementation note**: The planned Unit/Feature test responsibilities were completed through repo-equivalent focused files: `tests/Unit/TenantConfiguration/CoverageV2ReadinessBadgeTest.php`, `tests/Feature/Filament/CoverageV2ReadinessPageTest.php`, and `tests/Feature/TenantConfiguration/CoverageV2ReadinessGuardTest.php`. The browser proof uses the planned `tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php` name.
|
||||||
|
|
||||||
|
## Test Governance Checklist
|
||||||
|
|
||||||
|
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||||
|
- [x] New or changed tests stay in Unit/Feature/Browser lanes; any PostgreSQL or heavy-governance addition is explicit.
|
||||||
|
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default and opt-in.
|
||||||
|
- [x] Planned validation commands cover the change without pulling unrelated lane cost.
|
||||||
|
- [x] Browser proof is required because rendered UI changes.
|
||||||
|
- [x] Human Product Sanity and Product Surface close-out are completed in the implementation report.
|
||||||
|
- [x] Material budget, baseline, trend, or escalation notes are recorded if test cost changes.
|
||||||
|
|
||||||
|
## Phase 1: Preflight And Dependencies
|
||||||
|
|
||||||
|
- [x] T001 Capture branch, HEAD, and `git status --short` in `specs/418-coverage-v2-operator-surface/implementation-report.md`.
|
||||||
|
- [x] T002 Confirm `specs/414-tcm-first-coverage-core-cutover/implementation-report.md`, `specs/415-generic-content-backed-capture/implementation-report.md`, and `specs/417-canonical-identity-engine/implementation-report.md` are present and accepted context only; do not modify those packages.
|
||||||
|
- [x] T003 Confirm current Coverage v2 models/services exist: `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, `TenantConfigurationResource`, `TenantConfigurationResourceEvidence`, `ClaimGuard`, and identity/coverage/evidence/claim/source enums.
|
||||||
|
- [x] T004 Inspect current Filament page/table/widget patterns in `apps/platform/app/Filament/Pages`, `apps/platform/app/Filament/Resources`, and `apps/platform/app/Providers/Filament/AdminPanelProvider.php`.
|
||||||
|
- [x] T005 Inspect current workspace/environment/provider authorization helpers and decide whether `Capabilities::EVIDENCE_VIEW`, `Capabilities::TENANT_VIEW`, or a new narrow coverage-readiness capability is the correct gate.
|
||||||
|
- [x] T006 Stop before implementation if any prerequisite from Specs 414/415/417 is missing or if implementation would need customer output, capture start, remote work, v1 adapter, old snapshot promotion, or legacy compatibility.
|
||||||
|
|
||||||
|
## Phase 2: Product Surface Contract Before UI Edits
|
||||||
|
|
||||||
|
- [x] T007 Record Product Surface Impact, affected route, Decision Role, Surface Type, Native Surface classification, primary operator question, default-visible truth, diagnostics boundary, raw evidence boundary, action model, browser proof criteria, and Human Product Sanity criteria in the implementation report draft.
|
||||||
|
- [x] T008 Record the UI Action Matrix for Coverage v2 Readiness: inspect model only, no header mutation actions, no row mutation actions, no bulk actions, no destructive actions, no remote work.
|
||||||
|
- [x] T009 Document the Product Surface Contract Technical Annex surface-budget exception and spread-control rule in the implementation report; explicitly state `UI-EX-001 = none` if the implementation remains native Filament, or stop and name a catalogued UI-EX-001 exception before custom UI work.
|
||||||
|
- [x] T010 Update `docs/ui-ux-enterprise-audit/route-inventory.md` and `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`, apply `docs/product/standards/list-surface-review-checklist.md`, and record the checklist result or documented exception in the implementation report.
|
||||||
|
|
||||||
|
## Phase 3: Tests First - Read Model And Display Mapping
|
||||||
|
|
||||||
|
- [x] T011 Add `apps/platform/tests/Unit/Support/TenantConfiguration/CoverageV2ReadinessSummaryTest.php` proving summary counts derive from v2 states only.
|
||||||
|
- [x] T012 Add `apps/platform/tests/Unit/Support/TenantConfiguration/CoverageV2ActivationBlockerGroupingTest.php` proving blockers group by `identity_conflict`, `missing_external_id`, `unsupported_identity`, `not_captured`, `permission_blocked`, `source_unavailable`, `schema_unknown`, `capture_failed`, `claim_blocked`, and `beta_experimental`, and that top blocker ordering is deterministic by blocker priority, count descending, then stable key ascending.
|
||||||
|
- [x] T013 Add `apps/platform/tests/Unit/Support/TenantConfiguration/CoverageV2ClaimGuardDisplayMapperTest.php` or repo-equivalent tests proving Claim Guard results map to `Claim allowed`, `Claim limited`, `Claim blocked`, and `Internal only` without customer-ready wording, and that status-like rendered badges use `BadgeCatalog`/`BadgeRenderer` or a central BadgeDomain mapping rather than page-local color/status mapping.
|
||||||
|
- [x] T014 Add a unit or feature assertion proving old labels are not emitted by the read model or display mapper: `Evidence gaps`, `Raw gaps`, `Primary gaps`, `ambiguous_match`, `policy_record_missing`, `foundation_not_policy_backed`, and `meta_fallback`.
|
||||||
|
|
||||||
|
## Phase 4: Tests First - Surface Authorization, Scope, Redaction, And No Remote Render
|
||||||
|
|
||||||
|
- [x] T015 Add `apps/platform/tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceAuthorizationTest.php` covering authorized view, non-member 404, no environment entitlement 404, and missing capability 403.
|
||||||
|
- [x] T016 Add `apps/platform/tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceTest.php` proving resource type registry rows, supported scope, readiness summary, resource instance states, and filters render for an authorized actor.
|
||||||
|
- [x] T017 Add `apps/platform/tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceNoLegacyLabelsTest.php` proving old v1 labels and customer-ready coverage claims are absent from rendered output.
|
||||||
|
- [x] T018 Add `apps/platform/tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceRedactionTest.php` proving raw payloads, normalized payloads, permission context raw JSON, tokens, secrets, authorization headers, raw Graph responses, exception dumps, and unredacted PII are absent.
|
||||||
|
- [x] T019 Add a feature/static guard proving the page render path does not call Graph/TCM/provider clients and no capture/start action is registered.
|
||||||
|
- [x] T020 Add a feature/static guard proving `tenant_id` is not introduced as Coverage v2 ownership truth or read-model query scope.
|
||||||
|
- [x] T021 Add provider connection filter tests proving cross-environment provider connections cannot reveal records or labels.
|
||||||
|
- [x] T022 Add OperationRun diagnostic link tests proving links use the canonical helper, appear only when authorized, and remain secondary diagnostics.
|
||||||
|
|
||||||
|
## Phase 5: DB-Only Read Model
|
||||||
|
|
||||||
|
- [x] T023 Add `apps/platform/app/Services/TenantConfiguration/CoverageV2ReadinessReadModel.php` or repo-equivalent thin query service for summary counts, resource type rows, instance rows, activation blockers, and diagnostics payloads.
|
||||||
|
- [x] T024 Ensure the read model queries existing Coverage v2 tables only and does not create persisted UI summaries, denormalized readiness records, fallback readers, or v1 adapters.
|
||||||
|
- [x] T025 Ensure summary counts include `resource_types_total`, `resources_total`, `content_backed_count`, `identity_conflict_count`, `claim_allowed_count`, `claim_limited_count`, `claim_blocked_count`, `beta_experimental_count`, and `graph_fallback_count`.
|
||||||
|
- [x] T026 Ensure blocker grouping derives from `EvidenceState`, `IdentityState`, `ClaimState`, `SourceClass`, `SupportState`, and capture outcomes rather than old gap taxonomy, with deterministic top-blocker ordering.
|
||||||
|
- [x] T027 Ensure diagnostics are sanitized to reason codes, missing/present identity fields, source class, source contract state, provider provenance, evidence hash, and authorized OperationRun link only.
|
||||||
|
- [x] T028 If query cost requires an index, add a narrow reversible migration with a documented query path and PostgreSQL validation; otherwise document no migration.
|
||||||
|
|
||||||
|
## Phase 6: Filament Native Surface
|
||||||
|
|
||||||
|
- [x] T029 Add `apps/platform/app/Filament/Pages/TenantConfiguration/CoverageV2Readiness.php` or repo-equivalent Filament Page at `/admin/tenant-configuration/coverage-v2`.
|
||||||
|
- [x] T030 Add native summary widgets/tables under `apps/platform/app/Filament/Widgets/TenantConfiguration/` or a repo-equivalent native Filament structure for readiness summary, activation blockers, resource types, and resource instances.
|
||||||
|
- [x] T031 Add the minimal Blade wrapper only if required by Filament page composition, e.g. `apps/platform/resources/views/filament/pages/tenant-configuration/coverage-v2-readiness.blade.php`; do not build fake-native request UI.
|
||||||
|
- [x] T032 Register the page in `apps/platform/app/Providers/Filament/AdminPanelProvider.php` or rely on existing discovery if repo conventions support it; do not move provider registration from `apps/platform/bootstrap/providers.php`.
|
||||||
|
- [x] T033 Add a secondary navigation entry only if it fits repo IA; it must not replace Evidence Overview, Baseline Compare, Customer Review Workspace, Review Packs, Reports, or Restore Readiness.
|
||||||
|
- [x] T034 Implement scope summary: workspace, managed environment, supported scope, provider connection filter, source class filter, and last captured/updated time.
|
||||||
|
- [x] T035 Implement readiness summary with compact counts and deterministically ordered top activation blockers.
|
||||||
|
- [x] T036 Implement resource type table columns and filters from `spec.md`.
|
||||||
|
- [x] T037 Implement resource instance table columns and filters from `spec.md`; require managed environment scope for instance rows unless safe entitled workspace-wide aggregation is implemented and tested.
|
||||||
|
- [x] T038 Implement diagnostics disclosure using native infolists/sections/slide-over where possible.
|
||||||
|
- [x] T039 Ensure each table/detail surface has exactly one inspect/open model and no redundant View action beside row click or linked primary column.
|
||||||
|
- [x] T040 Ensure empty states explain missing environment/filter/capture state and do not leak inaccessible environments or provider connections.
|
||||||
|
|
||||||
|
## Phase 7: Authorization And Scope
|
||||||
|
|
||||||
|
- [x] T041 Enforce workspace membership before rendering and return 404 for non-members.
|
||||||
|
- [x] T042 Enforce managed environment entitlement and return 404 when the actor is not entitled to the requested environment.
|
||||||
|
- [x] T043 Enforce the selected view capability and return 403 when membership and entitlement exist but capability is missing.
|
||||||
|
- [x] T044 Ensure provider connection filters and rows are same-workspace and same-managed-environment.
|
||||||
|
- [x] T045 Ensure workspace-wide mode, if implemented, aggregates only across environments the actor is entitled to view.
|
||||||
|
- [x] T046 If a new capability is required, add it to `apps/platform/app/Support/Auth/Capabilities.php`, update role mapping in the repo-equivalent capability map, and add policy/capability tests.
|
||||||
|
|
||||||
|
## Phase 8: Claim Safety, Redaction, No-Legacy, And No-Remote Guards
|
||||||
|
|
||||||
|
- [x] T047 Display Claim Guard results only as internal/operator labels: `Claim allowed`, `Claim limited`, `Claim blocked`, `Internal only`; use central badge/status primitives for status-like rendering.
|
||||||
|
- [x] T048 Block unscoped 100% claims and all customer-facing phrases forbidden by `spec.md`.
|
||||||
|
- [x] T049 Hide raw payload, normalized payload, permission context raw JSON, tokens, secrets, PII, raw Graph responses, raw exception messages, and stack traces.
|
||||||
|
- [x] T050 Ensure old v1 labels never appear in page, view model, diagnostics, filters, empty states, browser fixture copy, or tests as active UI truth.
|
||||||
|
- [x] T051 Ensure no Graph/TCM/provider remote call can execute during render, table columns, badges, filters, or diagnostics disclosure.
|
||||||
|
- [x] T052 Ensure no start capture, sync, restore, publish, export, certify, apply, identity re-evaluate, or manual claim override action is added.
|
||||||
|
|
||||||
|
## Phase 9: Browser Smoke
|
||||||
|
|
||||||
|
- [x] T053 Add `apps/platform/tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php`.
|
||||||
|
- [x] T054 Browser smoke must load the route as an authorized actor without console, Livewire, Filament, network, or 500 errors.
|
||||||
|
- [x] T055 Browser smoke must assert visible labels: `Coverage level`, `Evidence state`, `Identity state`, `Claim state`, `Source class`, and `Supported scope`.
|
||||||
|
- [x] T056 Browser smoke must assert absence of `Evidence gaps`, `Raw gaps`, `policy_record_missing`, `foundation_not_policy_backed`, `meta_fallback`, `ambiguous_match`, `raw payload`, and customer-ready coverage claims.
|
||||||
|
- [x] T057 If browser environment is unavailable, document the exact blocker and do not mark browser proof as PASS without an accepted no-browser exception.
|
||||||
|
|
||||||
|
## Phase 10: Validation And Close-Out
|
||||||
|
|
||||||
|
- [x] T058 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
|
||||||
|
- [x] T059 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/CoverageV2ReadinessSummaryTest.php tests/Unit/Support/TenantConfiguration/CoverageV2ActivationBlockerGroupingTest.php tests/Unit/Support/TenantConfiguration/CoverageV2ClaimGuardDisplayMapperTest.php`.
|
||||||
|
- [x] T060 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceAuthorizationTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceNoLegacyLabelsTest.php tests/Feature/TenantConfiguration/Spec418CoverageV2OperatorSurfaceRedactionTest.php`.
|
||||||
|
- [x] T061 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec418CoverageV2OperatorSurfaceSmokeTest.php`.
|
||||||
|
- [x] T062 If migrations/indexes were added, run the focused PostgreSQL lane for affected TenantConfiguration tests.
|
||||||
|
- [x] T063 Run `git diff --check`.
|
||||||
|
- [x] T064 Complete `specs/418-coverage-v2-operator-surface/implementation-report.md` with candidate gate result, dirty state before/after, files changed, route/surface, Product Surface classification, UI Action Matrix, browser proof, Human Product Sanity, authorization proof, redaction proof, no remote render proof, no-tenant_id confirmation, no-legacy/no-dual-truth confirmation, tests, deployment impact, and deferred work.
|
||||||
|
- [x] T065 Confirm no completed historical spec was rewritten or stripped of close-out, validation, task, smoke, browser, or review history.
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
Stop and update `spec.md`, `plan.md`, and `tasks.md` before continuing if any of these appear:
|
||||||
|
|
||||||
|
- A customer-facing Coverage v2 claim, Review Pack/report output, Customer Review Workspace output, Evidence Overview conversion, Baseline Compare conversion, or Restore Readiness conversion is needed.
|
||||||
|
- A capture/start, sync, restore, apply, certify, publish, export, identity re-evaluate, or manual claim override action is needed.
|
||||||
|
- Graph/TCM/provider remote work is needed during page render.
|
||||||
|
- Raw payloads, normalized payloads, permission context raw JSON, tokens, secrets, PII, raw provider responses, or raw exception dumps need to render.
|
||||||
|
- Old v1 gap vocabulary appears as current UI truth.
|
||||||
|
- `tenant_id` is introduced as Coverage v2 ownership truth.
|
||||||
|
- A v1-to-v2 adapter, fallback reader, old snapshot promotion, dual write, or fallback-to-latest proof path is introduced.
|
||||||
|
- Provider connection filtering can reveal cross-workspace or cross-environment records.
|
||||||
|
- Page-local status-like badge/color/icon semantics are introduced instead of `BadgeCatalog`/`BadgeRenderer` or central BadgeDomain mapping.
|
||||||
|
- Custom UI is needed but no catalogued UI-EX-001 exception is named before implementation.
|
||||||
|
- Browser proof is missing without an accepted no-browser exception.
|
||||||
Loading…
Reference in New Issue
Block a user