Skip to main content

canic_backup/restore/apply/journal/reports/
mod.rs

1//! Module: restore::apply::journal::reports
2//!
3//! Responsibility: project restore apply journals into operator reports.
4//! Does not own: journal transitions, command rendering, or receipt validation.
5//! Boundary: returns compact read-only progress and attention summaries.
6
7use super::{
8    RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind,
9    RestoreApplyOperationKindCounts, RestoreApplyOperationState,
10};
11
12use serde::{Deserialize, Serialize};
13
14///
15/// RestoreApplyJournalReport
16///
17/// Internal operator report for one restore apply journal.
18/// Owned by restore apply journaling and exposed through restore report APIs.
19///
20
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22pub(in crate::restore) struct RestoreApplyJournalReport {
23    pub report_version: u16,
24    pub backup_id: String,
25    pub outcome: RestoreApplyReportOutcome,
26    pub attention_required: bool,
27    pub ready: bool,
28    pub complete: bool,
29    pub blocked_reasons: Vec<String>,
30    pub operation_count: usize,
31    pub operation_counts: RestoreApplyOperationKindCounts,
32    pub progress: RestoreApplyProgressSummary,
33    pub pending_summary: RestoreApplyPendingSummary,
34    pub pending_operations: usize,
35    pub ready_operations: usize,
36    pub blocked_operations: usize,
37    pub completed_operations: usize,
38    pub failed_operations: usize,
39    pub next_transition: Option<RestoreApplyReportOperation>,
40    pub pending: Vec<RestoreApplyReportOperation>,
41    pub failed: Vec<RestoreApplyReportOperation>,
42    pub blocked: Vec<RestoreApplyReportOperation>,
43}
44
45impl RestoreApplyJournalReport {
46    /// Build a compact operator report from a restore apply journal.
47    #[must_use]
48    pub(in crate::restore) fn from_journal(journal: &RestoreApplyJournal) -> Self {
49        let complete = journal.is_complete();
50        let outcome = RestoreApplyReportOutcome::from_journal(journal, complete);
51        let pending = report_operations_with_state(journal, RestoreApplyOperationState::Pending);
52        let failed = report_operations_with_state(journal, RestoreApplyOperationState::Failed);
53        let blocked = report_operations_with_state(journal, RestoreApplyOperationState::Blocked);
54
55        Self {
56            report_version: 1,
57            backup_id: journal.backup_id.clone(),
58            outcome: outcome.clone(),
59            attention_required: outcome.attention_required(),
60            ready: journal.ready,
61            complete,
62            blocked_reasons: journal.blocked_reasons.clone(),
63            operation_count: journal.operation_count,
64            operation_counts: journal.operation_kind_counts(),
65            progress: RestoreApplyProgressSummary::from_journal(journal),
66            pending_summary: RestoreApplyPendingSummary::from_journal(journal),
67            pending_operations: journal.pending_operations,
68            ready_operations: journal.ready_operations,
69            blocked_operations: journal.blocked_operations,
70            completed_operations: journal.completed_operations,
71            failed_operations: journal.failed_operations,
72            next_transition: journal
73                .next_transition_operation()
74                .map(RestoreApplyReportOperation::from_journal_operation),
75            pending,
76            failed,
77            blocked,
78        }
79    }
80}
81
82///
83/// RestoreApplyPendingSummary
84///
85/// Read-only summary of pending restore apply work.
86/// Owned by restore apply reporting and embedded in journal reports.
87///
88
89#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
90pub struct RestoreApplyPendingSummary {
91    pub pending_operations: usize,
92    pub pending_operation_available: bool,
93    pub pending_sequence: Option<usize>,
94    pub pending_operation: Option<RestoreApplyOperationKind>,
95    pub pending_updated_at: Option<String>,
96    pub pending_updated_at_known: bool,
97}
98
99impl RestoreApplyPendingSummary {
100    /// Build a compact pending-operation summary from a restore apply journal.
101    #[must_use]
102    pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
103        let pending = journal
104            .operations
105            .iter()
106            .filter(|operation| operation.state == RestoreApplyOperationState::Pending)
107            .min_by_key(|operation| operation.sequence);
108        let pending_updated_at = pending.and_then(|operation| operation.state_updated_at.clone());
109        let pending_updated_at_known = pending_updated_at
110            .as_deref()
111            .is_some_and(known_state_update_marker);
112
113        Self {
114            pending_operations: journal.pending_operations,
115            pending_operation_available: pending.is_some(),
116            pending_sequence: pending.map(|operation| operation.sequence),
117            pending_operation: pending.map(|operation| operation.operation.clone()),
118            pending_updated_at,
119            pending_updated_at_known,
120        }
121    }
122}
123
124fn known_state_update_marker(value: &str) -> bool {
125    !value.trim().is_empty() && value != "unknown"
126}
127
128///
129/// RestoreApplyProgressSummary
130///
131/// Read-only restore apply progress counters.
132/// Owned by restore apply reporting and embedded in journal reports.
133///
134
135#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
136pub struct RestoreApplyProgressSummary {
137    pub operation_count: usize,
138    pub completed_operations: usize,
139    pub remaining_operations: usize,
140    pub transitionable_operations: usize,
141    pub attention_operations: usize,
142    pub completion_basis_points: usize,
143}
144
145impl RestoreApplyProgressSummary {
146    /// Build a compact progress summary from restore apply journal counters.
147    #[must_use]
148    pub const fn from_journal(journal: &RestoreApplyJournal) -> Self {
149        let remaining_operations = journal
150            .operation_count
151            .saturating_sub(journal.completed_operations);
152        let transitionable_operations = journal.ready_operations + journal.pending_operations;
153        let attention_operations =
154            journal.pending_operations + journal.blocked_operations + journal.failed_operations;
155        let completion_basis_points =
156            completion_basis_points(journal.completed_operations, journal.operation_count);
157
158        Self {
159            operation_count: journal.operation_count,
160            completed_operations: journal.completed_operations,
161            remaining_operations,
162            transitionable_operations,
163            attention_operations,
164            completion_basis_points,
165        }
166    }
167}
168
169const fn completion_basis_points(completed_operations: usize, operation_count: usize) -> usize {
170    if operation_count == 0 {
171        return 0;
172    }
173
174    completed_operations.saturating_mul(10_000) / operation_count
175}
176
177///
178/// RestoreApplyReportOutcome
179///
180/// High-level operator outcome for one restore apply journal.
181/// Owned by restore apply reporting and used to classify attention needs.
182///
183
184#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
185#[serde(rename_all = "kebab-case")]
186pub enum RestoreApplyReportOutcome {
187    Empty,
188    Complete,
189    Failed,
190    Blocked,
191    Pending,
192    InProgress,
193}
194
195impl RestoreApplyReportOutcome {
196    const fn from_journal(journal: &RestoreApplyJournal, complete: bool) -> Self {
197        if journal.operation_count == 0 {
198            return Self::Empty;
199        }
200        if complete {
201            return Self::Complete;
202        }
203        if journal.failed_operations > 0 {
204            return Self::Failed;
205        }
206        if !journal.ready || journal.blocked_operations > 0 {
207            return Self::Blocked;
208        }
209        if journal.pending_operations > 0 {
210            return Self::Pending;
211        }
212        Self::InProgress
213    }
214
215    const fn attention_required(&self) -> bool {
216        matches!(self, Self::Failed | Self::Blocked | Self::Pending)
217    }
218}
219
220///
221/// RestoreApplyReportOperation
222///
223/// Compact report row for one restore apply journal operation.
224/// Owned by restore apply reporting and embedded in state-specific report lists.
225///
226
227#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
228pub struct RestoreApplyReportOperation {
229    pub sequence: usize,
230    pub operation: RestoreApplyOperationKind,
231    pub state: RestoreApplyOperationState,
232    pub member_order: usize,
233    pub role: String,
234    pub source_canister: String,
235    pub target_canister: String,
236    pub state_updated_at: Option<String>,
237    pub reasons: Vec<String>,
238}
239
240impl RestoreApplyReportOperation {
241    fn from_journal_operation(operation: &RestoreApplyJournalOperation) -> Self {
242        Self {
243            sequence: operation.sequence,
244            operation: operation.operation.clone(),
245            state: operation.state.clone(),
246            member_order: operation.member_order,
247            role: operation.role.clone(),
248            source_canister: operation.source_canister.clone(),
249            target_canister: operation.target_canister.clone(),
250            state_updated_at: operation.state_updated_at.clone(),
251            reasons: operation.blocking_reasons.clone(),
252        }
253    }
254}
255
256fn report_operations_with_state(
257    journal: &RestoreApplyJournal,
258    state: RestoreApplyOperationState,
259) -> Vec<RestoreApplyReportOperation> {
260    journal
261        .operations
262        .iter()
263        .filter(|operation| operation.state == state)
264        .map(RestoreApplyReportOperation::from_journal_operation)
265        .collect()
266}