Implements Spec 103 (IA semantics: Scope vs Filter vs Targeting) across Monitoring + Manage. Changes - Monitoring tenant indicator copy: “All tenants” / “Filtered by tenant: …” - Alerts KPI header resolves tenant via OperateHubShell::activeEntitledTenant() for consistency - Manage list pages (Alert Rules / Destinations) no longer show tenant indicator - AlertRule form uses targeting semantics + sections (Rule / Applies to / Delivery) - Additional UI polish: resource sections, tenant view widgets layout, RBAC progressive disclosure (“Not configured” when empty) Notes - US6 (“Add current tenant” convenience button) intentionally skipped (optional P3). Testing - CI=1 vendor/bin/sail artisan test tests/Feature/TenantRBAC/ tests/Feature/Onboarding/OnboardingIdentifyTenantTest.php - vendor/bin/sail bin pint --dirty --format agent Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #126
12 KiB
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, 1–2 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
/adminplane. 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 23–33)
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()withinOperateHubShellitself (L61)Operations.php(L66) — uses$operateHubShell->scopeLabel(request())directlyTenantlessOperationRunViewer.php(L47) — uses$operateHubShell->scopeLabel(request())directlyAuditLog.php,Alerts.php,ListAlertDeliveries.php— viaheaderActions()
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 101–118)
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(L23–37)app/Filament/Resources/AlertDestinationResource/Pages/ListAlertDestinations.php(L17–30)
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:
- "Rule" section: Name, Enabled, Event type, Minimum severity
- "Applies to" section: Applies to tenants (tenant_scope_mode), Selected tenants (tenant_allowlist, conditional)
- "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 allassertSee('Scope: Workspace — all tenants')→assertSee('All tenants')and allassertSee('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()includeswhere tenant_id = X - Manage pages: Visit
/admin/alert-ruleswith tenant-context →assertDontSee('Filtered by tenant')andassertDontSee('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 T1–T6 are complete and PR remains small. Skip if scope exceeds budget.
Filament v5 Compliance (Agent Output Contract)
- Livewire v4.0+ compliance: All code targets Livewire v4.0+. No v3 references.
- Provider registration: No new providers. Existing panel provider in
bootstrap/providers.phpunchanged. - Globally searchable resources: AlertRuleResource has
$isScopedToTenant = false— global search not applicable to workspace-owned resources. No changes to global search behavior. - Destructive actions: No destructive actions added or modified. Existing destructive actions on these pages retain
->requiresConfirmation(). - Asset strategy: No new assets registered. No changes to
filament:assetsdeployment. - 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.