TenantAtlas/specs/103-ia-scope-filter-semantics/plan.md
Ahmed Darrazi 8b13d6f55f feat: refine tenant scope semantics
- Update OperateHub scope label copy (All tenants / Filtered by tenant)

- Fix Alerts KPI tenant resolution via activeEntitledTenant()

- Remove tenant indicator from manage lists

- Improve AlertRule form labels + sections

- UI polish: resource sections + tenant view widget layout + RBAC progressive disclosure

- Add/adjust Pest coverage
2026-02-21 01:00:31 +01:00

12 KiB
Raw Blame History

Implementation Plan: IA Semantics — Scope vs Filter vs Targeting (Monitoring + Manage)

Branch: 103-ia-scope-filter-semantics | Date: 2026-02-20 | Spec: spec.md Input: Feature specification from /specs/103-ia-scope-filter-semantics/spec.md

Summary

Replace "Scope: …" wording with "Filtered by tenant: …" / "All tenants" on Monitoring canonical pages, fix the AlertsKpiHeader tenant-resolution bug that causes KPI numbers to diverge from the indicator when lastTenantId fallback is active, remove the semantically incorrect tenant indicator from workspace-owned Manage pages (Alert Rules, Alert Destinations), and relabel + restructure the AlertRule edit form to use targeting semantics instead of scope semantics.

Technical approach: Update OperateHubShell::scopeLabel() (single source of truth for indicator copy), fix AlertsKpiHeader::deliveriesQueryForViewer() to call OperateHubShell::activeEntitledTenant() instead of Filament::getTenant(), remove header-action spreads from Manage list pages, and refactor AlertRuleResource::form() labels/sections.

Technical Context

Language/Version: PHP 8.4 (Laravel 12) Primary Dependencies: Filament v5, Livewire v4, OperateHubShell support class Storage: PostgreSQL — no schema changes Testing: Pest v4 (feature tests) Target Platform: Web (Laravel Sail / Dokploy) Project Type: Web application (Laravel monolith) Performance Goals: N/A — copy + bugfix, no performance-sensitive changes Constraints: DB-only renders on Monitoring pages (no Graph/HTTP calls) Scale/Scope: 5 source files modified, 12 test files updated/created

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • Inventory-first: N/A — no inventory data touched.
  • Read/write separation: No new writes introduced. The KPI bugfix corrects an existing read query filter — no preview/confirmation/audit needed.
  • Graph contract path: No Graph calls introduced or modified. All changes are DB-only.
  • Deterministic capabilities: No capability changes. Existing RBAC enforcement unchanged.
  • RBAC-UX (two planes): No routing changes. All affected pages remain on /admin plane. No cross-plane routing introduced.
  • Workspace isolation: No workspace isolation changes. Monitoring pages remain workspace-context. Manage pages remain workspace-owned.
  • RBAC-UX (destructive actions): No destructive actions added. Removal of OperateHubShell header actions from Manage pages removes informational indicators, not mutation actions.
  • RBAC-UX (global search): No global search changes.
  • Tenant isolation: The bugfix improves tenant isolation by ensuring KPI queries respect the resolved tenant context. No cross-tenant views introduced.
  • Run observability: No long-running/remote/queued work introduced. All changes are synchronous UI/copy.
  • Automation: N/A — no queued/scheduled ops.
  • Data minimization: N/A — no new data storage.
  • Badge semantics (BADGE-001): No status badges added or changed. The "Filtered by tenant" indicator is informational copy, not a status badge.
  • Filament UI Action Surface Contract: See UI Action Matrix in spec.md. No new actions. Removed header actions on Manage pages were informational. Existing destructive actions unchanged.
  • Filament UI UX-001: AlertRule form adds Section grouping (conforming to "all fields inside Sections/Cards"). No new pages.

Post-Phase 1 re-check: All gates remain satisfied. No design decisions introduced violations.

Project Structure

Documentation (this feature)

specs/103-ia-scope-filter-semantics/
├── plan.md              # This file
├── spec.md              # Feature specification
├── research.md          # Phase 0 output — codebase research findings
├── data-model.md        # Phase 1 output — no schema changes documented
├── quickstart.md        # Phase 1 output — files to touch + verification commands
├── checklists/
│   └── requirements.md  # Spec quality checklist (from /speckit.spec)
└── tasks.md             # Phase 2 output (NOT created by /speckit.plan)

Source Code (files to modify)

app/
├── Support/OperateHub/
│   └── OperateHubShell.php             # T1: Update scopeLabel() copy
├── Filament/
│   ├── Widgets/Alerts/
│   │   └── AlertsKpiHeader.php         # T2: Fix deliveriesQueryForViewer() bug
│   ├── Resources/
│   │   ├── AlertRuleResource.php       # T4/T5: Relabel form + add Sections
│   │   └── AlertRuleResource/Pages/
│   │       └── ListAlertRules.php      # T3: Remove OperateHubShell header spread
│   └── Resources/AlertDestinationResource/Pages/
│       └── ListAlertDestinations.php   # T3: Remove OperateHubShell header spread

tests/
├── Feature/OpsUx/
│   └── OperateHubShellTest.php         # T6: Update existing assertions for new copy
└── Feature/Filament/Alerts/
    └── AlertRuleCrudTest.php           # T6: Verify/update label-dependent assertions

Structure Decision: Existing Laravel monolith structure. All modifications are to existing files. No new directories or structural changes.

Implementation Tasks

T1 — Update OperateHubShell::scopeLabel() Copy (FR-001, FR-002, FR-003)

File: app/Support/OperateHub/OperateHubShell.php (line 2333)

Current:

public function scopeLabel(?Request $request = null): string
{
    $activeTenant = $this->activeEntitledTenant($request);
    if ($activeTenant instanceof Tenant) {
        return 'Scope: Tenant — '.$activeTenant->name;
    }
    return 'Scope: Workspace — all tenants';
}

Target:

public function scopeLabel(?Request $request = null): string
{
    $activeTenant = $this->activeEntitledTenant($request);
    if ($activeTenant instanceof Tenant) {
        return 'Filtered by tenant: '.$activeTenant->name;
    }
    return 'All tenants';
}

Propagation: This single change propagates to all consumers:

  • headerActions() within OperateHubShell itself (L61)
  • Operations.php (L66) — uses $operateHubShell->scopeLabel(request()) directly
  • TenantlessOperationRunViewer.php (L47) — uses $operateHubShell->scopeLabel(request()) directly
  • AuditLog.php, Alerts.php, ListAlertDeliveries.php — via headerActions()

FR mapping: FR-001 (filtered indicator), FR-002 (all-tenants indicator), FR-003 (no "Scope:" text), FR-014 (DB-only renders unchanged).


T2 — Fix AlertsKpiHeader::deliveriesQueryForViewer() Bug (FR-004, FR-005)

File: app/Filament/Widgets/Alerts/AlertsKpiHeader.php (line 101118)

Current (buggy):

protected function deliveriesQueryForViewer(): Builder
{
    $query = AlertDelivery::query()->where(...);
    $tenant = Filament::getTenant();  // ← BUG: null when lastTenantId fallback
    if ($tenant) {
        $query->where('tenant_id', $tenant->id);
    }
    return $query;
}

Target:

protected function deliveriesQueryForViewer(): Builder
{
    $query = AlertDelivery::query()->where(...);
    $tenant = app(OperateHubShell::class)->activeEntitledTenant(request());
    if ($tenant instanceof Tenant) {
        $query->where('tenant_id', $tenant->id);
    }
    return $query;
}

FR mapping: FR-004 (consistent tenant resolution), FR-005 (KPI filters by resolved tenant).


T3 — Remove OperateHubShell Header Actions from Manage Pages (FR-006)

Files:

  • app/Filament/Resources/AlertRuleResource/Pages/ListAlertRules.php (L2337)
  • app/Filament/Resources/AlertDestinationResource/Pages/ListAlertDestinations.php (L1730)

Change: Remove ...app(OperateHubShell::class)->headerActions(...) from getHeaderActions(). Keep only the CreateAction::make().

FR mapping: FR-006 (no tenant indicator on Manage pages).


T4 — Relabel AlertRule Form Fields (FR-007, FR-008, FR-009, FR-010)

File: app/Filament/Resources/AlertRuleResource.php (form method, ~L155+)

Changes:

Current Target
Select::make('tenant_scope_mode') label (auto-generated "Tenant scope mode") Explicit ->label('Applies to tenants')
Option 'Allowlist' 'Selected tenants'
Select::make('tenant_allowlist')->label('Tenant allowlist') ->label('Selected tenants')
No helper texts ->helperText('This rule is workspace-wide. Use this to limit where it applies.') on tenant_scope_mode
No helper texts ->helperText('Only these tenants will trigger this rule.') on tenant_allowlist

DB values: all / allowlist constants UNCHANGED. Only display labels change.

FR mapping: FR-007, FR-008, FR-009, FR-010, FR-012 (no domain behavior changes), FR-013 (no schema changes).


T5 — Add Section Grouping to AlertRule Form (FR-011)

File: app/Filament/Resources/AlertRuleResource.php (form method)

Change: Wrap existing flat fields into three Filament\Schemas\Components\Section groups:

  1. "Rule" section: Name, Enabled, Event type, Minimum severity
  2. "Applies to" section: Applies to tenants (tenant_scope_mode), Selected tenants (tenant_allowlist, conditional)
  3. "Delivery" section: Cooldown, Quiet hours, Destinations

Import: use Filament\Schemas\Components\Section; (verified in 11 existing files).

FR mapping: FR-011.


T6 — Update Tests (all FRs)

Files to update:

  • tests/Feature/OpsUx/OperateHubShellTest.php — update all assertSee('Scope: Workspace — all tenants')assertSee('All tenants') and all assertSee('Scope: Tenant — ')assertSee('Filtered by tenant: ')
  • tests/Feature/Filament/Alerts/AlertRuleCrudTest.php — check for label-dependent assertions, update if needed

New test coverage (add to existing test files or create new):

  • KPI consistency: Set tenant-context via lastTenantId only → assert deliveriesQueryForViewer() includes where tenant_id = X
  • Manage pages: Visit /admin/alert-rules with tenant-context → assertDontSee('Filtered by tenant') and assertDontSee('Scope: Tenant')
  • Form labels: Visit AlertRule edit page → assertSee('Applies to tenants'), assertDontSee('Tenant scope mode')
  • Section headings: Visit AlertRule edit page → assertSee('Rule'), assertSee('Applies to'), assertSee('Delivery')

T7 — (Optional, P3) "Add Current Tenant" Convenience Button (FR: User Story 6)

File: app/Filament/Resources/AlertRuleResource.php

Change: Add a form action button visible when tenant-context is active AND "Selected tenants" mode is visible. On click, append current tenant to tenant_allowlist field state.

Note: Only implement if T1T6 are complete and PR remains small. Skip if scope exceeds budget.

Filament v5 Compliance (Agent Output Contract)

  1. Livewire v4.0+ compliance: All code targets Livewire v4.0+. No v3 references.
  2. Provider registration: No new providers. Existing panel provider in bootstrap/providers.php unchanged.
  3. Globally searchable resources: AlertRuleResource has $isScopedToTenant = false — global search not applicable to workspace-owned resources. No changes to global search behavior.
  4. Destructive actions: No destructive actions added or modified. Existing destructive actions on these pages retain ->requiresConfirmation().
  5. Asset strategy: No new assets registered. No changes to filament:assets deployment.
  6. Testing plan: OperateHubShellTest updated for new copy. AlertRuleCrudTest verified/updated. New test assertions for KPI consistency, Manage page indicator suppression, and form labels/sections.

Complexity Tracking

No constitution violations. No complexity justifications needed.