227 lines
15 KiB
Markdown
227 lines
15 KiB
Markdown
# Data Model: Global Context Shell Contract
|
|
|
|
## Persistence Impact
|
|
|
|
- No new database tables, columns, or persisted shell artifacts are introduced.
|
|
- Existing session-backed fields remain the only durable support state used by the contract:
|
|
- `current_workspace_id`
|
|
- `workspace_intended_url`
|
|
- `workspace_last_tenant_ids`
|
|
- Existing user-level fields such as `users.last_workspace_id` and `users.last_tenant_id` or `user_tenant_preferences.last_used_at` remain support inputs only and do not become active shell truth.
|
|
|
|
## Context Source Inventory
|
|
|
|
| Source | Context facet | Source role | Owning seam | Validation / notes |
|
|
|---|---|---|---|---|
|
|
| Explicit workspace switch request | Workspace | leading | `SwitchWorkspaceController` + `WorkspaceContext` | Must point to an accessible, selectable workspace before it can replace current workspace truth. |
|
|
| Current session workspace | Workspace | leading | `WorkspaceContext` | Remains the default current-workspace truth when no stronger explicit request exists and membership is still valid. |
|
|
| Remembered last workspace | Workspace | supporting | `WorkspaceContext` | Restore-only candidate used when no valid current session workspace exists and the entry flow allows restore. |
|
|
| Route tenant parameter | Tenant | leading | Route + `OperateHubShell` | Strongest tenant source on tenant-bound routes. Must belong to the resolved workspace and remain entitled. |
|
|
| Explicit tenant selection request | Tenant | leading | `SelectTenantController` + `OperateHubShell` | Can activate tenant scope only inside an already resolved workspace. |
|
|
| Filament panel tenant state | Tenant | supporting | `ResolvesPanelTenantContext` | May support resolution only after workspace compatibility and entitlement checks succeed. |
|
|
| Remembered tenant for resolved workspace | Tenant | supporting | `WorkspaceContext` | Restore-only candidate on tenantless-capable workspace pages. Never valid on its own for tenant-bound routes. |
|
|
| Query hint | Workspace or Tenant | supporting only when contract explicitly allows it | `OperateHubShell` | Must never become effective truth unless the contract explicitly names the query-backed flow. |
|
|
| View-local shell inference | Workspace or Tenant | never-leading | Shared shell partials and page-local views | Rendering surfaces may display resolved truth only. They cannot evaluate precedence or recovery. |
|
|
|
|
## Runtime Entities
|
|
|
|
| Entity | Kind | Fields | Validation / Notes |
|
|
|---|---|---|---|
|
|
| RequestedWorkspaceContext | Derived runtime input | `workspaceIdentifier`, `source`, `intendedUrl`, `pageCategory` | Represents a workspace request from route, explicit switch flow, or initial restore path before validation. |
|
|
| RequestedTenantContext | Derived runtime input | `tenantIdentifier`, `source`, `pageCategory`, `requiresExplicitTenant` | Represents route tenant, explicit tenant select, query hint, Filament tenant, or remembered tenant before validation. |
|
|
| RememberedContextCandidate | Derived support state | `workspaceId`, `tenantId`, `source`, `eligible` | Represents stored last-used tenant for the active workspace or last-used workspace during initial resolution. Never leading by itself. |
|
|
| ResolvedShellContext | Canonical request-scoped truth | `workspace`, `tenant`, `pageCategory`, `workspaceSource`, `tenantSource`, `state`, `recoveryDirective`, `displayMode` | The only context object shell UI and server-side consumers should trust for the current request. |
|
|
| RecoveryDirective | Derived outcome | `action`, `destination`, `reason`, `preserveIntendedUrl` | Encodes whether the request renders tenantless state, redirects, or aborts. |
|
|
| InvalidContext | Derived runtime outcome | `kind`, `source`, `reason`, `requestedWorkspaceIdentifier`, `requestedTenantIdentifier` | Captures why a requested or remembered context could not become active truth. |
|
|
|
|
## Supporting Enums / Value Domains
|
|
|
|
### ContextSource
|
|
|
|
- `route`
|
|
- `explicit_switch`
|
|
- `explicit_select`
|
|
- `session_workspace`
|
|
- `filament_tenant`
|
|
- `remembered`
|
|
- `query_hint`
|
|
- `none`
|
|
|
|
### ShellState
|
|
|
|
- `tenant_scoped`
|
|
- `tenantless_workspace`
|
|
- `missing_workspace`
|
|
- `invalid_workspace`
|
|
- `missing_tenant`
|
|
- `invalid_tenant`
|
|
- `inaccessible_tenant`
|
|
- `incompatible_tenant`
|
|
|
|
### RecoveryAction
|
|
|
|
- `none`
|
|
- `render_tenantless_workspace`
|
|
- `redirect_choose_workspace`
|
|
- `redirect_operations_index`
|
|
- `redirect_evidence_overview`
|
|
- `redirect_workspace_home`
|
|
- `redirect_workspace_managed_tenants`
|
|
- `redirect_workspace_record_fallback`
|
|
- `abort_not_found`
|
|
|
|
### PageCategory
|
|
|
|
- `workspace_scoped`
|
|
- `workspace_chooser_exception`
|
|
- `tenant_bound`
|
|
- `tenant_scoped_evidence`
|
|
- `canonical_workspace_record_viewer`
|
|
|
|
## Entity Details
|
|
|
|
### RequestedWorkspaceContext
|
|
|
|
| Field | Type | Required | Notes |
|
|
|---|---|---|---|
|
|
| `workspaceIdentifier` | string or int | yes | May come from explicit workspace switch flow or a safe intended URL restore path. |
|
|
| `source` | `ContextSource` | yes | `explicit_switch`, `session_workspace`, or `remembered` are the main inputs today. |
|
|
| `intendedUrl` | string or null | no | Safe `/admin...` path captured via `WorkspaceIntendedUrl`. |
|
|
| `pageCategory` | `PageCategory` | yes | Needed to determine if a tenantless fallback is valid after workspace resolution. |
|
|
|
|
**Validation rules**:
|
|
|
|
- Workspace must exist.
|
|
- Workspace must not be archived or otherwise unselectable.
|
|
- User must be a member of the workspace.
|
|
- Cross-plane routes remain out of scope; only `web`-guarded admin and tenant routes participate.
|
|
|
|
### RequestedTenantContext
|
|
|
|
| Field | Type | Required | Notes |
|
|
|---|---|---|---|
|
|
| `tenantIdentifier` | string or int | yes | May come from route param, explicit tenant selection, query hint, Filament panel state, or remembered session state. |
|
|
| `source` | `ContextSource` | yes | Route and explicit selection are strongest; remembered is weakest. |
|
|
| `pageCategory` | `PageCategory` | yes | Determines whether tenant fallback is valid, optional, or forbidden. |
|
|
| `requiresExplicitTenant` | bool | yes | `true` for tenant-bound pages; `false` for workspace-scoped pages that can remain tenantless. |
|
|
|
|
**Validation rules**:
|
|
|
|
- Tenant must exist.
|
|
- Tenant must belong to the resolved workspace.
|
|
- User must be entitled to the tenant.
|
|
- Tenant must satisfy the relevant operability question for the current lane.
|
|
- Tenant must be compatible with the current route type.
|
|
|
|
### RememberedContextCandidate
|
|
|
|
| Field | Type | Required | Notes |
|
|
|---|---|---|---|
|
|
| `workspaceId` | int | yes | Key for the remembered tenant map. |
|
|
| `tenantId` | int or null | no | Candidate tenant for restore. |
|
|
| `source` | `ContextSource` | yes | Today this is `remembered`, with supporting user-level last-used values. |
|
|
| `eligible` | bool | yes | `false` once access, operability, or workspace match fails. |
|
|
| `invalidReason` | string or null | no | Captures why the remembered candidate became ineligible during validation or cleanup. |
|
|
|
|
**Validation rules**:
|
|
|
|
- Candidate tenant must still exist.
|
|
- Candidate tenant must still belong to the active workspace.
|
|
- Candidate tenant must still be accessible to the user.
|
|
- Candidate tenant must still pass `RememberedContextValidity` for the current lane.
|
|
- Ineligible remembered context is cleared immediately and cannot survive as visible shell truth.
|
|
|
|
### ResolvedShellContext
|
|
|
|
| Field | Type | Required | Notes |
|
|
|---|---|---|---|
|
|
| `workspace` | Workspace or null | yes | Null only when recovery requires chooser or not-found handling. |
|
|
| `tenant` | Tenant or null | yes | Null is valid only in tenantless workspace state or before a hard recovery redirect. |
|
|
| `pageCategory` | `PageCategory` | yes | Controls whether tenantless state is valid. |
|
|
| `workspaceSource` | `ContextSource` | yes | Records which source actually won for workspace resolution. |
|
|
| `tenantSource` | `ContextSource` | yes | Records which source actually won for tenant resolution, or `none`. |
|
|
| `state` | `ShellState` | yes | The user-visible shell state. |
|
|
| `recoveryDirective` | `RecoveryDirective` | yes | Defines what to render or where to redirect if the request cannot continue as requested. |
|
|
| `displayMode` | string | yes | `tenant_scoped`, `tenantless`, or `recovery`. |
|
|
|
|
**Invariants**:
|
|
|
|
- A resolved tenant cannot exist without a resolved workspace.
|
|
- Remembered context cannot become active if a stronger valid source exists.
|
|
- The shell display must derive only from `ResolvedShellContext`.
|
|
- Tenant-bound pages cannot render a remembered-tenant fallback as though it were an explicit route tenant.
|
|
|
|
### InvalidContext
|
|
|
|
| Field | Type | Required | Notes |
|
|
|---|---|---|---|
|
|
| `kind` | string | yes | `workspace` or `tenant` |
|
|
| `source` | `ContextSource` | yes | Identifies whether route, panel, remembered, or query input failed. |
|
|
| `reason` | string | yes | `missing`, `inaccessible`, `incompatible`, `not_operable`, `not_member`, `archived`, or `mismatched_workspace` |
|
|
| `requestedWorkspaceIdentifier` | string or int or null | no | Included for diagnostics and testing only. |
|
|
| `requestedTenantIdentifier` | string or int or null | no | Included for diagnostics and testing only. |
|
|
|
|
## Relationships
|
|
|
|
- `ResolvedShellContext` is composed from zero or one `RequestedWorkspaceContext`, zero or one `RequestedTenantContext`, zero or one `RememberedContextCandidate`, and zero or one `InvalidContext` plus `RecoveryDirective`.
|
|
- `RecoveryDirective` is downstream of `ResolvedShellContext.state` and `PageCategory`.
|
|
- `RememberedContextCandidate.workspaceId` is always keyed to the resolved workspace candidate; it is not global across workspaces.
|
|
|
|
## Resolution Rules
|
|
|
|
### Workspace Resolution
|
|
|
|
1. Try a valid explicit workspace request when the current entry flow provides one.
|
|
2. Otherwise use the current session workspace if it remains valid.
|
|
3. Otherwise allow a valid last-used workspace restore only during initial resolution.
|
|
4. Otherwise emit `missing_workspace` or `invalid_workspace` with a chooser-oriented recovery directive.
|
|
|
|
### Tenant Resolution
|
|
|
|
1. On tenant-bound pages, validate route tenant first and fail if it is missing, inaccessible, or incompatible.
|
|
2. On workspace-scoped pages, accept a valid route tenant or explicit tenant-selection request first.
|
|
3. Next accept a validated query-backed tenant hint only on routes where the contract explicitly allows query-backed shell resolution.
|
|
4. Next accept validated `Filament::getTenant()` only if it matches the resolved workspace and remains entitled.
|
|
5. Next accept remembered tenant only when the page category permits tenantless fallback and no stronger valid tenant source exists.
|
|
6. Otherwise resolve to tenantless workspace state or to an explicit recovery directive depending on page category.
|
|
|
|
## Recovery Matrix
|
|
|
|
| Page category | Invalid workspace | Invalid explicit tenant | Missing tenant after clear | Invalid remembered tenant |
|
|
|---|---|---|---|---|
|
|
| `workspace_scoped` | redirect to chooser, or to `admin.operations.index` when cleanup is referrer-free or sentinel-driven | render tenantless workspace or clear request | render tenantless workspace on the current route, or use `admin.operations.index` when no safe prior route exists | clear remembered tenant and remain tenantless |
|
|
| `workspace_chooser_exception` | remain on `/admin/choose-workspace` | not applicable | remain on `/admin/choose-workspace` until the user selects a workspace | not applicable |
|
|
| `tenant_bound` | 404 or redirect to chooser if no workspace can be re-established | 404 when route tenant is invalid or inaccessible | redirect to `admin.workspace.managed-tenants.index` for the current workspace, else `admin.home` | ignored as active truth; route still governs |
|
|
| `tenant_scoped_evidence` | redirect to chooser if workspace truth cannot be re-established | redirect to `admin.evidence.overview` when tenant detail context is no longer valid | redirect to `admin.evidence.overview` | clear remembered tenant and return to `admin.evidence.overview` |
|
|
| `canonical_workspace_record_viewer` | 404 if the record itself is no longer entitled | 404 if tenant-scoped record access fails | remain on `admin.operations.view` when it is still workspace-safe, otherwise use the documented workspace fallback | clear remembered tenant and keep record-only rules |
|
|
|
|
## Documented Recovery Destinations
|
|
|
|
| RecoveryAction | Route target | When it applies |
|
|
|---|---|---|
|
|
| `redirect_choose_workspace` | `/admin/choose-workspace` | Missing or unrecoverable workspace truth at shell entry or restore time |
|
|
| `redirect_operations_index` | `admin.operations.index` | External or missing referrer, clear-flow sentinel path, and generic workspace-safe fallback for tenantless monitoring entry |
|
|
| `redirect_evidence_overview` | `admin.evidence.overview` | Tenant-scoped evidence paths that must return to a workspace-safe evidence landing |
|
|
| `redirect_workspace_managed_tenants` | `admin.workspace.managed-tenants.index` | Tenant-bound cleanup or workspace switch flows that must return to tenant selection inside the resolved workspace |
|
|
| `redirect_workspace_home` | `admin.home` | Tenant-bound cleanup when no current workspace truth remains available |
|
|
| `redirect_workspace_record_fallback` | `admin.operations.view` in the current-release scope | Canonical workspace record viewers that stay in workspace scope without reviving tenant truth |
|
|
|
|
## State Transitions
|
|
|
|
| Trigger | From | To | Notes |
|
|
|---|---|---|---|
|
|
| Workspace switch | `tenant_scoped` | `tenant_scoped` or `tenantless_workspace` | Existing tenant survives only after compatibility re-check in target workspace. |
|
|
| Tenant select | `tenantless_workspace` | `tenant_scoped` | Explicit user action; requires entitlement and operability validation. |
|
|
| Tenant clear on workspace page | `tenant_scoped` | `tenantless_workspace` | Valid only on workspace-scoped pages. |
|
|
| Tenant clear on tenant-bound page | `tenant_scoped` | `tenantless_workspace` plus redirect | Redirect destination depends on page category and route family. |
|
|
| Remembered tenant invalidation | `tenant_scoped` or restore candidate | `tenantless_workspace` | Candidate is cleared and cannot stay visible. |
|
|
| Workspace invalidation | any | `missing_workspace` or `invalid_workspace` | Recovery goes to chooser or not-found depending on entry path. |
|
|
|
|
## Display Semantics
|
|
|
|
| Resolved state | Workspace label | Tenant label | Action affordances |
|
|
|---|---|---|---|
|
|
| `tenant_scoped` | Active workspace name | Active tenant name | Switch workspace, Select tenant, Clear tenant context |
|
|
| `tenantless_workspace` | Active workspace name | `No tenant selected` | Switch workspace, Select tenant |
|
|
| `missing_workspace` / `invalid_workspace` | `Choose workspace` or recovery label | hidden or disabled | Choose workspace, optional safe return |
|
|
| `invalid_tenant` / `inaccessible_tenant` / `incompatible_tenant` | Active workspace name if valid | no stale tenant name shown | Recovery action only; no stale tenant truth | |