## 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
9.4 KiB
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_snapshotmust 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
- If the current concern is absent, the row is excluded from current-set state.
- If the current concern exists and no active row exists, the derived state is
not_reviewed. - If an active row exists and the current fingerprint does not match, the derived state is
changed_since_review. - 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_atwhen 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
TenantTriageReviewrows. - One tenant has many
TenantTriageReviewrows 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.