# Phase 0 Research: Ops-UX Constitution Rollout (v1.3.0 Alignment) (055)

**Date**: 2026-01-18

## Findings

### Current operation-run storage and UX primitives

- The repo has a tenant-scoped `operation_runs` table and an `OperationRun` model.
- Structured “metrics” for this rollout are stored as `operation_runs.summary_counts` (JSONB).
- Canonical Monitoring entrypoint exists via the Filament resource `OperationRunResource`.
- Canonical route building already exists in `App\Support\OperationRunLinks`.

### Existing progress widget patterns

- A bottom-right Livewire widget exists today for bulk operations: `App\Livewire\BulkOperationProgress`.
- It is injected globally via a Filament render hook in `App\Providers\Filament\AdminPanelProvider`.
- Current behavior is not constitution-compliant:
  - It shows terminal states and renders terminal wording.
  - It renders percentages and per-run counts unconditionally.
  - It is user-scoped today (filters by `user_id = auth()->id()`).

### Notifications

- Filament database notifications are already used and are persistent by default.
- Current code has OperationRun DB notifications (queued + completed), but this rollout requires:
  - no queued DB notifications
  - exactly one terminal notification per run
  - initiator-only audience

## Decisions

### Decision 1: Tenant-wide progress widget scope
- **Chosen**: The progress widget shows all active runs for the current tenant (not only runs started by the current user), for users with access to Monitoring → Operations.
- **Rationale**: Aligns with the constitution’s tenant-scoped operations model and avoids “why don’t I see what’s running?” support loops.
- **Alternatives considered**:
  - User-only widget: rejected (hides tenant work and defeats the monitoring intent).

### Decision 2: Canonical metrics source
- **Chosen**: Treat `operation_runs.summary_counts` as the canonical “metrics” field for this rollout.
- **Rationale**: Matches existing schema; avoids adding a new `metrics` column during a UX migration.
- **Alternatives considered**:
  - New `operation_runs.metrics` column: rejected (scope increase + migration risk).

### Decision 3: Unknown operation types in UI
- **Chosen**: Soft-fail at runtime with label `Unknown operation`, and fail-fast in CI for code-produced operation types.
- **Rationale**: Keeps Monitoring usable for legacy/dirty data while enforcing discipline for new work.
- **Alternatives considered**:
  - Throw exception at runtime: rejected (breaks Monitoring for historical data).
  - Render raw type string: rejected (leaks internal naming and encourages drift).

### Decision 4: DB notification audience
- **Chosen**: Initiator-only for terminal DB notifications.
- **Rationale**: Prevents tenant-wide notification spam; Monitoring remains the tenant-wide audit surface.
- **Alternatives considered**:
  - Tenant-wide fan-out: rejected (noisy; not necessary for monitoring).

### Decision 5: No queued DB notifications
- **Chosen**: Ban queued DB notifications repo-wide for OperationRuns.
- **Rationale**: Simplifies dedupe; queued intent belongs to the toast surface.
- **Alternatives considered**:
  - Allow queued DB notifications with dedupe: rejected (still noisy; adds edge cases).

### Decision 6: Migration approach for progress widget
- **Chosen**: Reuse the existing “global Livewire progress widget via render hook” pattern, but migrate it to query `OperationRun` and apply constitution rules.
- **Rationale**: Low-risk way to ship a single widget surface across the app.
- **Alternatives considered**:
  - Per-feature widgets/pages: rejected (violates the “three surfaces only” rule).

## Open Questions

None (all clarifications captured in the feature spec).
