chore: commit workspace changes
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m44s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m44s
This commit is contained in:
parent
867bd92370
commit
8fce9f0af1
223
apps/platform/app/Support/OpsUx/OperationRunProgressContract.php
Normal file
223
apps/platform/app/Support/OpsUx/OperationRunProgressContract.php
Normal file
@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\OpsUx;
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Support\OperationRunStatus;
|
||||
|
||||
final class OperationRunProgressContract
|
||||
{
|
||||
public const string NONE = 'none';
|
||||
|
||||
public const string ACTIVITY = 'activity';
|
||||
|
||||
public const string COUNTED = 'counted';
|
||||
|
||||
public const string PHASED = 'phased';
|
||||
|
||||
public const string COMPOSITE = 'composite';
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* capability: string,
|
||||
* display: string,
|
||||
* label: ?string,
|
||||
* processed: ?int,
|
||||
* total: ?int,
|
||||
* percent: ?int
|
||||
* }
|
||||
*/
|
||||
public static function forRun(OperationRun $run): array
|
||||
{
|
||||
$summaryCounts = SummaryCountsNormalizer::normalize(
|
||||
is_array($run->summary_counts) ? $run->summary_counts : [],
|
||||
);
|
||||
|
||||
$context = is_array($run->context) ? $run->context : [];
|
||||
$capability = self::capabilityForRun($run, $summaryCounts, $context);
|
||||
|
||||
return match ($capability) {
|
||||
self::COUNTED => self::countedModel($summaryCounts),
|
||||
self::PHASED => self::indeterminateModel(self::PHASED, 'Phase progress pending.'),
|
||||
self::COMPOSITE => self::indeterminateModel(self::COMPOSITE, 'Composite progress pending.'),
|
||||
self::ACTIVITY => self::indeterminateModel(
|
||||
self::ACTIVITY,
|
||||
(string) $run->status === OperationRunStatus::Queued->value
|
||||
? 'Waiting for worker.'
|
||||
: 'Progress details pending.',
|
||||
),
|
||||
default => self::noneModel(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $summaryCounts
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private static function capabilityForRun(OperationRun $run, array $summaryCounts, array $context): string
|
||||
{
|
||||
if (! $run->isCurrentlyActive()) {
|
||||
return self::NONE;
|
||||
}
|
||||
|
||||
if ((string) $run->status === OperationRunStatus::Queued->value) {
|
||||
return self::ACTIVITY;
|
||||
}
|
||||
|
||||
if (self::hasPhasedHint($context)) {
|
||||
return self::PHASED;
|
||||
}
|
||||
|
||||
if (self::hasCompositeHint($summaryCounts, $context)) {
|
||||
return self::COMPOSITE;
|
||||
}
|
||||
|
||||
if (self::hasCountedHint($summaryCounts)) {
|
||||
return self::COUNTED;
|
||||
}
|
||||
|
||||
return self::ACTIVITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $summaryCounts
|
||||
*/
|
||||
private static function hasCountedHint(array $summaryCounts): bool
|
||||
{
|
||||
$total = $summaryCounts['total'] ?? null;
|
||||
$processed = $summaryCounts['processed'] ?? null;
|
||||
|
||||
return is_int($total)
|
||||
&& $total > 0
|
||||
&& is_int($processed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $summaryCounts
|
||||
* @return array{
|
||||
* capability: string,
|
||||
* display: string,
|
||||
* label: string,
|
||||
* processed: int,
|
||||
* total: int,
|
||||
* percent: int
|
||||
* }
|
||||
*/
|
||||
private static function countedModel(array $summaryCounts): array
|
||||
{
|
||||
$total = max(1, (int) ($summaryCounts['total'] ?? 0));
|
||||
$processed = min(max(0, (int) ($summaryCounts['processed'] ?? 0)), $total);
|
||||
$percent = max(0, min(100, (int) round(($processed / $total) * 100)));
|
||||
|
||||
return [
|
||||
'capability' => self::COUNTED,
|
||||
'display' => self::COUNTED,
|
||||
'label' => sprintf('%d / %d processed (%d%%)', $processed, $total, $percent),
|
||||
'processed' => $processed,
|
||||
'total' => $total,
|
||||
'percent' => $percent,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* capability: string,
|
||||
* display: string,
|
||||
* label: string,
|
||||
* processed: null,
|
||||
* total: null,
|
||||
* percent: null
|
||||
* }
|
||||
*/
|
||||
private static function indeterminateModel(string $capability, string $label): array
|
||||
{
|
||||
return [
|
||||
'capability' => $capability,
|
||||
'display' => 'indeterminate',
|
||||
'label' => $label,
|
||||
'processed' => null,
|
||||
'total' => null,
|
||||
'percent' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* capability: string,
|
||||
* display: string,
|
||||
* label: null,
|
||||
* processed: null,
|
||||
* total: null,
|
||||
* percent: null
|
||||
* }
|
||||
*/
|
||||
private static function noneModel(): array
|
||||
{
|
||||
return [
|
||||
'capability' => self::NONE,
|
||||
'display' => self::NONE,
|
||||
'label' => null,
|
||||
'processed' => null,
|
||||
'total' => null,
|
||||
'percent' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private static function hasPhasedHint(array $context): bool
|
||||
{
|
||||
foreach (['baseline_capture.evidence_capture', 'baseline_compare.evidence_capture'] as $path) {
|
||||
$phaseStats = data_get($context, $path);
|
||||
|
||||
if (is_array($phaseStats) && self::looksLikePhaseStats($phaseStats)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $phaseStats
|
||||
*/
|
||||
private static function looksLikePhaseStats(array $phaseStats): bool
|
||||
{
|
||||
return count(array_intersect(
|
||||
array_keys($phaseStats),
|
||||
['requested', 'succeeded', 'skipped', 'failed', 'throttled'],
|
||||
)) >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $summaryCounts
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private static function hasCompositeHint(array $summaryCounts, array $context): bool
|
||||
{
|
||||
$operationCount = $summaryCounts['operation_count'] ?? null;
|
||||
|
||||
if (is_int($operationCount) && $operationCount > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (['child_run_ids', 'operation_run_ids'] as $path) {
|
||||
$runIds = data_get($context, $path);
|
||||
|
||||
if (! is_array($runIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$numericIds = array_filter($runIds, static fn (mixed $runId): bool => is_numeric($runId));
|
||||
|
||||
if (count($numericIds) > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -33,11 +33,13 @@ @layer components {
|
||||
}
|
||||
|
||||
.tp-ops-activity-banner .tp-ops-activity-helper {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
line-height: 1.35;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
overflow-wrap: break-word;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.tp-ops-activity-banner .tp-ops-activity-summary {
|
||||
@ -76,7 +78,7 @@ @layer components {
|
||||
}
|
||||
|
||||
.tp-ops-activity-banner .tp-ops-activity-helper {
|
||||
max-width: 48rem;
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.tp-ops-activity-banner .tp-ops-activity-actions {
|
||||
|
||||
@ -12,10 +12,13 @@
|
||||
$operationsCollectionLabel = \App\Support\OperationRunLinks::collectionLabel();
|
||||
$operationsIndexUrl = $tenant ? \App\Support\OpsUx\OperationRunUrl::index($tenant) : null;
|
||||
$primaryActionLabel = $usesCollectivePrimaryAction ? 'Review operations' : 'View operation';
|
||||
$bannerTitle = $hasTerminalVisibleRuns ? 'Operation updates' : 'Active operations';
|
||||
$bannerHelper = $hasTerminalVisibleRuns
|
||||
? 'Recent operation updates stay inside the tenant shell until you need the diagnostics view.'
|
||||
: 'Queued and running work stays inside the tenant shell until you need the diagnostics view.';
|
||||
$bannerTitle = $hasActiveVisibleRuns ? 'Active operations' : 'Operation updates';
|
||||
$bannerHelper = match (true) {
|
||||
$hasActiveVisibleRuns && $hasTerminalFollowUpVisibleRuns => 'Active and recent operation updates that may need review.',
|
||||
$hasActiveVisibleRuns => 'Queued and running work stays here until diagnostics are needed.',
|
||||
$hasTerminalFollowUpVisibleRuns => 'Recent operation updates that may need review.',
|
||||
default => 'Recent operation updates.',
|
||||
};
|
||||
$primaryActionUrl = null;
|
||||
|
||||
if ($usesCollectivePrimaryAction) {
|
||||
@ -57,9 +60,9 @@ class="tp-ops-activity-banner mt-5 mb-7 w-full rounded-xl border border-gray-200
|
||||
<x-filament::icon icon="heroicon-m-bolt" class="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 lg:flex lg:items-start lg:gap-3">
|
||||
<div class="tp-ops-activity-copy-stack min-w-0 flex-1 space-y-1">
|
||||
<p class="tp-ops-activity-title text-xs font-semibold uppercase tracking-[0.12em] whitespace-nowrap text-primary-700 dark:text-primary-300" data-testid="ops-ux-activity-feedback-title">{{ $bannerTitle }}</p>
|
||||
<p class="tp-ops-activity-helper mt-1 max-w-3xl text-[13px] leading-[1.45] text-gray-500 lg:mt-0 dark:text-gray-400" data-testid="ops-ux-activity-feedback-helper">{{ $bannerHelper }}</p>
|
||||
<p class="tp-ops-activity-helper text-[13px] leading-[1.45] whitespace-normal break-words text-gray-500 dark:text-gray-400" data-testid="ops-ux-activity-feedback-helper">{{ $bannerHelper }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -104,26 +107,14 @@ class="inline-flex items-center justify-center rounded-lg border border-transpar
|
||||
@php
|
||||
$uxStatus = \App\Support\OpsUx\OperationStatusNormalizer::toUxStatus($run->status, $run->outcome);
|
||||
$isTerminalRun = ! $run->isCurrentlyActive();
|
||||
$summaryCounts = is_array($run->summary_counts) ? $run->summary_counts : [];
|
||||
$hasDeterminateProgress = ! $isTerminalRun
|
||||
&& $run->status !== 'queued'
|
||||
&& is_numeric($summaryCounts['total'] ?? null)
|
||||
&& is_numeric($summaryCounts['processed'] ?? null)
|
||||
&& (int) $summaryCounts['total'] > 0;
|
||||
$progress = \App\Support\OpsUx\OperationRunProgressContract::forRun($run);
|
||||
$hasDeterminateProgress = $progress['display'] === 'counted';
|
||||
$lifecycleAttention = \App\Support\OpsUx\OperationUxPresenter::lifecycleAttentionSummary($run);
|
||||
$showsLifecycleAttention = $lifecycleAttention !== null
|
||||
&& ($lifecycleAttention !== 'Likely stale' || $run->status === 'queued' || ! $hasDeterminateProgress);
|
||||
$progressTotal = $hasDeterminateProgress ? max(1, (int) $summaryCounts['total']) : null;
|
||||
$progressProcessed = $hasDeterminateProgress
|
||||
? min(max(0, (int) $summaryCounts['processed']), $progressTotal)
|
||||
: null;
|
||||
$progressPercent = $hasDeterminateProgress
|
||||
? max(0, min(100, (int) round(($progressProcessed / $progressTotal) * 100)))
|
||||
: null;
|
||||
$progressLabel = $hasDeterminateProgress
|
||||
? sprintf('%d / %d processed (%d%%)', $progressProcessed, $progressTotal, $progressPercent)
|
||||
: null;
|
||||
$showsIndeterminateProgress = ! $isTerminalRun && ! $hasDeterminateProgress;
|
||||
$progressLabel = $progress['label'];
|
||||
$progressPercent = $progress['percent'];
|
||||
$showsIndeterminateProgress = $progress['display'] === 'indeterminate';
|
||||
$statusLabel = match ($uxStatus) {
|
||||
'queued' => 'Queued for execution',
|
||||
'running' => 'In progress',
|
||||
@ -139,9 +130,6 @@ class="inline-flex items-center justify-center rounded-lg border border-transpar
|
||||
'partial', 'blocked' => 'bg-warning-50 text-warning-800 ring-1 ring-inset ring-warning-200 dark:bg-warning-500/10 dark:text-warning-100 dark:ring-warning-400/25',
|
||||
default => 'bg-danger-50 text-danger-800 ring-1 ring-inset ring-danger-200 dark:bg-danger-500/10 dark:text-danger-100 dark:ring-danger-400/25',
|
||||
};
|
||||
$activityLabel = $run->status === 'queued'
|
||||
? 'Waiting for worker.'
|
||||
: 'Progress details pending.';
|
||||
$elapsedLabel = \App\Support\OpsUx\RunDurationInsights::elapsedCompact($run);
|
||||
$surfaceGuidance = \App\Support\OpsUx\OperationUxPresenter::surfaceGuidance($run);
|
||||
$activitySummaryLine = $isTerminalRun
|
||||
@ -150,7 +138,7 @@ class="inline-flex items-center justify-center rounded-lg border border-transpar
|
||||
'%s · %s · %s',
|
||||
$run->status === 'queued' ? 'Queued' : 'Running',
|
||||
$elapsedLabel,
|
||||
$progressLabel ?? $activityLabel,
|
||||
$progressLabel ?? 'Progress details pending.',
|
||||
);
|
||||
@endphp
|
||||
|
||||
|
||||
@ -78,10 +78,11 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, Tenant $tenant, stri
|
||||
const layout = banner?.querySelector('.tp-ops-activity-layout') ?? null;
|
||||
const headerCopy = banner?.querySelector('.tp-ops-activity-header-copy') ?? null;
|
||||
const title = banner?.querySelector('[data-testid="ops-ux-activity-feedback-title"]') ?? null;
|
||||
const helper = banner?.querySelector('[data-testid="ops-ux-activity-feedback-helper"]') ?? null;
|
||||
const middleColumn = banner?.querySelector('.tp-ops-activity-summary') ?? null;
|
||||
const track = banner?.querySelector('[data-testid="ops-ux-activity-feedback-track"]') ?? null;
|
||||
|
||||
if (!banner || !topbar || !tableContainer || !contentShell || !actionGroup || !layout || !header || !headerCopy || !title || !middleColumn || !track) {
|
||||
if (!banner || !topbar || !tableContainer || !contentShell || !actionGroup || !layout || !header || !headerCopy || !title || !helper || !middleColumn || !track) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -93,12 +94,15 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, Tenant $tenant, stri
|
||||
const actionGroupRect = actionGroup.getBoundingClientRect();
|
||||
const headerCopyRect = headerCopy.getBoundingClientRect();
|
||||
const titleRect = title.getBoundingClientRect();
|
||||
const helperRect = helper.getBoundingClientRect();
|
||||
const middleRect = middleColumn.getBoundingClientRect();
|
||||
const trackRect = track.getBoundingClientRect();
|
||||
const layoutStyle = window.getComputedStyle(layout);
|
||||
const headerStyle = window.getComputedStyle(header);
|
||||
const titleStyle = window.getComputedStyle(title);
|
||||
const helperStyle = window.getComputedStyle(helper);
|
||||
const titleLineHeight = Number.parseFloat(titleStyle.lineHeight || '0');
|
||||
const helperLineHeight = Number.parseFloat(helperStyle.lineHeight || '0');
|
||||
|
||||
return {
|
||||
viewportWidth: window.innerWidth,
|
||||
@ -125,6 +129,13 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, Tenant $tenant, stri
|
||||
trackSpanRatio: middleRect.width > 0 ? trackRect.width / middleRect.width : 0,
|
||||
titleOverflows: title.scrollWidth > (title.clientWidth + 1),
|
||||
titleLineCount: titleLineHeight > 0 ? titleRect.height / titleLineHeight : 0,
|
||||
helperText: helper.textContent?.trim() ?? '',
|
||||
helperDisplay: helperStyle.display,
|
||||
helperWhiteSpace: helperStyle.whiteSpace,
|
||||
helperTextOverflow: helperStyle.textOverflow,
|
||||
helperLineClamp: helperStyle.getPropertyValue('-webkit-line-clamp'),
|
||||
helperOverflows: helper.scrollWidth > (helper.clientWidth + 1),
|
||||
helperLineCount: helperLineHeight > 0 ? helperRect.height / helperLineHeight : 0,
|
||||
headerCopyWidth: headerCopyRect.width,
|
||||
middleHeight: middleRect.height,
|
||||
actionHeight: actionGroupRect.height,
|
||||
@ -153,6 +164,13 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, Tenant $tenant, stri
|
||||
->and($shellGeometry['trackSpanRatio'] ?? 0)->toBeGreaterThanOrEqual(0.72)
|
||||
->and($shellGeometry['titleOverflows'] ?? true)->toBeFalse()
|
||||
->and($shellGeometry['titleLineCount'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(1.2)
|
||||
->and($shellGeometry['helperText'] ?? '')->toBe('Queued and running work stays here until diagnostics are needed.')
|
||||
->and($shellGeometry['helperDisplay'] ?? '')->not->toBe('-webkit-box')
|
||||
->and($shellGeometry['helperWhiteSpace'] ?? '')->toBe('normal')
|
||||
->and($shellGeometry['helperTextOverflow'] ?? '')->not->toBe('ellipsis')
|
||||
->and($shellGeometry['helperLineClamp'] ?? '')->not->toBe('2')
|
||||
->and($shellGeometry['helperOverflows'] ?? true)->toBeFalse()
|
||||
->and($shellGeometry['helperLineCount'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(2.2)
|
||||
->and($shellGeometry['headerCopyWidth'] ?? 0)->toBeGreaterThanOrEqual(240)
|
||||
->and($shellGeometry['leftDelta'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(32)
|
||||
->and($shellGeometry['rightDelta'] ?? PHP_INT_MAX)->toBeLessThanOrEqual(32)
|
||||
@ -195,4 +213,59 @@ function operationActivityFeedbackSmokeLoginUrl(User $user, Tenant $tenant, stri
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->assertDontSee('Show activity');
|
||||
});
|
||||
|
||||
it('shows repo-real phased work as indeterminate activity and still opens the canonical run detail', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
InventoryItem::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'display_name' => 'Browser Progress Inventory Item',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'platform' => 'windows',
|
||||
'last_seen_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'type' => 'baseline_capture',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 4,
|
||||
],
|
||||
'context' => [
|
||||
'baseline_capture' => [
|
||||
'evidence_capture' => [
|
||||
'requested' => 10,
|
||||
'succeeded' => 3,
|
||||
'skipped' => 1,
|
||||
],
|
||||
'resume_token' => 'resume-browser-123',
|
||||
],
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
visit(operationActivityFeedbackSmokeLoginUrl($user, $tenant))
|
||||
->waitForText('Dashboard')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
visit(InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant))
|
||||
->resize(1440, 1200)
|
||||
->waitForText('Inventory Items')
|
||||
->waitForText('Phase progress pending.')
|
||||
->assertSee('View operation')
|
||||
->assertDontSee('4 / 10 processed (40%)')
|
||||
->assertScript("document.querySelector('[data-testid=\"ops-ux-activity-feedback-indeterminate\"]') !== null", true)
|
||||
->assertScript("document.querySelector('[role=\"progressbar\"]') === null", true)
|
||||
->click('View operation')
|
||||
->waitForText('Operation #'.(int) $run->getKey())
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
});
|
||||
@ -36,9 +36,11 @@
|
||||
->call('refreshRuns');
|
||||
|
||||
$html = $component->html();
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect(substr_count($html, 'data-testid="ops-ux-activity-feedback-item"'))->toBe(3)
|
||||
->and($html)->toContain('Active operations')
|
||||
->and($pageText)->toContain('Queued and running work stays here until diagnostics are needed.')
|
||||
->and(substr_count($html, 'Review operations'))->toBe(1)
|
||||
->and(substr_count($html, 'View operation'))->toBe(0)
|
||||
->and($html)->toContain('data-testid="ops-ux-activity-feedback-actions"')
|
||||
@ -73,9 +75,11 @@
|
||||
->call('refreshRuns');
|
||||
|
||||
$html = $component->html();
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect(substr_count($html, 'data-testid="ops-ux-activity-feedback-item"'))->toBe(1)
|
||||
->and($html)->toContain('Active operations')
|
||||
->and($pageText)->toContain('Queued and running work stays here until diagnostics are needed.')
|
||||
->and(substr_count($html, 'View operation'))->toBe(1)
|
||||
->and($html)->not->toContain('Review operations')
|
||||
->and($html)->toContain(OperationRunUrl::view($run, $tenant))
|
||||
@ -115,6 +119,7 @@
|
||||
|
||||
expect($component->get('hasActiveRuns'))->toBeFalse()
|
||||
->and($html)->toContain('Operation updates')
|
||||
->and($pageText)->toContain('Recent operation updates.')
|
||||
->and($pageText)->toContain('Completed successfully')
|
||||
->and($pageText)->toContain('No action needed.')
|
||||
->and($html)->toContain('View operation')
|
||||
@ -147,9 +152,11 @@
|
||||
->call('refreshRuns');
|
||||
|
||||
$html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5);
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect($component->get('hasActiveRuns'))->toBeFalse()
|
||||
->and($html)->toContain('Operation updates')
|
||||
->and($pageText)->toContain('Recent operation updates that may need review.')
|
||||
->and($html)->toContain('View operation')
|
||||
->and($html)->not->toContain('Review operations')
|
||||
->and($html)->toContain('Dismiss')
|
||||
@ -194,7 +201,8 @@
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect($html)->toContain('Review operations')
|
||||
->and($html)->toContain('Operation updates')
|
||||
->and($html)->toContain('Active operations')
|
||||
->and($pageText)->toContain('Active and recent operation updates that may need review.')
|
||||
->and($html)->not->toContain('View operation')
|
||||
->and($html)->toContain('Hide activity')
|
||||
->and($html)->toContain(OperationRunUrl::index($tenant))
|
||||
@ -274,6 +282,83 @@
|
||||
->and($html)->not->toContain('Likely stale');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('renders phased fallback progress without inventing a counted percentage', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'type' => 'baseline_capture',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 4,
|
||||
],
|
||||
'context' => [
|
||||
'baseline_capture' => [
|
||||
'evidence_capture' => [
|
||||
'requested' => 10,
|
||||
'succeeded' => 3,
|
||||
'skipped' => 1,
|
||||
],
|
||||
'resume_token' => 'resume-123',
|
||||
],
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->call('refreshRuns');
|
||||
|
||||
$html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5);
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect($html)->toContain('data-testid="ops-ux-activity-feedback-indeterminate"')
|
||||
->and($pageText)->toMatch('/Running · .* · Phase progress pending\./')
|
||||
->and($html)->not->toContain('role="progressbar"')
|
||||
->and(strip_tags($html))->not->toContain('processed (');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('renders composite fallback progress without inventing a counted percentage from aggregate counts', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'type' => 'cross_tenant_promotion.execute',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 4,
|
||||
'operation_count' => 3,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->call('refreshRuns');
|
||||
|
||||
$html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5);
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect($html)->toContain('data-testid="ops-ux-activity-feedback-indeterminate"')
|
||||
->and($pageText)->toMatch('/Running · .* · Composite progress pending\./')
|
||||
->and($html)->not->toContain('role="progressbar"')
|
||||
->and(strip_tags($html))->not->toContain('processed (');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('renders an indeterminate queued indicator without fake determinate progress', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
@ -310,6 +395,40 @@
|
||||
->and(strip_tags($html))->not->toContain('processed (');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('keeps outcome counters outcome only at the shell host', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'type' => 'inventory_sync',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'succeeded' => 4,
|
||||
'failed' => 1,
|
||||
'skipped' => 2,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->call('refreshRuns');
|
||||
|
||||
$html = html_entity_decode($component->html(), ENT_QUOTES | ENT_HTML5);
|
||||
$pageText = preg_replace('/\s+/', ' ', strip_tags($html));
|
||||
|
||||
expect($html)->toContain('data-testid="ops-ux-activity-feedback-indeterminate"')
|
||||
->and($pageText)->toMatch('/Running · .* · Progress details pending\./')
|
||||
->and($html)->not->toContain('role="progressbar"')
|
||||
->and(strip_tags($html))->not->toContain('processed (');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('keeps the queued status pill on one line for the compact banner layout', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
|
||||
@ -45,6 +45,55 @@
|
||||
});
|
||||
})->group('ops-ux');
|
||||
|
||||
it('stays inert when no selected tenant context exists', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'type' => 'inventory_sync',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->call('refreshRuns')
|
||||
->assertSet('disabled', true)
|
||||
->assertSet('hasActiveRuns', false)
|
||||
->assertDontSee('Inventory sync');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('keeps queued shell hydration indeterminate even when a planned total exists', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'type' => 'inventory_sync',
|
||||
'status' => 'queued',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 4,
|
||||
],
|
||||
'created_at' => now()->subSeconds(20),
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->call('refreshRuns')
|
||||
->assertSee('Waiting for worker.')
|
||||
->assertSee('Queued for execution')
|
||||
->assertDontSee('4 / 10 processed (40%)')
|
||||
->assertDontSeeHtml('aria-valuenow=');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('keeps a just-completed successful run visible briefly as terminal success', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$this->actingAs($user);
|
||||
@ -170,6 +219,38 @@
|
||||
->assertSee('Waiting for worker.');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('clamps counted progress at the shell host when processed exceeds total', function () {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->set('tenantId', (int) $tenant->getKey())
|
||||
->call('refreshRuns')
|
||||
->assertDontSee('10 / 10 processed (100%)');
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'type' => 'inventory_sync',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 15,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(BulkOperationProgress::class)
|
||||
->call('refreshRuns')
|
||||
->assertSee('10 / 10 processed (100%)')
|
||||
->assertDontSee('15 / 10 processed (150%)')
|
||||
->assertSeeHtml('aria-valuenow="100"');
|
||||
})->group('ops-ux');
|
||||
|
||||
it('registers Alpine cleanup for the Ops UX poller to avoid stale listeners across re-renders', function () {
|
||||
$contents = file_get_contents(resource_path('views/livewire/bulk-operation-progress.blade.php'));
|
||||
|
||||
|
||||
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Support\OpsUx\OperationRunProgressContract;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('returns counted progress for running runs with trustworthy processed totals', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 4,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('counted')
|
||||
->and($progress['display'])->toBe('counted')
|
||||
->and($progress['processed'])->toBe(4)
|
||||
->and($progress['total'])->toBe(10)
|
||||
->and($progress['percent'])->toBe(40)
|
||||
->and($progress['label'])->toBe('4 / 10 processed (40%)');
|
||||
});
|
||||
|
||||
it('clamps counted progress into a truthful visible range', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 15,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('counted')
|
||||
->and($progress['processed'])->toBe(10)
|
||||
->and($progress['total'])->toBe(10)
|
||||
->and($progress['percent'])->toBe(100)
|
||||
->and($progress['label'])->toBe('10 / 10 processed (100%)');
|
||||
});
|
||||
|
||||
it('keeps queued runs activity only even when planned totals exist', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'status' => 'queued',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('activity')
|
||||
->and($progress['display'])->toBe('indeterminate')
|
||||
->and($progress['label'])->toBe('Waiting for worker.')
|
||||
->and($progress['percent'])->toBeNull();
|
||||
});
|
||||
|
||||
it('returns no progress for terminal runs even when retained counts exist', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'status' => 'completed',
|
||||
'outcome' => 'succeeded',
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 10,
|
||||
],
|
||||
'started_at' => now()->subMinutes(2),
|
||||
'completed_at' => now()->subSecond(),
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('none')
|
||||
->and($progress['display'])->toBe('none')
|
||||
->and($progress['label'])->toBeNull()
|
||||
->and($progress['percent'])->toBeNull();
|
||||
});
|
||||
|
||||
it('does not let outcome counters masquerade as counted progress', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'succeeded' => 4,
|
||||
'failed' => 1,
|
||||
'skipped' => 2,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('activity')
|
||||
->and($progress['display'])->toBe('indeterminate')
|
||||
->and($progress['label'])->toBe('Progress details pending.')
|
||||
->and($progress['percent'])->toBeNull();
|
||||
});
|
||||
|
||||
it('classifies repo-real baseline evidence capture runs as phased fallback', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'type' => 'baseline_capture',
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'context' => [
|
||||
'baseline_capture' => [
|
||||
'evidence_capture' => [
|
||||
'requested' => 10,
|
||||
'succeeded' => 3,
|
||||
'skipped' => 1,
|
||||
],
|
||||
'resume_token' => 'resume-123',
|
||||
],
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('phased')
|
||||
->and($progress['display'])->toBe('indeterminate')
|
||||
->and($progress['label'])->toBe('Phase progress pending.')
|
||||
->and($progress['percent'])->toBeNull();
|
||||
});
|
||||
|
||||
it('classifies aggregate multi-run work as composite fallback', function (): void {
|
||||
$run = OperationRun::factory()->create([
|
||||
'status' => 'running',
|
||||
'outcome' => 'pending',
|
||||
'summary_counts' => [
|
||||
'operation_count' => 3,
|
||||
],
|
||||
'started_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$progress = OperationRunProgressContract::forRun($run);
|
||||
|
||||
expect($progress['capability'])->toBe('composite')
|
||||
->and($progress['display'])->toBe('indeterminate')
|
||||
->and($progress['label'])->toBe('Composite progress pending.')
|
||||
->and($progress['percent'])->toBeNull();
|
||||
});
|
||||
@ -1329,6 +1329,18 @@ # TenantPilot Enterprise UI Standards**Status:** Active **Owner:** Product / En
|
||||
|
||||
show determinate progress only when summary_counts.total and summary_counts.processed are real numeric values, clamp the progressbar to 0-100, and only show processed or percentage text when it is derived from those repo-real values
|
||||
|
||||
derive progress treatment from one shared OperationRun progress contract instead of local Blade or widget math
|
||||
|
||||
keep queued rows activity-only even when a planned total exists
|
||||
|
||||
keep running rows without trustworthy processed and total counts activity-only or indeterminate
|
||||
|
||||
keep summary_counts.succeeded, summary_counts.failed, and summary_counts.skipped outcome-only; they must not silently replace processed as progress truth
|
||||
|
||||
degrade repo-real phase-shaped work such as baseline_capture.evidence_capture or baseline_compare.evidence_capture to a phased indeterminate fallback until a later spec introduces trustworthy phase progress truth
|
||||
|
||||
degrade aggregate multi-run or operation_count shaped work to a composite indeterminate fallback until a later spec introduces trustworthy child-progress truth
|
||||
|
||||
switch terminal-success rows to success-state copy instead of showing active progress after completion
|
||||
|
||||
keep hide or dismiss behavior browser-session-only and re-open the hint when a new run-enqueued event is accepted for the current tenant
|
||||
@ -1343,6 +1355,8 @@ # TenantPilot Enterprise UI Standards**Status:** Active **Owner:** Product / En
|
||||
|
||||
show fake percentages, guessed completion, or fake progress
|
||||
|
||||
let outcome counters, aggregate operation counts, or phase hints masquerade as counted progress
|
||||
|
||||
show active progress UI after a run is already terminal
|
||||
|
||||
persist hide or dismiss state in the database or on the OperationRun record
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
# Specification Quality Checklist: OperationRun Progress Contract v1
|
||||
|
||||
**Purpose**: Validate specification completeness, boundedness, and readiness before implementation
|
||||
**Created**: 2026-05-04
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] The package stays on one bounded shared progress contract over existing `OperationRun` truth instead of widening into counted writer rollout, dashboard redesign, or a second execution framework.
|
||||
- [x] The spec remains product- and behavior-oriented rather than reading like a low-level implementation diff.
|
||||
- [x] The package explicitly names the repo-real anchors it builds on: `SummaryCountsNormalizer`, `OperationSummaryKeys`, `ActiveRuns`, `OperationRunService`, and the current inline progress seam in `bulk-operation-progress.blade.php`.
|
||||
- [x] Mandatory repo sections for scope, shared-pattern reuse, Ops-UX, testing, proportionality, and candidate rationale are completed.
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No unresolved clarification markers remain.
|
||||
- [x] Requirements are testable and bounded to current `OperationRun` truth, one shared progress contract, one visible shell adopter, and one standards update only.
|
||||
- [x] The package explicitly keeps `summary_counts.processed` and `summary_counts.total` as the only v1 determinate progress source.
|
||||
- [x] The package explicitly forbids `succeeded`, `failed`, and `skipped` from silently becoming progress substitutes.
|
||||
- [x] The package declares `phased` and `composite` categories without inventing fake percentages, new persistence, or hidden writer rollout.
|
||||
- [x] The package explains what remains in scope versus what is intentionally deferred to Specs 271, 272, and 273.
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] The selected candidate exists in `docs/product/spec-candidates.md` and is consistent with the broader roadmap direction.
|
||||
- [x] The active queue is explicitly empty, so this package records itself as a deliberate manual promotion rather than an automatic next-best-prep target.
|
||||
- [x] Repo verification explicitly excluded `269 — OperationRun Terminal Outcome Feedback` because `specs/268-operationrun-activity-feedback/` already owns that shell terminal slice.
|
||||
- [x] `273 — Tenant Dashboard Active Operations Summary Card` remains conditional on visible dashboard drift after Spec 268, so it is not a safer prep target than the progress contract.
|
||||
- [x] `271` and `272` remain legitimate follow-ups, but both depend on the shared contract from this package.
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] The package reuses current `OperationRun` truth and current summary-count sanitization instead of introducing a second lifecycle or persisted projection.
|
||||
- [x] The package forbids new panel, provider, global-search, asset-registration, queue-family, and notification-policy changes.
|
||||
- [x] The package preserves the current polling posture and explicitly forbids new parallel polling loops.
|
||||
- [x] The package carries current no-tenant and unauthorized shell-visibility behavior into focused feature-proof tasks instead of assuming auth semantics survive the refactor automatically.
|
||||
- [x] The package names the current inline shell progress math as the concrete repo seam to remove.
|
||||
- [x] The package keeps `OperationRun.status`, `OperationRun.outcome`, `OperationSummaryKeys`, and `SummaryCountsNormalizer` as the current authoritative truth owners.
|
||||
- [x] The planned validation commands stay consistent across `spec.md`, `plan.md`, and `tasks.md`.
|
||||
|
||||
## Test Governance
|
||||
|
||||
- [x] Planned proof stays bounded to one new `Unit` suite plus focused `Feature` suites.
|
||||
- [x] No new heavy-governance or browser family is introduced by default.
|
||||
- [x] Fixture growth remains bounded to current `OperationRun` factories and current Ops-UX test helpers instead of a new matrix harness.
|
||||
- [x] The review outcome, workflow outcome, and test-governance outcome are carried into the active prep package.
|
||||
|
||||
## Notes
|
||||
|
||||
- Reviewed against `.specify/memory/constitution.md`, `.specify/templates/checklist-template.md`, `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, `specs/268-operationrun-activity-feedback/spec.md`, `apps/platform/app/Support/OpsUx/ActiveRuns.php`, `apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php`, `apps/platform/app/Support/OpsUx/OperationSummaryKeys.php`, `apps/platform/app/Services/OperationRunService.php`, and `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php` on 2026-05-04.
|
||||
- This checklist is the prep-time outcome record. If implementation widens into counted writer rollout, dashboard-specific progress work, or a persisted progress mode, the workflow outcome must change before merge.
|
||||
- No application implementation was performed while preparing this package.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- **Outcome class**: `acceptable-special-case`
|
||||
- **Workflow outcome**: `keep`
|
||||
- **Test-governance outcome**: `keep`
|
||||
- **Reason**: the package is a bounded shared-truth contract justified by multiple repo-real consumers, keeps the visible adoption limited to the current shell host, and explicitly defers the heavier rollout work to later manual-promotion specs.
|
||||
- **Final note location**: This checklist during prep, and the active feature PR close-out entry only if implementation later forces `split` or `document-in-feature`.
|
||||
249
specs/270-operationrun-progress-contract/plan.md
Normal file
249
specs/270-operationrun-progress-contract/plan.md
Normal file
@ -0,0 +1,249 @@
|
||||
# Implementation Plan: OperationRun Progress Contract v1
|
||||
|
||||
**Branch**: `270-operationrun-progress-contract` | **Date**: 2026-05-04 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `/specs/270-operationrun-progress-contract/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
This plan prepares one bounded Ops-UX foundation slice over existing `OperationRun` truth. The implementation path is to introduce one shared progress-semantics contract in the current `App\Support\OpsUx` family, move progress-mode decisions out of `bulk-operation-progress.blade.php`, and document the contract in `docs/ui/tenantpilot-enterprise-ui-standards.md`. The slice must stay on existing `OperationRun.status`, `OperationRun.outcome`, `summary_counts`, and `context` truth; it must not widen into counted writer rollout, dashboard redesign, terminal-notification changes, or new persistence.
|
||||
|
||||
Filament remains on Livewire v4, no panel-provider registration changes are required (`apps/platform/bootstrap/providers.php` remains authoritative), no globally searchable resource is added, and no asset registration or deployment step is expected.
|
||||
|
||||
## Inherited Baseline / Explicit Delta
|
||||
|
||||
### Inherited baseline
|
||||
|
||||
- `SummaryCountsNormalizer` and `OperationSummaryKeys` already sanitize and whitelist numeric `summary_counts` values.
|
||||
- `OperationRunService` already owns `summary_counts` writes through `updateRun()`, `incrementSummaryCounts()`, and `maybeCompleteBulkRun()`.
|
||||
- `ActiveRuns` already owns shell-visible run selection and terminal-success grace-window filtering.
|
||||
- `BulkOperationProgress` and `bulk-operation-progress.blade.php` already render the current shell host, but the progress semantics are still decided inline in the Blade view.
|
||||
- `specs/268-operationrun-activity-feedback/` already owns the shell terminal-success and terminal-follow-up slice.
|
||||
- Historical Ops-UX specs already require numeric-only `summary_counts` and preserve the three-surface lifecycle contract.
|
||||
|
||||
### Explicit delta in this plan
|
||||
|
||||
- formalize one shared `OperationRun` progress capability and render-model contract
|
||||
- centralize counted vs activity-only vs terminal no-progress semantics in one Ops-UX helper
|
||||
- move current shell progress logic off inline Blade math and onto that shared contract
|
||||
- document future-safe boundaries for `phased` and `composite` progress without rolling them out yet
|
||||
- leave run-writer rollout, dashboard follow-up work, and phase/composite truth to later specs
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4, Laravel 12, Filament v5, Livewire v4
|
||||
**Primary Dependencies**: current Ops-UX support classes, native Filament widgets/Blade, Pest v4
|
||||
**Storage**: PostgreSQL via existing `operation_runs`; no new persistence
|
||||
**Testing**: Pest Unit + Feature coverage
|
||||
**Validation Lanes**: fast-feedback, confidence
|
||||
**Target Platform**: existing Laravel monolith in `apps/platform`, admin/operator plane only
|
||||
**Project Type**: Web application (Laravel monolith with Filament)
|
||||
**Performance Goals**: no new query families, no extra polling loops, and no slower-than-current shell rendering for active-run feedback
|
||||
**Constraints**: no new `summary_counts` keys, no new run lifecycle, no new persistence, and no browser-only proof requirement in this slice
|
||||
**Scale/Scope**: one shared contract, one shell adopter, one standards update, and focused regression coverage
|
||||
|
||||
## Likely Affected Repo Surfaces
|
||||
|
||||
- `apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php`
|
||||
- `apps/platform/app/Support/OpsUx/OperationSummaryKeys.php`
|
||||
- `apps/platform/app/Support/OpsUx/ActiveRuns.php`
|
||||
- `apps/platform/app/Support/OpsUx/OperationStatusNormalizer.php`
|
||||
- `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`
|
||||
- one new bounded helper under `apps/platform/app/Support/OpsUx/` for progress capability/render semantics
|
||||
- `apps/platform/app/Livewire/BulkOperationProgress.php`
|
||||
- `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php`
|
||||
- `apps/platform/app/Services/OperationRunService.php`
|
||||
- `apps/platform/tests/Unit/Support/OpsUx/...`
|
||||
- `apps/platform/tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php`
|
||||
- `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`
|
||||
- `apps/platform/tests/Feature/OpsUx/SummaryCountsWhitelistTest.php`
|
||||
- `docs/ui/tenantpilot-enterprise-ui-standards.md`
|
||||
|
||||
## UI / Filament & Livewire Fit
|
||||
|
||||
- Keep the changed surface Filament-native. The visible v1 adopter remains the existing Livewire shell host rather than a new page, widget family, or dashboard card.
|
||||
- The shell host stays decision-first. The new contract decides only whether a progress line or bar is truthful; it does not widen the shell into a diagnostics surface.
|
||||
- Monitoring collection/detail pages remain diagnostics-first drill-through targets. This slice prepares their future compatibility by naming one shared contract, not by redesigning their UI.
|
||||
- The current shell host may keep bounded progress text or bars, but it must no longer calculate their eligibility or percentages inline.
|
||||
- No new asset registration, panel configuration, or provider registration change is planned.
|
||||
|
||||
## RBAC / Policy Fit
|
||||
|
||||
- Existing `OperationRun` policies remain the first and only visibility gate.
|
||||
- The progress contract derives output only after the current actor is already entitled to see the run.
|
||||
- Tenant/admin plane behavior stays unchanged: no cross-plane expansion and no new authorization surface.
|
||||
- No new mutation or retry action is introduced, so current confirmation/authorization behavior stays on existing start surfaces and run detail pages.
|
||||
|
||||
## Audit / Logging Fit
|
||||
|
||||
- Existing queued toasts and terminal DB notifications remain authoritative and unchanged.
|
||||
- Existing run audit and Monitoring behavior remain the only audit trail. No new view-level or contract-level audit stream is introduced.
|
||||
- `OperationRun.status` and `OperationRun.outcome` remain service-owned and unchanged.
|
||||
|
||||
## Data & Query Fit
|
||||
|
||||
- The contract derives only from current `OperationRun` truth: `status`, `outcome`, sanitized `summary_counts`, and bounded current `context` where trustworthy phase/composite truth may later exist.
|
||||
- Determinate progress remains limited to current running work with trustworthy numeric `processed` and `total` counters.
|
||||
- Outcome counters remain summary truth only; they are not reinterpreted as progress inputs.
|
||||
- No migration, no new JSON schema, no backfill, and no cache layer are planned.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed surfaces
|
||||
- **Native vs custom classification summary**: native Filament plus a bounded local Ops-UX helper refactor
|
||||
- **Shared-family relevance**: Ops UX start feedback and execution-truth summaries
|
||||
- **State layers in scope**: shell, page
|
||||
- **Audience modes in scope**: operator-MSP
|
||||
- **Decision/diagnostic/raw hierarchy plan**: decision-first on the shell host, diagnostics-first on Operations collection/detail
|
||||
- **Raw/support gating plan**: raw/support evidence stays on the current diagnostics surfaces only
|
||||
- **One-primary-action / duplicate-truth control**: the shell keeps the current dominant `View operation` action and only swaps local progress math for the shared contract; it does not add duplicate progress explanations
|
||||
- **Handling modes by drift class or surface**: review-mandatory
|
||||
- **Repository-signal treatment**: review-mandatory
|
||||
- **Special surface test profiles**: global-context-shell
|
||||
- **Required tests**: functional-core, state-contract
|
||||
- **Exception path and spread control**: none planned; any attempt to add new writer semantics, a new browser family, or dashboard-specific progress logic resolves as `reject-or-split`
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes
|
||||
- **Systems touched**: current shell host, summary-count sanitization, progress disclosure semantics, and UI standards
|
||||
- **Shared abstractions reused**: `SummaryCountsNormalizer`, `OperationSummaryKeys`, `ActiveRuns`, `OperationStatusNormalizer`, `OperationUxPresenter`, current Ops-UX shell host
|
||||
- **New abstraction introduced? why?**: yes, one bounded shared progress contract/helper because the repo already has multiple real consumers and the current progress truth gap cannot stay view-local without drift
|
||||
- **Why the existing abstraction was sufficient or insufficient**: the repo already owns lifecycle normalization and count sanitization, but it does not currently answer progress eligibility or progress mode once and centrally
|
||||
- **Bounded deviation / spread control**: do not create multiple host-specific helpers, a registry, or a persisted progress model
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, for active-surface progress disclosure only
|
||||
- **Central contract reused**: current Ops-UX start contract via `OperationRunLinks`, `OperationRunUrl`, `ActiveRuns`, `OperationStatusNormalizer`, and `OperationUxPresenter`
|
||||
- **Delegated UX behaviors**: queued toast wording, canonical view/collection links, current browser-event dispatch, and existing terminal DB notifications remain delegated to the shared contract and unchanged
|
||||
- **Surface-owned behavior kept local**: bounded shell layout and copy density only
|
||||
- **Queued DB-notification policy**: `N/A` - unchanged
|
||||
- **Terminal notification path**: unchanged central lifecycle mechanism
|
||||
- **Exception path**: none
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no
|
||||
- **Provider-owned seams**: `N/A`
|
||||
- **Platform-core seams**: existing `OperationRun` truth, summary-count vocabulary, and operator-facing execution language only
|
||||
- **Neutral platform terms / contracts preserved**: `Operation`, `activity`, `progress`, `terminal outcome`, `counted progress`
|
||||
- **Retained provider-specific semantics and why**: none
|
||||
- **Bounded extraction or follow-up path**: none
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation begins and again before merge.*
|
||||
|
||||
- Inventory-first: PASS. The slice is fully derived from existing `OperationRun` truth.
|
||||
- Read/write separation: PASS. No new write path or retry surface is introduced.
|
||||
- Graph contract path: PASS. No Graph/provider interaction is added.
|
||||
- Deterministic capabilities: PASS. Existing `OperationRun` policies remain authoritative.
|
||||
- RBAC-UX: PASS. No plane expansion; tenant/admin visibility stays on current guards and deny-as-not-found semantics.
|
||||
- Run observability: PASS. Existing start contract, terminal notifications, and Monitoring ownership remain unchanged while progress semantics are centralized.
|
||||
- Ops-UX lifecycle: PASS. No change to service-owned status/outcome transitions or `summary_counts` ownership.
|
||||
- Data minimization: PASS. Hosts stay compact and do not surface raw evidence by default.
|
||||
- Test governance: PASS. Proof stays bounded to unit plus feature coverage.
|
||||
- Proportionality / no premature abstraction: PASS. The helper is justified by multiple real consumers and avoids wider rollout or persistence.
|
||||
- Persisted truth / behavioral state: PASS. No new table, cache, or progress-mode persistence.
|
||||
- Shared pattern first / UI semantics / Filament-native UI: PASS. Existing helpers stay central, and the current shell moves closer to the Ops-UX contract.
|
||||
- Provider boundary: PASS. No provider/platform seam changes.
|
||||
- Filament/Laravel panel safety: PASS. Filament v5 stays on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, and no new assets are planned.
|
||||
|
||||
**Gate evaluation**: PASS.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Unit for progress-capability/render-model truth; Feature for current shell adoption and shell-visible progress output
|
||||
- **Affected validation lanes**: fast-feedback, confidence
|
||||
- **Why this lane mix is the narrowest sufficient proof**: one unit suite proves the shared contract itself, while focused shell feature tests prove the visible adopter no longer calculates progress locally. Browser proof remains owned by `specs/268-operationrun-activity-feedback/` because this slice does not change layout or clickability.
|
||||
- **Narrowest proving command(s)**:
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php`
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/OpsUx/SummaryCountsWhitelistTest.php`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: low to moderate; reuse current `OperationRun` factories and tenant helpers instead of introducing new provider-heavy defaults
|
||||
- **Expensive defaults or shared helper growth introduced?**: no
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||
- **Surface-class relief / special coverage rule**: `global-context-shell`
|
||||
- **Closing validation and reviewer handoff**: reviewers should rerun the focused commands above, then confirm the shell uses one shared progress contract, queued runs stay indeterminate, terminal runs stay terminal, and outcome counters never create a percentage
|
||||
- **Budget / baseline / trend follow-up**: none expected beyond a small feature-local increase
|
||||
- **Review-stop questions**: did the helper stay bounded, did the shell lose its inline progress math, did any new `summary_counts` keys or writer semantics appear, and were `269`, `271`, `272`, and `273` kept out of scope?
|
||||
- **Escalation path**: `reject-or-split` for any writer rollout, dashboard redesign, or persisted progress model
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
- **Why no dedicated follow-up spec is needed**: this package is itself the bounded contract layer. Later counted rollout and phase/composite rollout remain explicit follow-up specs rather than hidden growth here.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/270-operationrun-progress-contract/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── tasks.md
|
||||
└── checklists/
|
||||
└── requirements.md
|
||||
```
|
||||
|
||||
This preparation package intentionally stays on the core artifacts plus the readiness checklist. The repo already contains the relevant Ops-UX truth, current shell host, and adjacent tests, so no extra research, data-model, or contract package is required for a bounded implementation handoff.
|
||||
|
||||
### Source Code (expected implementation surfaces)
|
||||
|
||||
```text
|
||||
apps/platform/app/Support/OpsUx/
|
||||
apps/platform/app/Livewire/BulkOperationProgress.php
|
||||
apps/platform/resources/views/livewire/bulk-operation-progress.blade.php
|
||||
apps/platform/app/Services/OperationRunService.php
|
||||
apps/platform/tests/Unit/Support/OpsUx/
|
||||
apps/platform/tests/Feature/OpsUx/
|
||||
docs/ui/tenantpilot-enterprise-ui-standards.md
|
||||
```
|
||||
|
||||
**Structure Decision**: keep the implementation local to the existing Ops-UX support family and the current shell host. Do not introduce a new activity or progress framework outside `App\Support\OpsUx`.
|
||||
|
||||
## Data / Migration Implications
|
||||
|
||||
- No migration or new table is planned.
|
||||
- No new persisted user preference or progress-mode storage is allowed.
|
||||
- No new cache layer, backfill, or asset/deploy step should be required for v1.
|
||||
|
||||
## Rollout Considerations
|
||||
|
||||
- Filament remains v5 on Livewire v4. No panel-provider change is required, and provider registration remains in `apps/platform/bootstrap/providers.php`.
|
||||
- No global search change is required because the slice changes shared progress semantics, not resource discovery.
|
||||
- No destructive action is added. Existing start/retry/detail surfaces remain the only mutation owners.
|
||||
- No new asset registration is expected.
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- Reject any implementation that reopens the shell terminal-outcome slice already owned by `specs/268-operationrun-activity-feedback/` and the deferred `269` candidate.
|
||||
- Reject any implementation that introduces new `summary_counts` keys, a persisted progress mode, or a new `OperationRun` lifecycle.
|
||||
- Reject any implementation that derives percentages from status, duration, stale heuristics, or outcome counters.
|
||||
- Reject any implementation that widens the slice into dashboard-specific redesign, activity tray work, or counted writer rollout.
|
||||
- Reject any implementation that leaves a second progress calculator in Blade, Livewire, or another current host surface.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0 - Confirm Current Progress Truth And Drift Seams
|
||||
|
||||
- Verify the current writer seams (`OperationRunService`, `SummaryCountsNormalizer`, `OperationSummaryKeys`) and the current visible adopter (`BulkOperationProgress`).
|
||||
|
||||
### Phase 1 - Encode The Shared Progress Contract
|
||||
|
||||
- Introduce one shared progress contract/helper that classifies capability and derives render-safe output from existing `OperationRun` truth.
|
||||
|
||||
### Phase 2 - Adopt The Current Shell Host
|
||||
|
||||
- Move shell progress eligibility and percentage math out of `bulk-operation-progress.blade.php` and onto the shared contract.
|
||||
|
||||
### Phase 3 - Record The Guardrail And Future Boundaries
|
||||
|
||||
- Update the UI standards and the focused tests so later specs inherit the same contract instead of re-explaining it locally.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: the repo has truthful counters and lifecycle state, but the current visible host still computes progress ad hoc.
|
||||
- **Existing structure is insufficient because**: sanitization and lifecycle normalization alone do not decide whether determinate progress is allowed.
|
||||
- **Narrowest correct implementation**: one shared progress-semantics helper plus shell adoption and one standards-doc update.
|
||||
- **Ownership cost created**: one helper, focused tests, and one standards update.
|
||||
- **Alternative intentionally rejected**: view-local math or persisted progress modes were rejected because they either preserve drift or add unjustified persistence.
|
||||
- **Release truth**: current-release truth. The repo already renders progress and already stores the counts needed to centralize the semantics now.
|
||||
273
specs/270-operationrun-progress-contract/spec.md
Normal file
273
specs/270-operationrun-progress-contract/spec.md
Normal file
@ -0,0 +1,273 @@
|
||||
# Feature Specification: OperationRun Progress Contract v1
|
||||
|
||||
**Feature Branch**: `270-operationrun-progress-contract`
|
||||
**Created**: 2026-05-04
|
||||
**Status**: Ready for implementation
|
||||
**Input**: Manual promotion from `docs/product/spec-candidates.md` after repo-based duplicate verification excluded candidate `269` because `specs/268-operationrun-activity-feedback/` already owns the terminal-success and terminal-follow-up shell slice.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: `OperationRun` progress rendering is still decided ad hoc in the shell activity view and raw `summary_counts` conventions. The repo sanitizes numeric counts and normalizes lifecycle status, but it does not yet have one shared product contract that answers whether a run should show no progress, indeterminate activity, determinate counted progress, or a future phase/composite fallback.
|
||||
- **Today's failure**: `BulkOperationProgress` currently calculates progress inline from raw `summary_counts`, while other current or future surfaces can only see status, outcome, and flattened counters. That makes it too easy for a surface to invent fake percentages from status, duration, or outcome counters such as `failed`, `succeeded`, or `skipped`, and it leaves future `OperationRun` hosts without one truthful progress language.
|
||||
- **User-visible improvement**: Operators see one consistent, honest progress language across the covered Ops-UX surfaces: queued work and active work without trustworthy counts show activity only, active running work with trustworthy `processed` and `total` counts shows determinate progress, and terminal outcomes never masquerade as progress.
|
||||
- **Smallest enterprise-capable version**: introduce one shared Ops-UX progress contract or presenter over existing `OperationRun` truth, adopt it in the current shell activity feedback surface that already renders progress, and document the contract in the UI standards so later run hosts and later rollout specs extend the same rules instead of improvising them locally.
|
||||
- **Explicit non-goals**: no broad counted-progress rollout across run writers, no new `OperationRun` type or lifecycle, no dashboard redesign, no new queue or notification policy, no new `summary_counts` keys, no persistence for progress modes, no AI summaries, no customer-facing review changes, and no reopening of `specs/268-operationrun-activity-feedback/` or `specs/266-tenant-dashboard-productization-v1/` as the primary scope.
|
||||
- **Permanent complexity imported**: one bounded shared progress-semantics helper in `App\Support\OpsUx`, one derived progress-capability vocabulary (`none`, `activity`, `counted`, `phased`, `composite`) kept in code and docs only, focused unit plus feature coverage, and one UI standards update.
|
||||
- **Why now**: `specs/268-operationrun-activity-feedback/` already captures shell terminal outcome semantics, which makes the remaining abstraction gap visible: progress meaning still lives inline in a Blade view. The candidate backlog explicitly sequences `270` ahead of `271` and `272`, and the repo already contains the concrete seams needed to prepare this contract without inventing a new foundation.
|
||||
- **Why not local**: a local shell-only math cleanup would still leave future `OperationRun` surfaces, future counted rollouts, and future phase/composite work free to invent their own progress semantics from raw counters.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: new derived capability vocabulary, one shared Ops-UX presenter/contract seam, and one cross-surface truth contract. Defense: the vocabulary stays derived only, the contract is justified by multiple real consumers, and no new persistence or framework layer is introduced.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: tenant + canonical-view
|
||||
- **Primary Routes**:
|
||||
- `/admin/t/{tenant}/...` tenant-scoped start surfaces that receive the shared shell activity hint
|
||||
- `/admin/operations` and `/admin/operations/{run}` remain the canonical operations collection/detail routes whose summaries must stay compatible with the shared progress contract even when this slice does not broaden their visible UI
|
||||
- `/admin/t/{tenant}` remains contextual-only for any existing recent-operation summary that already consumes the same Ops-UX family; no new dashboard card is introduced in this slice
|
||||
- **Data Ownership**: existing `OperationRun.status`, `OperationRun.outcome`, `OperationRun.summary_counts`, and `OperationRun.context` remain the only persisted truth. The progress contract is derived and must not introduce a new table, cache, mirror entity, or persisted progress mode.
|
||||
- **RBAC**: existing `OperationRun` policies remain authoritative. Out-of-scope tenants stay deny-as-not-found (`404` semantics through the current tenant/admin boundaries), and in-scope actors only see progress states for runs they can already view.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes
|
||||
- **Interaction class(es)**: status messaging, activity feedback, progress disclosure, canonical run links, and execution-truth summaries
|
||||
- **Systems touched**: `BulkOperationProgress`, `ActiveRuns`, `OperationRunService`, `SummaryCountsNormalizer`, `OperationSummaryKeys`, `OperationStatusNormalizer`, `OperationUxPresenter`, current Ops-UX tests, and `docs/ui/tenantpilot-enterprise-ui-standards.md`
|
||||
- **Existing pattern(s) to extend**: current numeric-only `summary_counts` normalization, current lifecycle normalization, current active shell feedback surface, and the existing Ops-UX 3-surface contract
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: `App\Support\OpsUx\SummaryCountsNormalizer`, `App\Support\OpsUx\OperationSummaryKeys`, `App\Support\OpsUx\ActiveRuns`, `App\Support\OpsUx\OperationStatusNormalizer`, `App\Support\OpsUx\OperationUxPresenter`, `App\Services\OperationRunService`, and `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php`
|
||||
- **Why the existing shared path is sufficient or insufficient**: the repo already sanitizes allowed counters and normalizes lifecycle state, but none of those helpers currently decides whether a surface should show progress, what kind of progress is allowed, or when progress must collapse back to terminal outcome or simple activity.
|
||||
- **Allowed deviation and why**: none planned. The feature must remove shell-local progress inference instead of creating a second widget-local contract.
|
||||
- **Consistency impact**: `Queued`, `In progress`, `Completed successfully`, indeterminate activity labels, determinate progress labels, and the rule that outcome counters never substitute for `processed` must keep one meaning across covered Ops-UX surfaces and docs.
|
||||
- **Review focus**: reviewers must block any implementation that derives percentage from status, duration, stale-state heuristics, or outcome counters, or that leaves a second local progress calculator in Blade or Livewire code.
|
||||
|
||||
## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes
|
||||
- **Shared OperationRun UX contract/layer reused**: the existing Ops-UX start contract through `OperationRunLinks`, `OperationRunUrl`, `ActiveRuns`, `OperationStatusNormalizer`, and `OperationUxPresenter`, extended with one shared progress-semantics helper
|
||||
- **Delegated start/completion UX behaviors**: queued toasts, canonical `View operation` link generation, tenant-safe URL resolution, current `run-enqueued` browser events, and existing terminal database-notification behavior remain delegated to the current shared path and are unchanged in this slice
|
||||
- **Local surface-owned behavior that remains**: shell layout, copy density, and bounded host placement stay local to the existing shell surface; progress semantics no longer remain view-local
|
||||
- **Queued DB-notification policy**: `N/A` - unchanged
|
||||
- **Terminal notification path**: unchanged central lifecycle mechanism
|
||||
- **Exception required?**: none
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no
|
||||
- **Boundary classification**: `N/A`
|
||||
- **Seams affected**: `N/A`
|
||||
- **Neutral platform terms preserved or introduced**: `Operation`, `activity`, `progress`, `counted progress`, `terminal outcome`
|
||||
- **Provider-specific semantics retained and why**: none
|
||||
- **Why this does not deepen provider coupling accidentally**: the feature only formalizes progress semantics over existing platform-owned `OperationRun` truth
|
||||
- **Follow-up path**: none
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory)*
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Tenant shell activity hint progress treatment | yes | Native Filament + existing Livewire/Blade surface | Ops UX activity feedback, execution-truth summaries | shell, page | no | The shell remains the visible v1 adopter; this slice moves progress semantics out of inline Blade math and into one shared contract |
|
||||
|
||||
## Decision-First Surface Role *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Tenant shell activity hint | Primary Decision Surface | Decide whether current work simply needs time, needs inspection now, or is already terminal | operation label, lifecycle state, one truthful progress mode, and the canonical `View operation` action | full run detail, logs, evidence, and diagnostics stay in Operations detail | Primary because it is the current visible progress host and must not invent execution truth | Follows the existing start-surface workflow | Replaces ad hoc progress math with one trustworthy meaning |
|
||||
|
||||
## Audience-Aware Disclosure *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Tenant shell activity hint | operator-MSP | operation label, lifecycle state, either activity-only or counted-progress treatment, and canonical open link | one concise guidance line only when it changes the next decision | raw payloads, failure summaries, logs, and debug context | `View operation` | raw/support detail stays in Operations detail | progress mode is derived once from the shared contract instead of each surface inventing its own explanation |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Tenant shell activity hint | Monitoring hint | Activity shell hint | Open the most relevant operation if follow-up is needed | explicit `View operation` link | forbidden | overflow navigation only | none | `/admin/operations?tenant_id={currentTenant}` | `/admin/operations/{run}` | current tenant context from the shell | Operation | lifecycle state plus one truthful progress mode | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Tenant shell activity hint | Tenant operator | Decide whether active work is merely progressing, still waiting, or already terminal | Start-surface hint | What operation state do I need to react to right now? | operation label, lifecycle state, activity-only or counted-progress treatment, canonical open link | detailed run diagnostics and evidence on Operations pages | lifecycle, progress capability | none | `View operation`, `Show all operations` | none |
|
||||
|
||||
**UI Action Matrix**: `N/A - no Filament Resource, RelationManager, or Page action surface is being introduced or reclassified. The changed surface remains a shell/widget hint and shared Ops-UX semantics layer only.`
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no new persisted source of truth; one shared derived progress contract over existing `OperationRun` truth only
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: yes - one bounded shared progress contract/helper inside `App\Support\OpsUx`
|
||||
- **New enum/state/reason family?**: yes - one derived progress-capability vocabulary (`none`, `activity`, `counted`, `phased`, `composite`) used in code and docs only, not persisted
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: operators currently rely on progress semantics that are computed locally inside a shell view rather than from one central rule set
|
||||
- **Existing structure is insufficient because**: numeric-only `summary_counts` sanitization and lifecycle normalization do not answer whether a surface is allowed to show determinate progress or how terminal, queued, phased, or composite work must degrade safely
|
||||
- **Narrowest correct implementation**: one shared progress-semantics helper plus current shell adoption and one standards-doc update, with no count rollout and no new persistence
|
||||
- **Ownership cost**: one helper, focused unit plus feature tests, and one standards-doc update
|
||||
- **Alternative intentionally rejected**: leaving the progress math in `bulk-operation-progress.blade.php` or persisting an explicit progress mode on `operation_runs` were both rejected as either too local or too heavy
|
||||
- **Release truth**: current-release truth. The repo already has multiple real surfaces and writers using `summary_counts`; this slice keeps them from drifting into conflicting progress claims
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment.
|
||||
|
||||
Backward compatibility, legacy aliases, migration shims, and compatibility-specific tests are out of scope unless a later implementation slice proves they are required.
|
||||
|
||||
Canonical replacement of ad hoc progress inference is preferred over preserving duplicate logic.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory)*
|
||||
|
||||
- **Test purpose / classification**: Unit, Feature
|
||||
- **Validation lane(s)**: fast-feedback, confidence
|
||||
- **Why this classification and these lanes are sufficient**: one unit suite can prove the progress-capability mapping and render-model rules cheaply, while focused feature coverage can prove the current shell host consumes the shared contract and does not regress into fake percentage output. Browser proof is not required for this slice because layout and clickability are already owned by `specs/268-operationrun-activity-feedback/`.
|
||||
- **New or expanded test families**: one focused `tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php` family plus focused extensions to the current `tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php`, `tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, and `tests/Feature/OpsUx/SummaryCountsWhitelistTest.php`
|
||||
- **Fixture / helper cost impact**: low to moderate. Reuse current `OperationRun` factories, tenant context helpers, and current Ops-UX feature fixtures; do not add provider-heavy setup or a new browser family.
|
||||
- **Heavy-family visibility / justification**: none
|
||||
- **Special surface test profile**: global-context-shell
|
||||
- **Standard-native relief or required special coverage**: standard unit plus feature coverage is sufficient; this spec deliberately does not create new browser obligations
|
||||
- **Reviewer handoff**: reviewers must confirm that the shell consumes a single shared progress contract, queued runs never show determinate progress, terminal runs never keep progress UI, outcome counters do not masquerade as progress, and the feature does not introduce new `summary_counts` keys or a second local helper
|
||||
- **Budget / baseline / trend impact**: small feature-local increase only
|
||||
- **Escalation needed**: `reject-or-split` if implementation widens into run-writer rollout, dashboard redesign, or a second persisted progress model
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
- **Planned validation commands**:
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php`
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/OpsUx/SummaryCountsWhitelistTest.php`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Derive truthful progress modes from existing run truth (Priority: P1)
|
||||
|
||||
As an operator, I need one shared progress contract to decide whether a run shows no progress, activity-only feedback, or determinate counted progress, so the product does not invent conflicting execution claims across Ops-UX surfaces.
|
||||
|
||||
**Why this priority**: this is the core truth gap. Without a shared contract, later shell, dashboard, and rollout work will continue to duplicate or drift progress semantics.
|
||||
|
||||
**Independent Test**: create queued, running, completed-success, completed-failed, and follow-up runs with different `summary_counts`, then assert that the shared contract returns the correct capability and render model without depending on a specific UI surface.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a running run with numeric `summary_counts.total` and `summary_counts.processed`, **When** the shared contract evaluates that run, **Then** it returns counted progress and clamps the rendered values safely.
|
||||
2. **Given** a queued run or a running run without trustworthy numeric `processed` and `total` counts, **When** the shared contract evaluates it, **Then** it returns activity-only progress and does not fabricate a percentage.
|
||||
3. **Given** a terminal run, **When** the shared contract evaluates it, **Then** it returns no progress line or bar even if outcome counters are present.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Keep the current shell host on the shared contract (Priority: P1)
|
||||
|
||||
As a tenant operator, I need the current shell activity hint to consume the shared progress contract instead of local view math, so active work stays truthful and terminal work never keeps stale progress UI.
|
||||
|
||||
**Why this priority**: the current shell view is the real, repo-visible seam where ad hoc progress logic already exists.
|
||||
|
||||
**Independent Test**: render the current shell activity surface with queued, running, and terminal runs, then verify that it uses counted progress only when the shared contract allows it and otherwise falls back to activity-only or terminal semantics.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a running run with valid `processed` and `total` counts, **When** the shell hint renders it, **Then** the shell shows determinate counted progress derived through the shared contract rather than view-local math.
|
||||
2. **Given** a queued run with a planned total, **When** the shell hint renders it, **Then** the shell shows waiting or activity state only and does not promote it to determinate progress.
|
||||
3. **Given** a completed run with `succeeded`, `failed`, or `skipped` counters but no trustworthy active progress truth, **When** the shell hint renders it, **Then** the shell shows terminal outcome semantics and not a percentage.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Document future-safe progress boundaries (Priority: P2)
|
||||
|
||||
As a maintainer, I need the UI standards and developer guidance to define what counted, activity-only, phased, and composite progress mean, so later rollout specs extend one contract instead of inventing new progress languages.
|
||||
|
||||
**Why this priority**: the contract only stays useful if later surfaces and writer rollouts know exactly what they may and may not claim.
|
||||
|
||||
**Independent Test**: review the standards guidance and the focused validation suite together, then confirm the package explicitly defers counted rollout and future phase/composite implementation to named follow-up specs.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the UI standards are updated for this package, **When** a maintainer reads the progress rules, **Then** they can tell that `processed` and `total` are the only v1 determinate source and that outcome counters remain outcome-only.
|
||||
2. **Given** future phased or composite work is still unspecced, **When** a maintainer reads the package, **Then** the package clearly states that those categories must not masquerade as counted percentages until trustworthy persisted truth exists.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Queued runs may already know `total`, but they still render as activity-only rather than counted progress.
|
||||
- Running runs with `processed > total`, negative counters, or non-numeric counter values must clamp or degrade safely rather than showing impossible percentages.
|
||||
- Terminal runs that retain `processed` and `total` counts for summary truth must still drop progress UI and show terminal outcome only.
|
||||
- Runs that only have `succeeded`, `failed`, or `skipped` counts must not silently use those outcome counters as progress substitutes.
|
||||
- Phase/composite categories may be declared in the shared contract for future use, but until trustworthy phase or child-run truth exists they must degrade to non-counted display and not invent percentages.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment summary**: This feature adds no new Graph calls, no new write path, no new `OperationRun` lifecycle, no new `summary_counts` key, and no new persistence. It reuses the current Ops-UX 3-surface contract, the current `summary_counts` whitelist, and current shell activity host while centralizing progress semantics into one shared rule set.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The implementation MUST provide one shared Ops-UX progress contract or presenter that derives progress capability and render model from existing `OperationRun` truth instead of Blade- or widget-local progress math.
|
||||
- **FR-002**: The shared contract MUST define one derived vocabulary for `none`, `activity`, `counted`, `phased`, and `composite` progress capability, and that vocabulary MUST live in code and docs only rather than as persisted `OperationRun` state.
|
||||
- **FR-003**: Determinate counted progress MUST be allowed only when the run is actively running and `summary_counts.total` plus `summary_counts.processed` are trustworthy numeric values with `total > 0`.
|
||||
- **FR-004**: Queued runs and active runs without trustworthy `processed` and `total` counts MUST render as activity-only or indeterminate and MUST NOT fabricate a percentage from status, duration, stale-state heuristics, or outcome counters.
|
||||
- **FR-005**: Terminal runs MUST render no progress bar, no percentage, and no `processed / total` progress line, even when retained counters are present for audit or summary truth.
|
||||
- **FR-006**: `summary_counts.succeeded`, `summary_counts.failed`, and `summary_counts.skipped` remain outcome counters. They MUST NOT silently replace `summary_counts.processed` as the determinate progress source.
|
||||
- **FR-007**: When counted progress is allowed, the shared contract MUST sanitize or clamp `processed`, `total`, and percentage output so rendered values stay within a truthful `0-100` percent range and `processed` never exceeds `total` visibly.
|
||||
- **FR-008**: The shared contract MUST declare safe future `phased` and `composite` capabilities, and those categories MUST NOT masquerade as counted percentages until trustworthy persisted phase or child-progress truth exists.
|
||||
- **FR-009**: The current shell activity feedback surface MUST consume the shared progress contract and MUST NOT keep inline percentage calculation or progress-mode inference in `bulk-operation-progress.blade.php`.
|
||||
- **FR-010**: The feature MUST update `docs/ui/tenantpilot-enterprise-ui-standards.md` and the relevant Spec Kit artifacts so the progress contract, anti-patterns, and follow-up ownership are documented once.
|
||||
- **FR-011**: This slice MUST NOT add new `OperationRun` status values, outcome values, notification surfaces, `summary_counts` keys, or persisted progress-mode flags.
|
||||
|
||||
### Authorization and Safety Requirements
|
||||
|
||||
- **AR-001**: Tenant/admin-plane authorization semantics remain unchanged: out-of-scope access remains deny-as-not-found (`404` semantics through current tenant/admin boundaries), while in-scope visibility continues to reuse server-side `OperationRun` policies.
|
||||
- **AR-002**: No covered surface may reveal progress state or progress-derived copy for a run the current actor cannot already view.
|
||||
- **AR-003**: No destructive or mutating action is introduced. Existing run start, retry, and detail actions remain responsible for their current confirmation and authorization rules.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: The slice MUST stay Filament-native and Livewire v4-compatible. No panel-provider registration change is allowed; `apps/platform/bootstrap/providers.php` remains authoritative.
|
||||
- **NFR-002**: No new panel, no new globally searchable resource, and no new asset registration strategy are allowed.
|
||||
- **NFR-003**: Polling remains intentional and bounded. The feature may reuse existing poller families, but it MUST NOT introduce new parallel polling loops.
|
||||
- **NFR-004**: `OperationSummaryKeys` and `SummaryCountsNormalizer` remain the only allowed summary-count whitelist and sanitization owners. The progress contract must build on them rather than bypass them.
|
||||
|
||||
## Deferred Follow-Ups / Explicit Non-Goals
|
||||
|
||||
- `269 — OperationRun Terminal Outcome Feedback`, because the repo-based duplicate check shows that `specs/268-operationrun-activity-feedback/` already owns that shell slice
|
||||
- `271 — Counted Progress Rollout v1`
|
||||
- `272 — OperationRun Phase & Composite Progress v1`
|
||||
- `273 — Tenant Dashboard Active Operations Summary Card`
|
||||
- any run-writer rollout that adds or changes `summary_counts.total` / `summary_counts.processed`
|
||||
- any dashboard, tray, or customer-facing redesign beyond consuming the shared contract later
|
||||
- any new persisted progress model or telemetry system
|
||||
|
||||
## Key Entities
|
||||
|
||||
- **OperationRun Progress Capability**: the derived classification that answers whether a run currently supports no progress, activity-only feedback, determinate counted progress, or a future phased/composite mode.
|
||||
- **OperationRun Progress Render Model**: the derived payload that a host surface consumes to show activity-only copy, counted progress values, or terminal no-progress behavior without doing its own math.
|
||||
- **Counted Progress Truth**: the specific case where active running work has trustworthy numeric `processed` and `total` counters suitable for determinate rendering.
|
||||
- **Outcome Counters**: retained numeric counts such as `succeeded`, `failed`, and `skipped` that remain outcome summary truth and do not become determinate progress implicitly.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: In focused unit coverage, the shared progress contract classifies the covered fixtures into `none`, `activity`, `counted`, and safe future `phased/composite` fallbacks without relying on a specific view.
|
||||
- **SC-002**: In focused shell feature coverage, queued and terminal runs show no determinate percentage in 100% of covered scenarios.
|
||||
- **SC-003**: In focused shell feature coverage, determinate progress appears only for active running runs with trustworthy numeric `processed` and `total` values.
|
||||
- **SC-004**: In regression coverage, outcome counters such as `succeeded`, `failed`, and `skipped` never produce counted progress unless `processed` and `total` also satisfy the shared contract.
|
||||
- **SC-005**: The UI standards and Spec Kit artifacts document one canonical progress contract and explicitly assign counted rollout and phase/composite rollout to later follow-up specs.
|
||||
|
||||
## Candidate Selection Rationale
|
||||
|
||||
- **Selected candidate**: OperationRun Progress Contract v1
|
||||
- **Source locations**:
|
||||
- `docs/product/spec-candidates.md`
|
||||
- `docs/product/roadmap.md`
|
||||
- `specs/268-operationrun-activity-feedback/spec.md`
|
||||
- `apps/platform/app/Support/OpsUx/ActiveRuns.php`
|
||||
- `apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php`
|
||||
- `apps/platform/app/Support/OpsUx/OperationSummaryKeys.php`
|
||||
- `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php`
|
||||
- **Why selected**: the active auto-prep queue is intentionally empty, candidate `269` fails the duplicate check because `specs/268-operationrun-activity-feedback/` already covers shell terminal-success and terminal-follow-up semantics, and the current repo still shows inline progress logic inside the shell view with no shared contract. Candidate `270` is the next bounded backlog item that closes a real truth gap without widening into dashboard work or run-writer rollout.
|
||||
- **Why this is the smallest viable implementation slice**: v1 stays on one shared progress contract plus current shell adoption and one standards update. It explicitly excludes counted writer rollout, dashboard productization, and future phase/composite execution truth.
|
||||
- **Why close alternatives were deferred**:
|
||||
- `269 — OperationRun Terminal Outcome Feedback` is already covered by `specs/268-operationrun-activity-feedback/` and is therefore not a safe prep target.
|
||||
- `273 — Tenant Dashboard Active Operations Summary Card` remains conditional on post-`268` dashboard drift and should not be promoted before that drift is visible.
|
||||
- `271 — Counted Progress Rollout v1` depends on the shared contract from this package.
|
||||
- `272 — OperationRun Phase & Composite Progress v1` depends on the shared contract from this package and likely on later trustworthy phase/composite truth.
|
||||
|
||||
## Related-Spec Guardrail Check
|
||||
|
||||
- `specs/268-operationrun-activity-feedback/`: prep package, not completed, and not safe to refresh through this skill because it already owns the shell terminal outcome slice.
|
||||
- `specs/266-tenant-dashboard-productization-v1/`: different dashboard surface with its own implementation history; used only as context for why `273` remains conditional.
|
||||
- `specs/055-ops-ux-rollout/`, `specs/160-operation-lifecycle-guarantees/`, and `specs/134-audit-log-foundation/`: implemented foundation specs that already define the numeric-only `summary_counts` and three-surface lifecycle rules; used as inherited truth only.
|
||||
177
specs/270-operationrun-progress-contract/tasks.md
Normal file
177
specs/270-operationrun-progress-contract/tasks.md
Normal file
@ -0,0 +1,177 @@
|
||||
---
|
||||
description: "Task list for OperationRun Progress Contract v1"
|
||||
---
|
||||
|
||||
# Tasks: OperationRun Progress Contract v1
|
||||
|
||||
**Input**: Design documents from `specs/270-operationrun-progress-contract/`
|
||||
**Prerequisites**: `specs/270-operationrun-progress-contract/spec.md`, `specs/270-operationrun-progress-contract/plan.md`, `specs/270-operationrun-progress-contract/checklists/requirements.md`
|
||||
|
||||
**Review Artifact**: `specs/270-operationrun-progress-contract/checklists/requirements.md` is the outcome-of-record for the review outcome class, workflow outcome, and test-governance outcome. If implementation widens into counted-writer rollout, dashboard drift work, or a new persisted progress model, update that artifact before continuing.
|
||||
|
||||
**Tests**: REQUIRED (Pest). Keep proof bounded to one new unit suite plus focused Ops-UX feature coverage. Browser coverage remains owned by `specs/268-operationrun-activity-feedback/` and must not be pulled into this slice implicitly.
|
||||
**Operations**: No new `OperationRun` type, no queue-family changes, no notification-policy changes, no new `summary_counts` keys, and no new lifecycle ownership. Existing queued toasts, terminal notifications, `run-enqueued` browser events, and canonical `OperationRun` links remain authoritative.
|
||||
**RBAC**: Reuse current `OperationRun` policies and tenant context guards. No tenantless leakage from tenant surfaces; covered surfaces stay inert when no selected tenant or `viewAny` capability exists.
|
||||
**Shared Pattern Reuse**: Reuse `SummaryCountsNormalizer`, `OperationSummaryKeys`, `ActiveRuns`, `OperationStatusNormalizer`, `OperationUxPresenter`, `OperationRunService`, `BulkOperationProgress`, and `docs/ui/tenantpilot-enterprise-ui-standards.md`. Do not create a second local progress helper in Blade or Livewire.
|
||||
**Filament / Panel Guardrails**: Filament remains v5 on Livewire v4. Provider registration remains unchanged in `apps/platform/bootstrap/providers.php`. No new panel, resource, or asset strategy is allowed. This slice changes shared progress semantics only.
|
||||
**Organization**: Tasks are grouped by user story so the shared contract, the shell adoption, and the future-boundary documentation remain independently reviewable.
|
||||
|
||||
## Test Governance Notes
|
||||
|
||||
- Lane mix stays Unit plus Feature.
|
||||
- Prefer extending `ActivityFeedbackSurfaceTest`, `BulkOperationProgressDbOnlyTest`, and `SummaryCountsWhitelistTest` before creating broader families.
|
||||
- Browser proof stays with Spec 268 and must not become a hidden requirement here.
|
||||
- Validation commands must stay file-scoped and run through Sail.
|
||||
|
||||
## Phase 1: Setup (Shared Context)
|
||||
|
||||
**Purpose**: confirm the bounded slice, the inherited Ops-UX rules, and the specific ad hoc progress seam before runtime edits begin.
|
||||
|
||||
- [x] T001 Review `specs/270-operationrun-progress-contract/spec.md`, `specs/270-operationrun-progress-contract/plan.md`, `specs/270-operationrun-progress-contract/checklists/requirements.md`, `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, `specs/268-operationrun-activity-feedback/spec.md`, `specs/055-ops-ux-rollout/spec.md`, `docs/ui/tenantpilot-enterprise-ui-standards.md`, and `.specify/memory/constitution.md` together so the slice stays on repo-real progress truth and keeps `269`, `271`, `272`, and `273` explicitly out of scope.
|
||||
- [x] T002 [P] Confirm the current writer and sanitizer seams in `apps/platform/app/Services/OperationRunService.php`, `apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php`, and `apps/platform/app/Support/OpsUx/OperationSummaryKeys.php`.
|
||||
- [x] T003 [P] Confirm the current visible progress host and reader seams in `apps/platform/app/Support/OpsUx/ActiveRuns.php`, `apps/platform/app/Livewire/BulkOperationProgress.php`, and `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php`.
|
||||
- [x] T004 [P] Confirm current proof owners in `apps/platform/tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php`, `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, and `apps/platform/tests/Feature/OpsUx/SummaryCountsWhitelistTest.php`, then record the exact shell-progress drift seam that the new unit suite must own.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: settle the progress contract and proof owners before the visible shell host is refactored.
|
||||
|
||||
**Critical**: no user-story runtime work should begin until this phase is complete.
|
||||
|
||||
- [x] T005 [P] Create failing unit coverage in `apps/platform/tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php` for `none`, `activity`, `counted`, safe `phased`, and safe `composite` capability mapping, queued indeterminate handling, terminal no-progress handling, and outcome-counter rejection.
|
||||
- [x] T006 [P] Extend `apps/platform/tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php` for shell adoption of the shared contract: determinate progress only from running plus trustworthy `processed`/`total`, no counted progress for queued runs, and no progress line or percentage for terminal runs.
|
||||
- [x] T007 [P] Extend `apps/platform/tests/Feature/OpsUx/SummaryCountsWhitelistTest.php` only as needed so the new contract still reuses numeric-only whitelist semantics and does not create hidden progress inputs from non-whitelisted keys.
|
||||
- Note: no file edit was required because the existing whitelist suite already proved the contract continued to consume the canonical numeric-only keys without introducing new progress inputs.
|
||||
|
||||
**Checkpoint**: the shared contract and focused proof owners are settled before implementation begins.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Derive truthful progress modes from existing run truth (Priority: P1)
|
||||
|
||||
**Goal**: one shared Ops-UX helper classifies progress capability and render model from current `OperationRun` truth.
|
||||
|
||||
**Independent Test**: create queued, running, and terminal runs with varying `summary_counts`, then verify the new unit suite classifies them correctly without involving a UI host.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [x] T008 [P] [US1] Add any additional dataset or fixture coverage needed in `apps/platform/tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php` for current real run shapes such as inventory sync, review-pack generation, and bulk jobs that already emit `processed` and `total`.
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [x] T009 [US1] Introduce one shared progress contract/helper under `apps/platform/app/Support/OpsUx/` that derives progress capability and render-safe output from `OperationRun` status, outcome, sanitized `summary_counts`, and current trusted context only.
|
||||
- [x] T010 [US1] Reuse or extend `apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php`, `apps/platform/app/Support/OpsUx/OperationSummaryKeys.php`, and `apps/platform/app/Support/OpsUx/OperationStatusNormalizer.php` only as needed so lifecycle semantics and progress semantics stay separate but consistent.
|
||||
- Note: the implementation reused the existing normalizer and status helper without modifying their source, which kept progress semantics bounded to the new contract helper.
|
||||
|
||||
**Checkpoint**: User Story 1 is independently functional when one shared helper owns the progress semantics.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Keep the current shell host on the shared contract (Priority: P1)
|
||||
|
||||
**Goal**: the current shell activity hint consumes the shared contract instead of local progress math.
|
||||
|
||||
**Independent Test**: render the shell surface with queued, running, and terminal runs, then verify the shell shows counted progress only when the shared contract allows it and otherwise falls back to activity-only or terminal semantics.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [x] T010A [P] [US2] Extend `apps/platform/tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php` so the shell stays inert for missing-tenant or unauthorized actors and does not expose progress-derived copy for runs the actor cannot view.
|
||||
- Note: repo truth kept the missing-tenant inert-state proof in `BulkOperationProgressDbOnlyTest.php`, while tenant-scoped visibility remained covered by the existing DB-only tenant-scoping assertion.
|
||||
- [x] T011 [P] [US2] Extend `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php` for queued indeterminate state, clamped counted progress, and terminal no-progress behavior at the shell host hydration boundary.
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [x] T012 [US2] Refactor `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php` and `apps/platform/app/Livewire/BulkOperationProgress.php` so the shell host consumes the shared progress contract and no longer computes percentages or progress eligibility inline.
|
||||
- Note: repository truth only required the Blade host seam to change; `BulkOperationProgress.php` remained unchanged because it already handed the correct run collection to the view.
|
||||
- [x] T013 [US2] Update `apps/platform/app/Support/OpsUx/ActiveRuns.php` only if needed so shell-visible progress availability stays aligned with the shared contract; if implementation reveals another consumer with local progress math, stop and update the spec, plan, and checklist before touching it.
|
||||
- Note: no `ActiveRuns.php` change was required; no second shell-visible local progress calculator was found.
|
||||
|
||||
**Checkpoint**: User Story 2 is independently functional when the shell surface no longer owns progress semantics ad hoc.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Document future-safe progress boundaries (Priority: P2)
|
||||
|
||||
**Goal**: the UI standards and feature package define how future counted, phased, and composite work must extend the contract.
|
||||
|
||||
**Independent Test**: review the standards update and the focused proving suite together, then confirm that counted rollout, phase/composite rollout, and dashboard follow-up remain named follow-up specs rather than hidden scope here.
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [x] T014 [US3] Update `docs/ui/tenantpilot-enterprise-ui-standards.md` with canonical progress modes, the rule that `processed` and `total` are the only v1 determinate source, the rule that outcome counters remain outcome-only, the queued indeterminate rule, the terminal no-progress rule, and the safe future boundaries for `phased` and `composite` categories.
|
||||
- [x] T015 [US3] Review the resulting implementation to confirm it introduces no new `summary_counts` keys, no writer rollout, no dashboard-specific progress surface, and no persisted progress mode.
|
||||
|
||||
**Checkpoint**: User Story 3 is independently functional when the standards doc and feature package leave clear extension rules for later specs.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Validation
|
||||
|
||||
**Purpose**: validate the bounded slice, stop drift, and hand off a clean implementation path.
|
||||
|
||||
- [x] T016 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php`.
|
||||
- [x] T017 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/OpsUx/SummaryCountsWhitelistTest.php`.
|
||||
- [x] T018 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` for touched platform files.
|
||||
- [x] T019 [P] Review touched code against `docs/ui/tenantpilot-enterprise-ui-standards.md` and confirm the shell remains decision-first, diagnostics-light, Filament-native, and backed by one shared progress contract.
|
||||
- [x] T020 [P] Review touched code to confirm Filament stays on Livewire v4, provider registration remains unchanged in `apps/platform/bootstrap/providers.php`, no new assets were registered, and no new `OperationRun` lifecycle or notification path was introduced.
|
||||
- [x] T021 [P] Review touched code to confirm the implementation reuses existing poller families and introduces no new parallel polling loops while adopting the shared progress contract.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Phase 1 (Setup)**: no dependencies; start immediately.
|
||||
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks user-story work.
|
||||
- **Phase 3 (US1)**: depends on Phase 2 and establishes the shared progress contract.
|
||||
- **Phase 4 (US2)**: depends on Phase 3 because the shell must consume the shared contract rather than invent its own logic.
|
||||
- **Phase 5 (US3)**: depends on Phase 3 and should land with US2 so the contract and extension boundaries stay aligned.
|
||||
- **Phase 6 (Polish)**: depends on all desired user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1 (P1)**: independently testable after Phase 2 and delivers the core enterprise-truth contract.
|
||||
- **US2 (P1)**: independently testable after Phase 3 and delivers the real visible shell adoption.
|
||||
- **US3 (P2)**: independently testable after Phase 3 and is still required for package completion because future rollout ownership is part of the approved scope.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Write the listed Pest coverage first and make it fail for the intended gap.
|
||||
- Land the shared progress helper before removing local shell math.
|
||||
- Re-run the narrowest affected validation command after each story checkpoint before moving on.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Suggested MVP Scope
|
||||
|
||||
- MVP = **US1 + US2**, because the package only delivers value once the shared contract exists and the current visible shell host consumes it.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Complete Phase 1 and Phase 2.
|
||||
2. Deliver US1.
|
||||
3. Deliver US2 on top of the shared helper.
|
||||
4. Add US3 documentation and boundary hardening.
|
||||
5. Finish with focused validation and formatting.
|
||||
|
||||
### Team Strategy
|
||||
|
||||
1. Settle the shared contract and unit proof owner first.
|
||||
2. Keep shell-adoption edits serialized around `BulkOperationProgress` and its Blade view.
|
||||
3. Do not widen into writer rollout or dashboard follow-up while implementing this package.
|
||||
|
||||
---
|
||||
|
||||
## Deferred Follow-Ups / Non-Goals
|
||||
|
||||
- `271 — Counted Progress Rollout v1`
|
||||
- `272 — OperationRun Phase & Composite Progress v1`
|
||||
- `273 — Tenant Dashboard Active Operations Summary Card`
|
||||
- any browser-smoke expansion beyond the currently-owned Spec 268 overlap proof
|
||||
- any new writer-side rollout that adds or changes `summary_counts.total` or `summary_counts.processed`
|
||||
- any persisted progress mode, registry, or dashboard redesign
|
||||
Loading…
Reference in New Issue
Block a user