diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index fa5222d36a..b299c2b2e5 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -440,6 +440,9 @@ The summary remains additive to the inventory-first workflow rather than a replacement for it: Proxmox-heavy operators still land on protected items by default, but the first visible framing now reflects the whole multi-platform recovery system instead of a single backup table metaphor. +That top recovery frame must rely on solid elevated operator panels and border +hierarchy rather than decorative gradients so the page reads like a monitoring +workspace instead of a marketing-style dashboard shell. That same summary contract now also includes platform- and item-coverage framing through `frontend-modern/src/utils/recoverySummaryPresentation.ts`, so the first visible recovery overview explicitly shows which protected item @@ -595,11 +598,14 @@ same active recovery view. That same recovery filter contract now depends on the canonical recovery index carrying a normalized `itemType` instead of forcing each UI surface to re-derive protected item classes from raw provider-native `subjectType` values. -That same recovery product surface keeps the activity timeline available even -when point-history loading fails: `frontend-modern/src/components/Recovery/Recovery.tsx` -must continue to render `RecoveryActivitySection` and the point-history error -card side by side so operators can still inspect backup cadence and active -filter context while recovery-point transport is degraded. +That same recovery product surface keeps the primary workspace visually ahead +of secondary analytics: once the summary frame ends, +`frontend-modern/src/components/Recovery/Recovery.tsx` must lead into the +route-backed recovery workspace and its protected-item or recovery-event table +surface before it drops into `RecoveryActivitySection`, so operators reach the +main control surface ahead of trend telemetry. The activity timeline remains +required even when point-history loading fails, but it is secondary to the +workspace rather than a competing top-of-page peer. That shared unified-resource dependency now also includes policy-governed resource metadata on the frontend decode path: storage and recovery surfaces that route through `frontend-modern/src/hooks/useUnifiedResources.ts` must diff --git a/frontend-modern/src/components/Recovery/Recovery.tsx b/frontend-modern/src/components/Recovery/Recovery.tsx index 4c8ba9a7fa..96cd3b7582 100644 --- a/frontend-modern/src/components/Recovery/Recovery.tsx +++ b/frontend-modern/src/components/Recovery/Recovery.tsx @@ -484,57 +484,37 @@ const Recovery: Component = () => { timeRange={summaryRange} /> - { - setClusterFilter('all'); - setCurrentPage(1); - }} - clearFocusedRollup={() => setRollupId('')} - clearItemTypeFilter={() => { - setItemTypeFilter('all'); - setCurrentPage(1); - }} - clearNamespaceFilter={() => { - setNamespaceFilter('all'); - setCurrentPage(1); - }} - clearNodeFilter={() => { - setNodeFilter('all'); - setCurrentPage(1); - }} - clearSelectedDate={() => { - setSelectedDateKey(null); - setCurrentPage(1); - }} - hasFocusedRollup={() => rollupId().trim().length > 0} - isMobile={isMobile()} - loading={() => recoverySeries.response.loading} - overallRollupsSummary={overallRollupsSummary} - selectedDateKey={selectedDateKey} - selectedDateLabel={selectedDateLabel} - selectedHistoryItemLabel={selectedHistoryItemLabel} - setChartRangeDays={(range) => { - setChartRangeDays(range); - setSelectedDateKey(null); - setCurrentPage(1); - }} - timeline={timeline} - toggleSelectedDate={(key) => { - setWorkspaceView('events'); - setSelectedDateKey((previous) => (previous === key ? null : key)); - setCurrentPage(1); - }} - /> - -
- -
+
+ +
+
+
+
+ Recovery Workspace +
+
+ Run recovery operations from one shared surface: protected inventory first, + event drill-in second, with the same item-first filters and route state. +
+
+
+ + {filteredRollups().length} protected items + + + {recoveryPoints.meta().total} recovery events + + 0}> + + Focused drill-in + + +
+
setWorkspaceView(value as RecoveryWorkspaceView)} @@ -561,7 +541,7 @@ const Recovery: Component = () => { ]} />
-
+
{ verificationFilter={verificationFilter} /> + + { + setClusterFilter('all'); + setCurrentPage(1); + }} + clearFocusedRollup={() => setRollupId('')} + clearItemTypeFilter={() => { + setItemTypeFilter('all'); + setCurrentPage(1); + }} + clearNamespaceFilter={() => { + setNamespaceFilter('all'); + setCurrentPage(1); + }} + clearNodeFilter={() => { + setNodeFilter('all'); + setCurrentPage(1); + }} + clearSelectedDate={() => { + setSelectedDateKey(null); + setCurrentPage(1); + }} + hasFocusedRollup={() => rollupId().trim().length > 0} + isMobile={isMobile()} + loading={() => recoverySeries.response.loading} + overallRollupsSummary={overallRollupsSummary} + selectedDateKey={selectedDateKey} + selectedDateLabel={selectedDateLabel} + selectedHistoryItemLabel={selectedHistoryItemLabel} + setChartRangeDays={(range) => { + setChartRangeDays(range); + setSelectedDateKey(null); + setCurrentPage(1); + }} + timeline={timeline} + toggleSelectedDate={(key) => { + setWorkspaceView('events'); + setSelectedDateKey((previous) => (previous === key ? null : key)); + setCurrentPage(1); + }} + />
); diff --git a/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx b/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx index 2d3d65a48a..be2ac722b6 100644 --- a/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryActivitySection.tsx @@ -81,41 +81,46 @@ interface RecoveryActivitySectionProps { const rangeOptions: Array<7 | 30 | 90 | 365> = [7, 30, 90, 365]; export const RecoveryActivitySection: Component = (props) => ( - +
-
- +
+ {props.overallRollupsSummary().total} protected 0}> - + {props.overallRollupsSummary().total - props.overallRollupsSummary().stale} healthy 0}> - {props.overallRollupsSummary().stale} stale + + {props.overallRollupsSummary().stale} stale + 0}> - + {props.overallRollupsSummary().neverSucceeded} never succeeded
-
+
Recovery Activity
-
+
Daily recovery points across the selected history window.
-
- Focused Item +
+ Focused Item {props.selectedHistoryItemLabel()} @@ -242,8 +247,8 @@ export const RecoveryActivitySection: Component =
-
-
+
+
Selected range @@ -290,7 +295,7 @@ export const RecoveryActivitySection: Component =
-
+
{(step) => { const value = Math.round((props.timeline().axisMax * (4 - step)) / 4); @@ -299,7 +304,7 @@ export const RecoveryActivitySection: Component =
-
+
-
+ +
-
+
Protected Items
-
+
Unified protection inventory across workloads, datasets, and other protected items, with platform mix carried as supporting context.
-
- +
+ {props.filteredRollups().length} of {props.rollups().length} items shown 0}> - + {props.rollupsSummary().stale} stale 0}> - + {props.rollupsSummary().neverSucceeded} never succeeded @@ -189,7 +193,7 @@ export const RecoveryProtectedInventorySection: Component<
-
+
0}> -
+
- + {( [ ['item', getRecoveryArtifactColumnLabel('item', 'Item')], @@ -386,7 +390,7 @@ export const RecoveryProtectedInventorySection: Component< return ( props.onSelectRollup(rollup.rollupId)} > { expect(screen.getByText('Protected Footprint')).toBeInTheDocument(); expect(screen.getByText('Freshness')).toBeInTheDocument(); expect(screen.getByText('Recent History')).toBeInTheDocument(); - expect(screen.getByText('Attention Queue')).toBeInTheDocument(); + expect(screen.getAllByText('Attention Queue').length).toBeGreaterThan(0); expect(screen.getByText('Recovery Points')).toBeInTheDocument(); expect(screen.getAllByText('Item Types').length).toBeGreaterThan(0); expect(screen.getByText('Primary Item')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Recovery/RecoverySummary.tsx b/frontend-modern/src/components/Recovery/RecoverySummary.tsx index 0257dcd22c..e7bd77ac41 100644 --- a/frontend-modern/src/components/Recovery/RecoverySummary.tsx +++ b/frontend-modern/src/components/Recovery/RecoverySummary.tsx @@ -49,25 +49,41 @@ export const RecoverySummary: Component = (props) => { return ( - -
-
-
- {summary().total} protected - - {itemCoverage().itemTypeCount} item type - {itemCoverage().itemTypeCount === 1 ? '' : 's'} + +
+
+
+ + {summary().total} protected - - {platformCoverage().platformCount} platform - {platformCoverage().platformCount === 1 ? '' : 's'} + + + {itemCoverage().itemTypeCount} item type + {itemCoverage().itemTypeCount === 1 ? '' : 's'} + + + + + {platformCoverage().platformCount} platform + {platformCoverage().platformCount === 1 ? '' : 's'} + + + + {healthyCount()} healthy - {healthyCount()} healthy 0}> - {attentionCount()} attention + + {attentionCount()} attention + 0}> - {postureSummary().running} running + + {postureSummary().running} running +
@@ -87,27 +103,44 @@ export const RecoverySummary: Component = (props) => {
-
-
-
- Recovery Posture +
+
+
+
+
+ Recovery Posture +
+
+ Recovery readiness across the current protected estate, with unhealthy + coverage and stale protection surfaced first. +
+
+ 0}> +
+ Attention Queue + + {attentionCount()} + +
+
-
+ +
Protected
-
{summary().total}
+
{summary().total}
Healthy
-
{healthyCount()}
+
{healthyCount()}
Attention
-
{attentionCount()}
+
{attentionCount()}
-
+
{(segment) => ( @@ -121,10 +154,10 @@ export const RecoverySummary: Component = (props) => {
-
+
{(segment) => ( -
+
{segment.label} @@ -134,49 +167,132 @@ export const RecoverySummary: Component = (props) => { )}
+
+
+
+ Freshness +
+
+ + {(bucket) => ( +
+ {bucket.label} +
+
0 ? 8 : 0)}%` }} + /> +
+ {bucket.count} +
+ )} + +
+
+ +
+
+ Attention Queue +
+ 0} + fallback={
No active recovery risks.
} + > +
+ + {(item) => ( +
+
+
+ + + {item.label} + +
+ {item.count} +
+
{item.detail}
+
+ )} +
+
+
+
+
-
-
- Protected Footprint -
-
-
-
Item Types
-
- {itemCoverage().itemTypeCount} +
+
+
+ Protected Footprint +
+
+
+
Item Types
+
+ {itemCoverage().itemTypeCount} +
+
+
+
Primary Item
+
+ {itemCoverage().primaryItemLabel ?? 'n/a'} +
+
+
+
Platforms
+
+ {platformCoverage().platformCount} +
+
+
+
Primary Platform
+
+ {platformCoverage().primaryPlatformLabel ?? 'n/a'} +
-
-
Primary Item
-
- {itemCoverage().primaryItemLabel ?? 'n/a'} -
-
-
-
Platforms
-
- {platformCoverage().platformCount} -
-
-
-
Primary Platform
-
- {platformCoverage().primaryPlatformLabel ?? 'n/a'} -
-
-
-
- 0}> +
+ 0}> +
+
+ Item Types +
+
+ + {(item) => ( +
+ + {item.label} + + {item.count} + {item.percent}% +
+ )} +
+
+
+
- Item Types + Platform Mix
+ 0}> +
+ {platformCoverage().multiPlatformCount} protected item + {platformCoverage().multiPlatformCount === 1 ? '' : 's'} span multiple + platforms. +
+
- + {(item) => ( -
+
{item.label} @@ -187,148 +303,80 @@ export const RecoverySummary: Component = (props) => {
- -
-
- Platform Mix -
- 0}> -
- {platformCoverage().multiPlatformCount} protected item - {platformCoverage().multiPlatformCount === 1 ? '' : 's'} span multiple - platforms. +
+
+ +
+
+
+
+ Recent History
- -
- - {(item) => ( -
- - {item.label} - - {item.count} - {item.percent}% +
+
+
+ Recovery Points +
+
+ {activity().totalEvents} +
+
+
+
Avg / Day
+
+ {activity().averagePerDay.toFixed(1)} +
+
+
+
Days Active
+
+ {activity().activeDays} +
+
+
+
+
+ + + {props.seriesFailed?.() ? 'Trend data unavailable' : 'No recovery activity yet'} +
+ } + > +
+ + {(bar) => ( +
+
)}
-
-
-
-
-
- Freshness -
-
- - {(bucket) => ( -
- {bucket.label} -
-
0 ? 8 : 0)}%` }} - /> -
- {bucket.count} -
- )} - -
-
- -
-
-
-
- Recent History +
+ + Peak: {activity().busiestLabel ?? 'n/a'} ({activity().busiestCount}) + + + Latest: {activity().latestLabel ?? 'n/a'} ({activity().latestCount}) +
-
-
-
- Recovery Points -
-
- {activity().totalEvents} -
-
-
-
Avg / Day
-
- {activity().averagePerDay.toFixed(1)} -
-
-
-
Days Active
-
- {activity().activeDays} -
-
-
-
-
- - - {props.seriesFailed?.() ? 'Trend data unavailable' : 'No recovery activity yet'} -
- } - > -
- - {(bar) => ( -
-
-
- )} - -
- -
- - Peak: {activity().busiestLabel ?? 'n/a'} ({activity().busiestCount}) - - - Latest: {activity().latestLabel ?? 'n/a'} ({activity().latestCount}) - -
- -
-
- - 0}> -
-
- Attention Queue -
- - {(item) => ( -
- - {item.label} - {item.count} -
- )} -
+ +
- +