1use super::{
2 RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind,
3 RestoreApplyOperationKindCounts, RestoreApplyOperationState,
4};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct RestoreApplyJournalStatus {
13 pub status_version: u16,
14 pub backup_id: String,
15 pub ready: bool,
16 pub complete: bool,
17 pub blocked_reasons: Vec<String>,
18 pub operation_count: usize,
19 #[serde(default)]
20 pub operation_counts: RestoreApplyOperationKindCounts,
21 pub operation_counts_supplied: bool,
22 pub progress: RestoreApplyProgressSummary,
23 pub pending_summary: RestoreApplyPendingSummary,
24 pub pending_operations: usize,
25 pub ready_operations: usize,
26 pub blocked_operations: usize,
27 pub completed_operations: usize,
28 pub failed_operations: usize,
29 pub next_ready_sequence: Option<usize>,
30 pub next_ready_operation: Option<RestoreApplyOperationKind>,
31 pub next_transition_sequence: Option<usize>,
32 pub next_transition_state: Option<RestoreApplyOperationState>,
33 pub next_transition_operation: Option<RestoreApplyOperationKind>,
34 pub next_transition_updated_at: Option<String>,
35}
36
37impl RestoreApplyJournalStatus {
38 #[must_use]
40 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
41 let next_ready = journal.next_ready_operation();
42 let next_transition = journal.next_transition_operation();
43
44 Self {
45 status_version: 1,
46 backup_id: journal.backup_id.clone(),
47 ready: journal.ready,
48 complete: journal.is_complete(),
49 blocked_reasons: journal.blocked_reasons.clone(),
50 operation_count: journal.operation_count,
51 operation_counts: journal.operation_kind_counts(),
52 operation_counts_supplied: journal.operation_counts_supplied(),
53 progress: RestoreApplyProgressSummary::from_journal(journal),
54 pending_summary: RestoreApplyPendingSummary::from_journal(journal),
55 pending_operations: journal.pending_operations,
56 ready_operations: journal.ready_operations,
57 blocked_operations: journal.blocked_operations,
58 completed_operations: journal.completed_operations,
59 failed_operations: journal.failed_operations,
60 next_ready_sequence: next_ready.map(|operation| operation.sequence),
61 next_ready_operation: next_ready.map(|operation| operation.operation.clone()),
62 next_transition_sequence: next_transition.map(|operation| operation.sequence),
63 next_transition_state: next_transition.map(|operation| operation.state.clone()),
64 next_transition_operation: next_transition.map(|operation| operation.operation.clone()),
65 next_transition_updated_at: next_transition
66 .and_then(|operation| operation.state_updated_at.clone()),
67 }
68 }
69}
70
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
76#[expect(
77 clippy::struct_excessive_bools,
78 reason = "apply reports intentionally expose stable JSON flags for operators and CI"
79)]
80pub struct RestoreApplyJournalReport {
81 pub report_version: u16,
82 pub backup_id: String,
83 pub outcome: RestoreApplyReportOutcome,
84 pub attention_required: bool,
85 pub ready: bool,
86 pub complete: bool,
87 pub blocked_reasons: Vec<String>,
88 pub operation_count: usize,
89 #[serde(default)]
90 pub operation_counts: RestoreApplyOperationKindCounts,
91 pub operation_counts_supplied: bool,
92 pub progress: RestoreApplyProgressSummary,
93 pub pending_summary: RestoreApplyPendingSummary,
94 pub pending_operations: usize,
95 pub ready_operations: usize,
96 pub blocked_operations: usize,
97 pub completed_operations: usize,
98 pub failed_operations: usize,
99 pub next_transition: Option<RestoreApplyReportOperation>,
100 pub pending: Vec<RestoreApplyReportOperation>,
101 pub failed: Vec<RestoreApplyReportOperation>,
102 pub blocked: Vec<RestoreApplyReportOperation>,
103}
104
105impl RestoreApplyJournalReport {
106 #[must_use]
108 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
109 let complete = journal.is_complete();
110 let outcome = RestoreApplyReportOutcome::from_journal(journal, complete);
111 let pending = report_operations_with_state(journal, RestoreApplyOperationState::Pending);
112 let failed = report_operations_with_state(journal, RestoreApplyOperationState::Failed);
113 let blocked = report_operations_with_state(journal, RestoreApplyOperationState::Blocked);
114
115 Self {
116 report_version: 1,
117 backup_id: journal.backup_id.clone(),
118 outcome: outcome.clone(),
119 attention_required: outcome.attention_required(),
120 ready: journal.ready,
121 complete,
122 blocked_reasons: journal.blocked_reasons.clone(),
123 operation_count: journal.operation_count,
124 operation_counts: journal.operation_kind_counts(),
125 operation_counts_supplied: journal.operation_counts_supplied(),
126 progress: RestoreApplyProgressSummary::from_journal(journal),
127 pending_summary: RestoreApplyPendingSummary::from_journal(journal),
128 pending_operations: journal.pending_operations,
129 ready_operations: journal.ready_operations,
130 blocked_operations: journal.blocked_operations,
131 completed_operations: journal.completed_operations,
132 failed_operations: journal.failed_operations,
133 next_transition: journal
134 .next_transition_operation()
135 .map(RestoreApplyReportOperation::from_journal_operation),
136 pending,
137 failed,
138 blocked,
139 }
140 }
141}
142
143#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
148pub struct RestoreApplyPendingSummary {
149 pub pending_operations: usize,
150 pub pending_operation_available: bool,
151 pub pending_sequence: Option<usize>,
152 pub pending_operation: Option<RestoreApplyOperationKind>,
153 pub pending_updated_at: Option<String>,
154 pub pending_updated_at_known: bool,
155}
156
157impl RestoreApplyPendingSummary {
158 #[must_use]
160 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
161 let pending = journal
162 .operations
163 .iter()
164 .filter(|operation| operation.state == RestoreApplyOperationState::Pending)
165 .min_by_key(|operation| operation.sequence);
166 let pending_updated_at = pending.and_then(|operation| operation.state_updated_at.clone());
167 let pending_updated_at_known = pending_updated_at
168 .as_deref()
169 .is_some_and(known_state_update_marker);
170
171 Self {
172 pending_operations: journal.pending_operations,
173 pending_operation_available: pending.is_some(),
174 pending_sequence: pending.map(|operation| operation.sequence),
175 pending_operation: pending.map(|operation| operation.operation.clone()),
176 pending_updated_at,
177 pending_updated_at_known,
178 }
179 }
180}
181
182fn known_state_update_marker(value: &str) -> bool {
184 !value.trim().is_empty() && value != "unknown"
185}
186
187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
192pub struct RestoreApplyProgressSummary {
193 pub operation_count: usize,
194 pub completed_operations: usize,
195 pub remaining_operations: usize,
196 pub transitionable_operations: usize,
197 pub attention_operations: usize,
198 pub completion_basis_points: usize,
199}
200
201impl RestoreApplyProgressSummary {
202 #[must_use]
204 pub const fn from_journal(journal: &RestoreApplyJournal) -> Self {
205 let remaining_operations = journal
206 .operation_count
207 .saturating_sub(journal.completed_operations);
208 let transitionable_operations = journal.ready_operations + journal.pending_operations;
209 let attention_operations =
210 journal.pending_operations + journal.blocked_operations + journal.failed_operations;
211 let completion_basis_points =
212 completion_basis_points(journal.completed_operations, journal.operation_count);
213
214 Self {
215 operation_count: journal.operation_count,
216 completed_operations: journal.completed_operations,
217 remaining_operations,
218 transitionable_operations,
219 attention_operations,
220 completion_basis_points,
221 }
222 }
223}
224
225const fn completion_basis_points(completed_operations: usize, operation_count: usize) -> usize {
227 if operation_count == 0 {
228 return 0;
229 }
230
231 completed_operations.saturating_mul(10_000) / operation_count
232}
233
234#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
239#[serde(rename_all = "kebab-case")]
240pub enum RestoreApplyReportOutcome {
241 Empty,
242 Complete,
243 Failed,
244 Blocked,
245 Pending,
246 InProgress,
247}
248
249impl RestoreApplyReportOutcome {
250 const fn from_journal(journal: &RestoreApplyJournal, complete: bool) -> Self {
252 if journal.operation_count == 0 {
253 return Self::Empty;
254 }
255 if complete {
256 return Self::Complete;
257 }
258 if journal.failed_operations > 0 {
259 return Self::Failed;
260 }
261 if !journal.ready || journal.blocked_operations > 0 {
262 return Self::Blocked;
263 }
264 if journal.pending_operations > 0 {
265 return Self::Pending;
266 }
267 Self::InProgress
268 }
269
270 const fn attention_required(&self) -> bool {
272 matches!(self, Self::Failed | Self::Blocked | Self::Pending)
273 }
274}
275
276#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
281pub struct RestoreApplyReportOperation {
282 pub sequence: usize,
283 pub operation: RestoreApplyOperationKind,
284 pub state: RestoreApplyOperationState,
285 pub restore_group: u16,
286 pub phase_order: usize,
287 pub role: String,
288 pub source_canister: String,
289 pub target_canister: String,
290 pub state_updated_at: Option<String>,
291 pub reasons: Vec<String>,
292}
293
294impl RestoreApplyReportOperation {
295 fn from_journal_operation(operation: &RestoreApplyJournalOperation) -> Self {
297 Self {
298 sequence: operation.sequence,
299 operation: operation.operation.clone(),
300 state: operation.state.clone(),
301 restore_group: operation.restore_group,
302 phase_order: operation.phase_order,
303 role: operation.role.clone(),
304 source_canister: operation.source_canister.clone(),
305 target_canister: operation.target_canister.clone(),
306 state_updated_at: operation.state_updated_at.clone(),
307 reasons: operation.blocking_reasons.clone(),
308 }
309 }
310}
311
312fn report_operations_with_state(
314 journal: &RestoreApplyJournal,
315 state: RestoreApplyOperationState,
316) -> Vec<RestoreApplyReportOperation> {
317 journal
318 .operations
319 .iter()
320 .filter(|operation| operation.state == state)
321 .map(RestoreApplyReportOperation::from_journal_operation)
322 .collect()
323}
324
325#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
330pub struct RestoreApplyNextOperation {
331 pub response_version: u16,
332 pub backup_id: String,
333 pub ready: bool,
334 pub complete: bool,
335 pub operation_available: bool,
336 pub blocked_reasons: Vec<String>,
337 pub operation: Option<RestoreApplyJournalOperation>,
338}
339
340impl RestoreApplyNextOperation {
341 #[must_use]
343 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
344 let operation = journal.next_transition_operation().cloned();
345
346 Self {
347 response_version: 1,
348 backup_id: journal.backup_id.clone(),
349 ready: journal.ready,
350 complete: journal.is_complete(),
351 operation_available: operation.is_some(),
352 blocked_reasons: journal.blocked_reasons.clone(),
353 operation,
354 }
355 }
356}