diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index de486ebde8..84c1ed17f3 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -448,6 +448,11 @@ first scroll window. `RecoverySummary.tsx` and the workspace shell in `Recovery.tsx` should compress posture, coverage, and mode framing into concise operator panels instead of stacking multiple dashboard-like slabs that push the main inventory or event table too far down the page. +That same summary shell should read as one dominant posture surface with a +supporting rail, not as several equal-weight dashboard cards. The governed +recovery summary should keep the left side focused on posture, attention, and +freshness, while the right side carries compact coverage and recent-history +reference detail instead of competing headline widgets. That same workspace shell should stay a compact control strip rather than a second summary card: the protected-items versus recovery-events switcher belongs close to the active table surface, and it should not restate table counts that diff --git a/frontend-modern/src/components/Recovery/Recovery.tsx b/frontend-modern/src/components/Recovery/Recovery.tsx index 2ec4a16ee2..85eb8e162c 100644 --- a/frontend-modern/src/components/Recovery/Recovery.tsx +++ b/frontend-modern/src/components/Recovery/Recovery.tsx @@ -490,16 +490,15 @@ const Recovery: Component = () => { tone="card" class="overflow-hidden border-border-subtle bg-surface" > -
-
-
+
+
+
Recovery Workspace
-
- Protected inventory first, event drill-in second, with the same item-first - filters and route state. -
+ + Inventory first, event drill-in second, with shared item-first filters. +
0}> @@ -508,7 +507,7 @@ const Recovery: Component = () => {
-
+
setWorkspaceView(value as RecoveryWorkspaceView)} diff --git a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx index 9a72f1166d..726be7deab 100644 --- a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx @@ -218,13 +218,12 @@ export const RecoveryProtectedInventorySection: Component< tone="card" class="overflow-hidden border-border-subtle bg-surface shadow-[0_10px_24px_rgba(2,6,23,0.1)]" > -
+
-
+
Protected inventory
-
- Item-first recovery inventory, with item class and latest-point posture kept visible - in the main grid instead of buried in drill-in. +
+ Item class and latest-point posture stay visible in the primary grid.
diff --git a/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx b/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx index 9cebb687b5..caa4b42b7f 100644 --- a/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx +++ b/frontend-modern/src/components/Recovery/RecoverySummary.test.tsx @@ -64,9 +64,10 @@ describe('RecoverySummary', () => { expect(screen.getByText('Primary Platform')).toBeInTheDocument(); expect(screen.getByText('Platform Mix')).toBeInTheDocument(); expect(screen.getByText('2 protected')).toBeInTheDocument(); - expect(screen.getByText('2 platforms')).toBeInTheDocument(); - expect(screen.getByText('2 item types')).toBeInTheDocument(); expect(screen.getByText('1 healthy')).toBeInTheDocument(); + expect( + screen.getByText((_, element) => element?.textContent === '2 item types across 2 platforms.'), + ).toBeInTheDocument(); expect(screen.getByText('Never succeeded')).toBeInTheDocument(); }); }); diff --git a/frontend-modern/src/components/Recovery/RecoverySummary.tsx b/frontend-modern/src/components/Recovery/RecoverySummary.tsx index 285cced37a..ac8c33aa29 100644 --- a/frontend-modern/src/components/Recovery/RecoverySummary.tsx +++ b/frontend-modern/src/components/Recovery/RecoverySummary.tsx @@ -46,42 +46,57 @@ export const RecoverySummary: Component = (props) => { const healthyCount = createMemo(() => postureSummary().healthy); const attentionCount = createMemo(() => postureSummary().attention); + const attentionSummary = createMemo(() => { + const parts: string[] = []; + if (postureSummary().failed > 0) { + parts.push(`${postureSummary().failed} failed`); + } + if (postureSummary().neverSucceeded > 0) { + parts.push(`${postureSummary().neverSucceeded} never succeeded`); + } + if (postureSummary().stale > 0) { + parts.push(`${postureSummary().stale} stale`); + } + if (postureSummary().warning > 0) { + parts.push(`${postureSummary().warning} warning`); + } + if (parts.length === 0 && postureSummary().running > 0) { + parts.push(`${postureSummary().running} running`); + } + return parts.slice(0, 3).join(' • '); + }); + const recentWindowLabel = createMemo(() => { + const activitySummary = activity(); + if (!activitySummary.startLabel || !activitySummary.endLabel) return null; + return `${activitySummary.startLabel} to ${activitySummary.endLabel}`; + }); return (
- + {summary().total} protected - - - {itemCoverage().itemTypeCount} item type - {itemCoverage().itemTypeCount === 1 ? '' : 's'} - - - - - {platformCoverage().platformCount} platform - {platformCoverage().platformCount === 1 ? '' : 's'} - - - + + {healthyCount()} healthy 0}> - + + {attentionCount()} attention 0}> - + + {postureSummary().running} running @@ -104,39 +119,54 @@ export const RecoverySummary: Component = (props) => {
-
-
+
+
Recovery Posture
- Recovery readiness across the current protected estate, with unhealthy - coverage surfaced before activity detail. + Recovery readiness across the current protected estate, with the attention + queue surfaced before timeline detail.
0}> -
+
Attention Queue - + {attentionCount()}
-
-
-
Protected
-
{summary().total}
-
-
+
+
Healthy
-
{healthyCount()}
+
+ {healthyCount()} +
+
Recent successful protection within the current window.
-
+
Attention
-
{attentionCount()}
+
+ {attentionCount()} +
+
+ {attentionSummary() || 'No active recovery risks.'} +
+
+
+
Protected
+
+ {summary().total} +
+
+ {itemCoverage().itemTypeCount} item type + {itemCoverage().itemTypeCount === 1 ? '' : 's'} across {platformCoverage().platformCount}{' '} + platform{platformCoverage().platformCount === 1 ? '' : 's'}. +
@@ -167,8 +197,9 @@ export const RecoverySummary: Component = (props) => { )}
-
-
+ +
+
Freshness
@@ -190,7 +221,7 @@ export const RecoverySummary: Component = (props) => {
-
+
Attention Queue
@@ -199,7 +230,7 @@ export const RecoverySummary: Component = (props) => { fallback={
No active recovery risks.
} >
- + {(item) => (
= (props) => {
-
+
Protected Footprint
-
-
-
Item Types
-
- {itemCoverage().itemTypeCount} +
+
+
+
Item Types
+
+ {itemCoverage().itemTypeCount} +
-
-
-
Primary Item
-
- {itemCoverage().primaryItemLabel ?? 'n/a'} +
+
Primary Item
+
+ {itemCoverage().primaryItemLabel ?? 'n/a'} +
-
-
-
Platforms
-
- {platformCoverage().platformCount} +
+
Platforms
+
+ {platformCoverage().platformCount} +
-
-
-
Primary Platform
-
- {platformCoverage().primaryPlatformLabel ?? 'n/a'} +
+
+ Primary Platform +
+
+ {platformCoverage().primaryPlatformLabel ?? 'n/a'} +
-
+
@@ -267,8 +302,10 @@ export const RecoverySummary: Component = (props) => {
{(item) => ( -
- +
+ {item.label} {item.count} @@ -293,8 +330,10 @@ export const RecoverySummary: Component = (props) => {
{(item) => ( -
- +
+ {item.label} {item.count} @@ -311,31 +350,49 @@ export const RecoverySummary: Component = (props) => {
Recent History
-
-
-
-
-
- Recovery Points -
-
- {activity().totalEvents} -
-
-
-
Avg / Day
-
- {activity().averagePerDay.toFixed(1)} -
-
-
-
Days Active
-
- {activity().activeDays} -
-
+
+ + {recentWindowLabel()} + +
+
+
+
+
Recovery Points
+
+ {activity().totalEvents} +
-
+
+
Avg / Day
+
+ {activity().averagePerDay.toFixed(1)} +
+
+
+
Days Active
+
+ {activity().activeDays} +
+
+
+
Peak Day
+
+ {activity().busiestLabel ?? 'n/a'} +
+
+
+
+ Latest Activity +
+
+ {activity().latestLabel ?? 'n/a'} +
+
+ +
+ +
= (props) => {
-
Peak Day
-
- {activity().busiestLabel ?? 'n/a'} +
+ Peak Throughput +
+
+ {activity().busiestCount}
- {activity().busiestCount} recovery point - {activity().busiestCount === 1 ? '' : 's'} + {activity().busiestLabel ?? 'n/a'}
-
Latest Activity
-
- {activity().latestLabel ?? 'n/a'} +
+ Latest Throughput +
+
+ {activity().latestCount}
- {activity().latestCount} recovery point - {activity().latestCount === 1 ? '' : 's'} + {activity().latestLabel ?? 'n/a'}
diff --git a/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx b/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx index 9298adbbe3..75129daf88 100644 --- a/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx +++ b/frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx @@ -316,8 +316,11 @@ describe('Recovery', () => { it('surfaces item-first recovery coverage in the unified summary', async () => { render(() => ); - expect(await screen.findByText('2 item types')).toBeInTheDocument(); - expect(await screen.findByText('2 platforms')).toBeInTheDocument(); + expect( + await screen.findByText( + (_, element) => element?.textContent === '2 item types across 2 platforms.', + ), + ).toBeInTheDocument(); expect(screen.getByText('Primary Item')).toBeInTheDocument(); expect(screen.getByText('Primary Platform')).toBeInTheDocument(); expect(screen.getByText('Platform Mix')).toBeInTheDocument();