winbrew_models\reporting/
report.rs1use serde::{Deserialize, Serialize, Serializer};
4use std::time::Duration;
5
6use super::diagnostics::DiagnosisResult;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum RecoveryIssueKind {
12 RecoveryTrailMissing,
14 IncompleteInstall,
16 Conflict,
18 DiskDrift,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum RecoveryActionGroup {
26 JournalReplay,
28 OrphanCleanup,
30 FileRestore,
32 Reinstall,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct RecoveryFinding {
39 pub error_code: String,
41 pub issue_kind: RecoveryIssueKind,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub action_group: Option<RecoveryActionGroup>,
46 pub description: String,
48 pub severity: super::diagnostics::DiagnosisSeverity,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub target_path: Option<String>,
53}
54
55impl RecoveryFinding {
56 pub fn from_diagnosis(diagnosis: &DiagnosisResult) -> Option<Self> {
58 let (issue_kind, action_group) = match diagnosis.error_code.as_str() {
59 "missing_install_directory" | "install_directory_not_a_directory" => (
60 RecoveryIssueKind::DiskDrift,
61 Some(RecoveryActionGroup::Reinstall),
62 ),
63 "install_directory_permission_denied" | "install_directory_unreadable" => (
64 RecoveryIssueKind::DiskDrift,
65 Some(RecoveryActionGroup::Reinstall),
66 ),
67 "missing_msi_file"
68 | "msi_file_not_a_file"
69 | "msi_file_unreadable"
70 | "msi_file_permission_denied"
71 | "msi_file_hash_mismatch"
72 | "msi_file_hash_unavailable" => (
73 RecoveryIssueKind::DiskDrift,
74 Some(RecoveryActionGroup::FileRestore),
75 ),
76 "missing_msi_inventory_snapshot"
77 | "msi_inventory_unreadable"
78 | "pkgdb_unreadable"
79 | "incomplete_package_journal"
80 | "unreadable_package_journal"
81 | "malformed_package_journal"
82 | "missing_journal_metadata" => (RecoveryIssueKind::RecoveryTrailMissing, None),
83 "orphan_install_directory" => (
84 RecoveryIssueKind::IncompleteInstall,
85 Some(RecoveryActionGroup::OrphanCleanup),
86 ),
87 "orphan_package_journal" => (
88 RecoveryIssueKind::IncompleteInstall,
89 Some(RecoveryActionGroup::JournalReplay),
90 ),
91 "stale_package_journal" | "trailing_package_journal" => (
92 RecoveryIssueKind::Conflict,
93 Some(RecoveryActionGroup::JournalReplay),
94 ),
95 _ => return None,
96 };
97
98 Some(Self {
99 error_code: diagnosis.error_code.clone(),
100 issue_kind,
101 action_group,
102 description: diagnosis.description.clone(),
103 severity: diagnosis.severity,
104 target_path: None,
105 })
106 }
107
108 pub fn with_target_path(mut self, target_path: impl Into<String>) -> Self {
110 self.target_path = Some(target_path.into());
111 self
112 }
113}
114
115#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
120pub struct HealthScanTimings {
121 #[serde(
123 rename = "database_connection_micros",
124 serialize_with = "serialize_duration_micros"
125 )]
126 pub database_connection: Duration,
127 #[serde(
129 rename = "installed_packages_micros",
130 serialize_with = "serialize_duration_micros"
131 )]
132 pub installed_packages: Duration,
133 #[serde(
135 rename = "package_scan_micros",
136 serialize_with = "serialize_duration_micros"
137 )]
138 pub package_scan: Duration,
139 #[serde(
141 rename = "msi_scan_micros",
142 serialize_with = "serialize_duration_micros"
143 )]
144 pub msi_scan: Duration,
145 #[serde(
147 rename = "orphan_scan_micros",
148 serialize_with = "serialize_duration_micros"
149 )]
150 pub orphan_scan: Duration,
151 #[serde(
153 rename = "journal_scan_micros",
154 serialize_with = "serialize_duration_micros"
155 )]
156 pub journal_scan: Duration,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
161pub struct HealthReport {
162 pub database_path: String,
164 pub database_exists: bool,
166 pub catalog_database_path: String,
168 pub catalog_database_exists: bool,
170 pub install_root_source: String,
172 pub install_root: String,
174 pub install_root_exists: bool,
176 pub packages_dir: String,
178 pub diagnostics: Vec<DiagnosisResult>,
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
182 pub recovery_findings: Vec<RecoveryFinding>,
183 pub scan_timings: HealthScanTimings,
185 #[serde(
187 rename = "scan_duration_micros",
188 serialize_with = "serialize_duration_micros"
189 )]
190 pub scan_duration: Duration,
191 pub error_count: usize,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct RuntimeReport {
198 pub sections: Vec<ReportSection>,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct ReportSection {
205 pub title: String,
207 pub entries: Vec<(String, String)>,
209}
210
211impl RuntimeReport {
212 pub fn new(sections: Vec<ReportSection>) -> Self {
214 Self { sections }
215 }
216}
217
218fn serialize_duration_micros<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
219where
220 S: Serializer,
221{
222 let micros = duration.as_micros().min(u64::MAX as u128) as u64;
223 serializer.serialize_u64(micros)
224}