## Summary - add tenant triage review-state persistence, fingerprinting, resolver logic, service layer, and migration for current affected-set tracking - surface review-state and affected-set progress across tenant registry, tenant dashboard arrival continuity, and workspace overview - extend RBAC, audit/badge support, specs, and test coverage for portfolio triage review-state workflows - suppress expected hidden-page background transport failures in the global unhandled rejection logger while keeping visible-page failures logged ## Validation - targeted Pest coverage added for tenant registry, workspace overview, arrival context, RBAC authorization, badges, fingerprinting, resolver behavior, and logger asset behavior - code formatted with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Notes - full suite was not re-run in this final step - branch includes the spec artifacts under `specs/189-portfolio-triage-review-state/` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #220
161 lines
9.4 KiB
Markdown
161 lines
9.4 KiB
Markdown
# Data Model: Portfolio Triage Review State and Operator Progress
|
|
|
|
## Overview
|
|
|
|
This feature adds one persisted operator-progress entity and a small set of derived read models. Current backup-health and recovery-evidence posture remain authoritative and are not duplicated in storage.
|
|
|
|
## Existing Source Truths
|
|
|
|
### Current concern truth
|
|
|
|
**Type**: Existing derived posture state
|
|
**Sources**: `TenantBackupHealthResolver`, `TenantBackupHealthAssessment`, `RestoreSafetyResolver`, `WorkspaceOverviewBuilder`, `TenantResource`, `PortfolioArrivalContextResolver`
|
|
|
|
| Concern Family | Stable Inputs | Notes |
|
|
|---------------|---------------|-------|
|
|
| `backup_health` | posture state, stable reason code, schedule-follow-up family, freshness family | Current source of truth for backup triage remains existing tenant backup-health assessment |
|
|
| `recovery_evidence` | posture state, stable reason code, restore-evidence concern family | Current source of truth for recovery triage remains existing restore-safety and recovery-evidence assessment |
|
|
|
|
### Existing arrival context
|
|
|
|
**Type**: Existing request-scoped continuity contract
|
|
**Source**: `PortfolioArrivalContext`, `PortfolioArrivalContextToken`, `PortfolioArrivalContextResolver`
|
|
|
|
This feature consumes the existing concern-family focus on `/admin/t/{tenant}` to know which triage-review state should be rendered or mutated from the tenant dashboard.
|
|
|
|
## New Persisted Entity
|
|
|
|
### TenantTriageReview
|
|
|
|
**Table**: `tenant_triage_reviews`
|
|
**Type**: Workspace-shared persisted operator-progress record
|
|
**Lifecycle**: Active while it is the current manual review record for one workspace, tenant, and concern family; becomes inactive when superseded or explicitly resolved
|
|
|
|
| Field | Type | Validation / Notes |
|
|
|------|------|--------------------|
|
|
| `id` | bigint | Primary key |
|
|
| `workspace_id` | foreign key | Required; must reference the active workspace scope |
|
|
| `tenant_id` | foreign key | Required; must reference the tenant inside the same workspace |
|
|
| `concern_family` | string | Required; allowlisted to `backup_health` or `recovery_evidence` in V1 |
|
|
| `current_state` | string | Required; persisted values limited to `reviewed` or `follow_up_needed` |
|
|
| `reviewed_at` | timestamp | Required for active rows; when the manual state was recorded |
|
|
| `reviewed_by_user_id` | foreign key nullable | Optional actor reference for workspace-shared progress visibility |
|
|
| `review_fingerprint` | string | Required; deterministic fingerprint of the current material concern situation at review time |
|
|
| `review_snapshot` | jsonb | Required; bounded diagnostic snapshot with stable concern state, reason, and small supporting keys |
|
|
| `last_seen_matching_at` | timestamp nullable | Optional lightweight diagnostic field; initialized on write and may be refreshed opportunistically by future maintenance, but render-time correctness must not depend on it |
|
|
| `resolved_at` | timestamp nullable | Null for the current active row; set when a row is superseded or explicitly marked inactive |
|
|
| `created_at` | timestamp | Laravel default |
|
|
| `updated_at` | timestamp | Laravel default |
|
|
|
|
### Constraints and Indexes
|
|
|
|
| Constraint | Purpose |
|
|
|-----------|---------|
|
|
| Foreign keys on `workspace_id`, `tenant_id`, `reviewed_by_user_id` | Preserve workspace and tenant ownership plus reviewer linkage |
|
|
| Partial unique index on (`workspace_id`, `tenant_id`, `concern_family`) where `resolved_at IS NULL` | Ensures at most one active review record per workspace, tenant, and concern family |
|
|
| Lookup index on (`workspace_id`, `concern_family`, `resolved_at`, `tenant_id`) | Supports batch loading for workspace and registry current-set resolution |
|
|
| Check constraint or enum cast for `current_state` | Limits persisted manual states to `reviewed` or `follow_up_needed` |
|
|
|
|
## New Derived Read Models
|
|
|
|
### Derived review state
|
|
|
|
**Type**: Request-scoped resolved state
|
|
**Source**: `TenantTriageReviewStateResolver`
|
|
|
|
| Derived State | Rule | Stored? |
|
|
|--------------|------|---------|
|
|
| `not_reviewed` | Current concern exists and no active review row exists for the same workspace, tenant, and concern family | No |
|
|
| `reviewed` | Current concern exists, active review row exists, `current_state = reviewed`, and fingerprint matches | No |
|
|
| `follow_up_needed` | Current concern exists, active review row exists, `current_state = follow_up_needed`, and fingerprint matches | No |
|
|
| `changed_since_review` | Current concern exists, active review row exists, and current fingerprint differs from the stored fingerprint | No |
|
|
| `inactive` / excluded | Current concern does not exist in the current affected set | No |
|
|
|
|
### Current-set progress summary
|
|
|
|
**Type**: Request-scoped aggregate summary
|
|
**Source**: `TenantTriageReviewStateResolver` batch output, consumed by workspace overview and registry surfaces
|
|
|
|
| Field | Type | Notes |
|
|
|------|------|-------|
|
|
| `concern_family` | string | `backup_health` or `recovery_evidence` |
|
|
| `affected_total` | integer | Count of currently visible affected tenants in the family-specific set |
|
|
| `reviewed_count` | integer | Count of current affected rows resolved to `reviewed` |
|
|
| `follow_up_needed_count` | integer | Count of current affected rows resolved to `follow_up_needed` |
|
|
| `changed_since_review_count` | integer | Count of current affected rows resolved to `changed_since_review` |
|
|
| `not_reviewed_count` | integer | Count of current affected rows resolved to `not_reviewed` |
|
|
|
|
### Resolved row payload
|
|
|
|
**Type**: Request-scoped row-level bundle
|
|
**Source**: `TenantTriageReviewStateResolver`
|
|
|
|
| Field | Type | Notes |
|
|
|------|------|-------|
|
|
| `tenant_id` | integer | Tenant identifier for the current row |
|
|
| `concern_family` | string | Family the resolved review state refers to |
|
|
| `current_concern_present` | boolean | False rows are excluded from current-set progress |
|
|
| `current_fingerprint` | string | Deterministic fingerprint of current concern truth |
|
|
| `derived_state` | string | `not_reviewed`, `reviewed`, `follow_up_needed`, or `changed_since_review` |
|
|
| `reviewed_at` | timestamp or null | From active review row when present |
|
|
| `reviewed_by_user_id` | integer or null | From active review row when present |
|
|
| `review_snapshot` | array or null | Bounded snapshot for optional secondary display |
|
|
|
|
## Validation Rules
|
|
|
|
### Concern-family rules
|
|
|
|
| Concern Family | Allowed Current States | Fingerprint Inputs |
|
|
|---------------|------------------------|--------------------|
|
|
| `backup_health` | `reviewed`, `follow_up_needed` | Stable backup posture, stable reason code, schedule-follow-up family, freshness family |
|
|
| `recovery_evidence` | `reviewed`, `follow_up_needed` | Stable recovery posture, stable reason code, restore-evidence concern family |
|
|
|
|
### Snapshot rules
|
|
|
|
- `review_snapshot` must remain lightweight and bounded.
|
|
- Allowed snapshot keys may include `concern_family`, `concern_state`, `reason_code`, `severity_key`, `supporting_key`, and small label-safe metadata.
|
|
- Snapshot data must not contain comments, evidence payloads, free-text notes, rendered HTML, or volatile timestamps that would destabilize equality.
|
|
|
|
### Fingerprint rules
|
|
|
|
- Fingerprints must be deterministic across repeated reads of the same material concern situation.
|
|
- Fingerprints must ignore translated copy, badge labels, rendered descriptions, and volatile time values.
|
|
- Fingerprints must change when material concern family, stable state, or stable reason keys change.
|
|
|
|
## Lifecycle and State Transitions
|
|
|
|
### Manual mutation transitions
|
|
|
|
| Event | Existing Active Row | Result |
|
|
|------|---------------------|--------|
|
|
| `Mark reviewed` | none | Insert new active row with `current_state = reviewed` |
|
|
| `Mark reviewed` | active row exists | Set prior row `resolved_at`, then insert new active `reviewed` row |
|
|
| `Mark follow_up_needed` | none | Insert new active row with `current_state = follow_up_needed` |
|
|
| `Mark follow_up_needed` | active row exists | Set prior row `resolved_at`, then insert new active `follow_up_needed` row |
|
|
|
|
### Derived-state precedence
|
|
|
|
1. If the current concern is absent, the row is excluded from current-set state.
|
|
2. If the current concern exists and no active row exists, the derived state is `not_reviewed`.
|
|
3. If an active row exists and the current fingerprint does not match, the derived state is `changed_since_review`.
|
|
4. If an active row exists and fingerprints match, the derived state follows the stored manual state.
|
|
|
|
### Inactivity handling
|
|
|
|
- Superseded writes always resolve the previous active row.
|
|
- UI correctness does not depend on immediately writing `resolved_at` when a concern naturally disappears from the current affected set; current-set exclusion is derived from current concern truth.
|
|
- If later cleanup or maintenance chooses to mark concern-gone rows as resolved, that is an implementation detail and not required for V1 correctness.
|
|
|
|
## Relationships
|
|
|
|
- One workspace has many `TenantTriageReview` rows.
|
|
- One tenant has many `TenantTriageReview` rows across concern families and historical supersessions.
|
|
- One user may review many rows through `reviewed_by_user_id`.
|
|
- One current concern family on one tenant resolves to zero or one active row.
|
|
|
|
## Rendering Rules
|
|
|
|
- Posture truth remains primary and is displayed independently of review state.
|
|
- Registry and overview counts include only current affected rows, never calm or resolved rows.
|
|
- Mixed-family registry views must label which concern family the displayed review state refers to.
|
|
- Tenant dashboard review-state actions render only when portfolio-arrival context provides a valid concern-family focus. |