## Summary - add explicit workspace closure and tenant removal lifecycle truth with a bounded `WorkspaceLifecycleService` - surface closure and removal posture across admin/system pages, chooser recovery, and canonical historical viewers - block new review-pack and operation starts for closed workspaces or removed tenants while preserving memberships, audit, and history - add focused Pest coverage plus the Spec 292 artifacts for the implemented slice ## Testing - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Directory/ViewWorkspaceClosureTest.php tests/Feature/System/Ops/ClosedWorkspaceHistoricalAccessTest.php tests/Feature/Filament/Resources/Workspaces/WorkspaceClosureStatusTest.php tests/Feature/Filament/Resources/TenantResource/TenantWorkspaceRemovalTest.php tests/Feature/Filament/Pages/WorkspaceContextClosureRecoveryTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - manual integrated-browser smoke for admin tenant remove/restore plus chooser recovery and system workspace close/reopen Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #337
24 KiB
Implementation Plan: Workspace & Tenant Closure Lifecycle v1
Branch: 292-workspace-tenant-closure | Date: 2026-05-07 | Spec: spec.md
Input: Feature specification from specs/292-workspace-tenant-closure/spec.md
Summary
Prepare one bounded lifecycle runtime follow-through on top of the Spec 262 taxonomy foundation plus the already-real tenant operability and commercial lifecycle seams. The narrow implementation path is to add explicit workspace closed truth on Workspace, explicit removed-from-workspace truth on ManagedEnvironment, keep commercial suspension as a separate shared gate, and reuse the existing system directory, admin resource, chooser, and canonical historical viewer surfaces instead of opening a new workflow console.
This slice stays deliberately narrow. Filament remains v5 on Livewire v4, panel-provider registration stays in apps/platform/bootstrap/providers.php, no new globally searchable resource is introduced, and no asset registration change is expected. The plan does not reopen the broader taxonomy, does not implement purge or export-before-delete, does not add payment-provider or billing-portal behavior, and does not add a new OperationRun family.
Inherited Baseline / Explicit Delta
Inherited baseline
Workspacealready carriesarchived_at, andWorkspaceContext::isWorkspaceSelectable()already uses that archived truth to prevent invalid workspace selection.ManagedEnvironmentalready carries tenant lifecycle truth, andTenantLifecycleplusTenantOperabilityServicealready distinguish active context from onboarding or archive semantics.WorkspaceCommercialLifecycleResolveralready maps commercial truth intoSUSPENDED_READ_ONLYversus normal workspace posture for high-impact actions.EnsureWorkspaceSelected,EnsureFilamentTenantSelected, andDenyNonMemberTenantAccessalready provide the current context-recovery and deny-as-not-found seams.WorkspaceMembershipManager,TenantMembershipManager,WorkspaceAuditLogger, and current tenant audit paths already provide bounded audit-safe mutation seams.- Existing admin and system surfaces already exist at
ViewWorkspace,ViewTenant,ViewRun,ChooseWorkspace,ChooseTenant,WorkspaceResource,TenantResource, andWorkspaceSettings.
Explicit delta in this plan
- Add explicit closure truth to
Workspaceusing bounded persisted fields for closed posture, actor, and reason. - Add explicit removed-from-workspace truth to
ManagedEnvironmentusing bounded persisted fields for removal posture, actor, and reason. - Introduce one bounded
WorkspaceLifecycleServiceto orchestrate close/reopen and remove/restore without opening a generic lifecycle framework. - Keep
archived,suspended read-only,closed, andremoved from workspaceas four distinct meanings with separate badges, copy, and blocked-action explanations. - Update chooser recovery, tenant-context legitimacy, and canonical historical viewers so historical records remain readable while invalid active context is cleared explicitly.
- Keep all new lifecycle mutations inside current admin and system surfaces and current audit infrastructure.
Technical Context
Language/Version: PHP 8.4, Laravel 12
Primary Dependencies: Filament v5, Livewire v4, Pest v4, existing WorkspaceContext, TenantOperabilityService, WorkspaceCommercialLifecycleResolver, current audit infrastructure, current Filament admin and system pages
Storage: PostgreSQL via targeted new lifecycle fields on existing workspaces and managed_environments tables plus existing audit and history tables
Testing: Pest v4 focused Feature coverage
Validation Lanes: fast-feedback, confidence
Target Platform: Laravel monolith in apps/platform across the existing admin and system Filament panels
Project Type: Web application (Laravel monolith with Filament panels)
Performance Goals: DB-only lifecycle gating, no new queue family, no new Graph calls, no new browser-only proof requirement
Constraints: no purge, no export-before-delete, no payment-provider or billing workflow, no new global-search resource, no new panel, no lifecycle engine, no asset-registration change
Scale/Scope: 2 existing persisted records gain bounded lifecycle truth, 1 bounded service seam, 6 existing operator surfaces, and focused feature-test extensions
Likely Affected Repo Surfaces
apps/platform/app/Models/Workspace.phpapps/platform/app/Models/ManagedEnvironment.phpapps/platform/database/migrations/*_add_workspace_closure_fields.phpapps/platform/database/migrations/*_add_managed_environment_workspace_removal_fields.phpapps/platform/app/Services/Workspaces/WorkspaceLifecycleService.phpapps/platform/app/Support/Workspaces/WorkspaceContext.phpapps/platform/app/Services/Tenants/TenantOperabilityService.phpapps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.phpapps/platform/app/Http/Middleware/EnsureWorkspaceSelected.phpapps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.phpapps/platform/app/Support/Middleware/DenyNonMemberTenantAccess.phpapps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.phpapps/platform/app/Filament/System/Pages/Directory/ViewTenant.phpapps/platform/app/Filament/System/Pages/Ops/ViewRun.phpapps/platform/app/Filament/Pages/ChooseWorkspace.phpapps/platform/app/Filament/Pages/ChooseTenant.phpapps/platform/app/Filament/Pages/Settings/WorkspaceSettings.phpapps/platform/app/Filament/Resources/Workspaces/WorkspaceResource.phpapps/platform/app/Filament/Resources/Workspaces/Pages/ViewWorkspace.phpapps/platform/app/Filament/Resources/TenantResource.phpapps/platform/app/Services/Audit/WorkspaceAuditLogger.php- current tenant audit logging path where tenant membership and lifecycle actions are recorded
- focused feature tests under
apps/platform/tests/Feature/System/,apps/platform/tests/Feature/Filament/Resources/, andapps/platform/tests/Feature/Filament/Pages/
Lifecycle Truth Fit
- Treat workspace closure as a dedicated workspace-owned lifecycle posture, not as archive and not as commercial suspension.
- Treat tenant removal from workspace as a dedicated workspace-owned posture on the tenant record, not as tenant archive and not as provider absence.
- Keep commercial suspension in
WorkspaceCommercialLifecycleResolveras the shared source ofSUSPENDED_READ_ONLY; closure logic must compose with it, not replace it. - Preserve current tenant lifecycle and provider-missing semantics; the feature only adds workspace-governed closure and removal truth.
- Preserve historical readability:
- closed workspace -> not selectable as current workspace, read-only historical inspection remains available to entitled actors
- removed tenant -> not selectable as current tenant, canonical historical inspection remains available to entitled actors
- suspended read-only -> current workspace remains inspectable and current tenant may remain visible, but mutations and starts stay blocked per existing commercial rules
Data & Query Fit
- Extend
workspaceswith bounded closure fields only. The expected shape is a timestamp, actor reference, and reason text rather than a broad lifecycle ledger. - Extend
managed_environmentswith bounded removed-from-workspace fields only. The expected shape mirrors the workspace closure truth and remains reversible. - Add only the indexes needed to filter active versus closed workspaces and active versus removed tenants.
- Keep lifecycle truth on the primary records instead of introducing a new history table. Audit remains the historical source for who changed posture and when.
- Preserve existing ownership rules: workspaces stay workspace-owned, tenants stay workspace-owned, and no cross-workspace migration or copy is introduced.
UI / Filament & Livewire Fit
- Existing operator-facing surfaces remain native Filament surfaces under Livewire v4; this slice must stay inside those surfaces.
ViewWorkspacein the system plane keeps one dominant action:Close workspaceorReopen workspace.- Admin workspace detail gains read-only posture summary only; no second mutation plane for closure appears on
/admin. - Managed tenant list keeps row click as the inspect model and moves remove or restore into
More; managed tenant detail may exposeRemove tenantorRestore tenantas the dominant lifecycle action. - Chooser surfaces remain chooser surfaces. They gain explicit recovery messaging, not a new workflow.
- Canonical historical viewers gain posture badges or supporting text only; they do not become mutation surfaces.
- Confirmation modals and success or error notifications on the in-scope action surfaces must reuse the canonical verbs
Close workspace,Reopen workspace,Remove tenant, andRestore tenant. - No new globally searchable Filament resource is introduced, so there is no new global-search Edit or View page requirement to satisfy.
- Provider registration remains unchanged in
apps/platform/bootstrap/providers.php, and no new asset strategy is planned. If future shared assets ever become necessary, deployment remains the normalcd apps/platform && php artisan filament:assetspath.
RBAC / Policy Fit
- Workspace and tenant membership remain the isolation boundaries.
- Wrong-plane actors and non-members stay
404; in-scope actors missing capability stay403. - Closure and removal actions remain server-side authorized and confirmation-protected.
- Platform closure uses the current platform workspace-governance seam or the narrowest bounded extension of it. Tenant removal uses the current workspace owner or equivalent tenant-governance seam.
- The exact write-side enforcement seam is
apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php, and the in-scope mutation entry points are the system workspace detail actions inapps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.phpand the tenant lifecycle actions inapps/platform/app/Filament/Resources/TenantResource.php. - Unsafe close or remove attempts must preserve current guard failures with a clear operator-facing reason and no partial lifecycle mutation.
- Closure or removal posture must never substitute for authorization. A historically readable record still requires entitlement.
- Tenant-context routes under
/admin/t/{tenant}/...must reject removed tenants as active context targets while preserving canonical viewers that are not tenant-context routes.
Audit / Logging Fit
- Every close/reopen and remove/restore mutation must write an audit event with actor, old posture, new posture, timestamp, and reason.
- Existing audit infrastructure must be reused rather than opening a new lifecycle audit subsystem.
- Historical viewers should surface closure or removal context through current summary and linked audit rather than duplicating the full audit payload.
- Blocked active-context starts caused by closure or removal do not need a new audit family in this slice unless existing blocked-action logging already applies.
UI / Surface Guardrail Plan
- Guardrail scope: changed surfaces
- Native vs custom classification summary: native Filament
- Shared-family relevance: status messaging, chooser recovery, detail actions, canonical historical viewers, audit-backed lifecycle copy
- State layers in scope: shell, page, detail
- Audience modes in scope: operator-MSP, support-platform
- Decision/diagnostic/raw hierarchy plan: decision-first, diagnostics-second, support-raw-third
- Raw/support gating plan: raw audit and low-level identifiers remain secondary and platform-only where applicable
- One-primary-action / duplicate-truth control: system workspace detail keeps one dominant closure action; tenant detail keeps one dominant remove/restore action; chooser recovery surfaces keep one next step
- Handling modes by drift class or surface: review-mandatory
- Repository-signal treatment: review-mandatory now; hard-stop candidate if implementation adds a second closure plane or local lifecycle vocabulary
- Special surface test profiles: standard-native-filament, global-context-shell, shared-detail-family
- Required tests or manual smoke: functional-core, state-contract
- Exception path and spread control: none planned; any lifecycle dashboard, broad workbench, or browser-heavy proof demand is out-of-scope drift
- Active feature PR close-out entry: Guardrail
Shared Pattern & System Fit
- Cross-cutting feature marker: yes
- Systems touched:
WorkspaceContext,TenantOperabilityService,WorkspaceCommercialLifecycleResolver, chooser pages, system directory pages, admin workspace and tenant resources, audit logging, and canonical historical viewers - Shared abstractions reused: current context manager, current commercial lifecycle resolver, current tenant operability service, current audit infrastructure,
BadgeCatalogandBadgeRenderer, current Filament action-surface patterns - New abstraction introduced? why?: one bounded
WorkspaceLifecycleService, because close/reopen and remove/restore each require coordinated record mutation, audit logging, and shared blocked-action consequences across multiple surfaces - Why the existing abstraction was sufficient or insufficient: current services already decide selectability, commercial blocking, and audit shape, but no existing service owns the explicit closure or removal mutation path or the shared write-side behavior for it
- Bounded deviation / spread control: no second closure orchestration helper is allowed outside the bounded service path
OperationRun UX Impact
- Touches OperationRun start/completion/link UX?: yes, by reuse and blocking only
- Central contract reused: current shared
OperationRunstart UX and current canonical monitoring pages remain authoritative - Delegated UX behaviors: blocked starts in closed workspaces or removed-tenant contexts must fail before enqueue on
apps/platform/app/Filament/Widgets/Tenant/TenantReviewPackCard.php; existing run links and canonical historical viewers remain unchanged for already-created runs - Surface-owned behavior kept local: close/reopen and remove/restore input collection plus impact summaries only
- Queued DB-notification policy: unchanged
- Terminal notification path: unchanged central lifecycle mechanism
- Exception path: none
Provider Boundary & Portability Fit
- Shared provider/platform boundary touched?: no
- Provider-owned seams: none in this slice
- Platform-core seams: workspace closure truth, tenant removal truth, chooser recovery, and historical record readability
- Neutral platform terms / contracts preserved:
workspace,tenant,closed,removed from workspace,suspended read-only,history - Retained provider-specific semantics and why: none
- Bounded extraction or follow-up path: provider-missing and other provider lifecycle work stays in separate follow-up specs
Constitution Check
GATE: Must pass before implementation begins and again before merge.
- Inventory-first / snapshot truth: PASS. No inventory or snapshot source is reinterpreted.
- Read/write separation: PASS. The only new writes are explicit lifecycle mutations with confirmation, audit, and test requirements.
- Graph contract path: PASS. No Graph calls are introduced.
- Deterministic capabilities: PASS. Existing capability registries remain authoritative.
- Workspace and tenant isolation: PASS. Existing 404 and 403 rules remain and are reinforced.
- RBAC-UX plane separation: PASS. System owns workspace closure mutation; admin remains read-only for workspace closure and bounded for tenant removal inside the current workspace.
- Destructive action discipline: PASS. Close/reopen and remove/restore are destructive-like and must remain confirmation-protected.
- Global search safety: PASS. No new searchable resource is introduced.
- OperationRun / Ops-UX: PASS by reuse only. No new run family is added, and blocked starts stay pre-enqueue.
- Data minimization: PASS. Only bounded lifecycle truth fields are added on existing records.
- Test governance: PASS. Focused feature lanes are the narrowest honest proof.
- Proportionality / no premature abstraction: PASS. One bounded service is the narrowest write-side seam; no generic lifecycle framework is planned.
- Persisted truth: PASS. Closure and removal posture are real product truth with independent lifecycle and audit need.
- Behavioral state: PASS. Closed and removed-from-workspace change chooser behavior, mutation legality, and historical-view posture.
- Shared pattern first / UI semantics / Filament-native UI: PASS. Existing chooser, audit, badge, and Filament surfaces remain the shared path.
- Provider boundary: PASS. No provider-specific semantics are added.
- Filament / Laravel planning contract: PASS. Filament stays v5 on Livewire v4, provider registration remains in
apps/platform/bootstrap/providers.php, no globally searchable resource is added, and no asset registration change is planned.
Gate evaluation: PASS.
Test Governance Check
- Test purpose / classification by changed surface:
Featurefor system workspace governance, admin read-only posture, managed-tenant remove/restore, chooser recovery, and canonical historical viewer legitimacy - Affected validation lanes: fast-feedback, confidence
- Why this lane mix is the narrowest sufficient proof: the slice is about integrated route, page, capability, chooser, and audit behavior; unit-only proof would miss the real lifecycle consequences
- Narrowest proving command(s):
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Directory/ViewWorkspaceClosureTest.php tests/Feature/System/Ops/ClosedWorkspaceHistoricalAccessTest.phpexport PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/Resources/Workspaces/WorkspaceClosureStatusTest.php tests/Feature/Filament/Resources/TenantResource/TenantWorkspaceRemovalTest.php tests/Feature/Filament/Pages/WorkspaceContextClosureRecoveryTest.phpexport PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
- Fixture / helper / factory / seed / context cost risks: moderate but contained; setup needs explicit workspace and tenant membership plus lifecycle posture states, but no new provider, browser, or heavy defaults
- Expensive defaults or shared helper growth introduced?: no
- Heavy-family additions, promotions, or visibility changes: none planned
- Surface-class relief / special coverage rule: standard-native-filament relief for the system and admin detail surfaces; global-context-shell coverage for chooser recovery; shared-detail-family coverage for historical viewers
- Closing validation and reviewer handoff: reviewers should rely on the exact commands above, verify that no second mutation plane or lifecycle vocabulary appeared, and confirm that blocked starts stay pre-enqueue while historical viewers stay readable
- Budget / baseline / trend follow-up: none expected beyond a small feature-local feature-test increase
- Review-stop questions: did the slice collapse closure into archive or suspension, did it break canonical history viewers, did it add a second mutation plane, and did it widen into purge/export/billing scope
- Escalation path:
document-in-featurefor contained naming or surface drift;reject-or-splitif the slice widens into broader offboarding, purge, or billing workflows - Active feature PR close-out entry: Guardrail
- Why no dedicated follow-up spec is needed: the remaining adjacent work is already known and separately named; this slice is the bounded workspace and tenant closure follow-through itself
- Test-governance outcome: keep
Rollout Considerations
- Land persistence and the bounded service seam first, then workspace closure surfaces plus chooser recovery, then tenant removal surfaces plus canonical historical viewer polish.
- Keep admin workspace detail read-only for closure mutation throughout the slice.
- Keep commercial suspension readable and separate from closure throughout the rollout.
- Keep no asset changes, no provider registration changes, no new panel work, and no global-search work.
Risk Controls
- Reject any implementation that reuses
archived_ator commercial suspension as the new closure truth. - Reject any implementation that deletes memberships, tenants, workspaces, or historical records as part of close/reopen or remove/restore.
- Reject any implementation that makes
/admina second workspace-closure mutation plane. - Reject any implementation that turns the slice into export, purge, payment-provider, or portal work.
- Reject browser-heavy proof as the default validation lane.
Project Structure
Documentation (this feature)
specs/292-workspace-tenant-closure/
├── plan.md
├── spec.md
└── tasks.md
Source Code (expected implementation surfaces)
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/
│ │ │ ├── ChooseTenant.php
│ │ │ ├── ChooseWorkspace.php
│ │ │ └── Settings/
│ │ │ └── WorkspaceSettings.php
│ │ ├── Resources/
│ │ │ ├── TenantResource.php
│ │ │ └── Workspaces/
│ │ │ ├── WorkspaceResource.php
│ │ │ └── Pages/
│ │ │ └── ViewWorkspace.php
│ │ └── System/Pages/
│ │ ├── Directory/
│ │ │ ├── ViewTenant.php
│ │ │ └── ViewWorkspace.php
│ │ └── Ops/
│ │ └── ViewRun.php
│ ├── Http/Middleware/
│ │ └── EnsureWorkspaceSelected.php
│ ├── Models/
│ │ ├── ManagedEnvironment.php
│ │ └── Workspace.php
│ ├── Services/
│ │ ├── Audit/
│ │ │ └── WorkspaceAuditLogger.php
│ │ ├── Entitlements/
│ │ │ └── WorkspaceCommercialLifecycleResolver.php
│ │ ├── Tenants/
│ │ │ └── TenantOperabilityService.php
│ │ └── Workspaces/
│ │ └── WorkspaceLifecycleService.php
│ └── Support/
│ ├── Middleware/
│ │ ├── DenyNonMemberTenantAccess.php
│ │ └── EnsureFilamentTenantSelected.php
│ └── Workspaces/
│ └── WorkspaceContext.php
├── database/
│ └── migrations/
└── tests/
└── Feature/
├── Filament/
└── System/
Structure Decision: Keep the slice inside the existing Laravel monolith and current Filament admin plus system surfaces. Add only targeted lifecycle fields, one bounded service, and focused feature tests.
Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| Bounded lifecycle write service | Close/reopen and remove/restore need coordinated mutation, audit, and shared blocked-action behavior | Controller or page-local closures would duplicate audit and lifecycle consequences across multiple surfaces |
Proportionality Review
- Current operator problem: Operators cannot deliberately close a workspace or remove a tenant from a workspace while keeping history readable and posture explicit.
- Existing structure is insufficient because: archive and commercial suspension represent different meanings and cannot safely absorb closure or removal truth.
- Narrowest correct implementation: add bounded fields on existing records, route writes through one bounded service, and reuse current chooser, audit, and Filament surfaces.
- Ownership cost created: migrations, one service, shared status and chooser updates, and feature tests.
- Alternative intentionally rejected: generic lifecycle engine, new closure history tables, or overloading archive and suspension.
- Release truth: current-release truth.