1use anyhow::Result;
9use std::path::Path;
10use tracing::{error, info};
11
12use super::{
13 cert::generate_certificates, config::generate_local_config, install::LoreInstaller,
14 manager::ServerManager, version::verify_lore_installation,
15};
16
17pub struct NapDoctor {
19 nap_home: std::path::PathBuf,
20}
21
22impl NapDoctor {
23 pub fn new(nap_home: &Path) -> Self {
25 Self {
26 nap_home: nap_home.to_path_buf(),
27 }
28 }
29
30 pub async fn diagnose(&self) -> Result<DoctorReport> {
32 info!("Running NAP doctor diagnostics");
33
34 let mut checks = vec![];
35
36 checks.push(self.check_nap_configuration());
38
39 checks.push(self.check_lore_installation());
41
42 checks.push(self.check_lore_configuration());
44
45 checks.push(self.check_lore_certificates());
47
48 checks.push(self.check_lore_server_status().await);
50
51 checks.push(self.check_store_directories());
53
54 checks.push(self.check_provider_connectivity().await);
56
57 let report = DoctorReport {
58 checks,
59 nap_home: self.nap_home.clone(),
60 };
61
62 info!("Diagnostics complete: {} checks", report.checks.len());
63 Ok(report)
64 }
65
66 pub async fn repair(&self, report: &DoctorReport) -> Result<RepairReport> {
68 info!("Starting repair based on diagnostics");
69
70 let mut repairs = vec![];
71
72 for check in &report.checks {
73 if !check.passed {
74 match self.repair_check(check).await {
75 Ok(repair_result) => {
76 repairs.push(repair_result);
77 }
78 Err(e) => {
79 error!("Failed to repair {}: {}", check.name, e);
80 repairs.push(RepairResult {
81 check_name: check.name.clone(),
82 success: false,
83 message: format!("Repair failed: {}", e),
84 });
85 }
86 }
87 }
88 }
89
90 let repair_report = RepairReport { repairs };
91 info!(
92 "Repair complete: {} repairs attempted",
93 repair_report.repairs.len()
94 );
95 Ok(repair_report)
96 }
97
98 fn check_nap_configuration(&self) -> CheckResult {
100 let name = "NAP Configuration";
101
102 let config_exists = self.nap_home.exists();
103 let config_dir = self.nap_home.join("lore").join("config");
104 let lore_config_exists = config_dir.exists();
105
106 if config_exists && lore_config_exists {
107 CheckResult {
108 name: name.to_string(),
109 passed: true,
110 message: "NAP configuration exists".to_string(),
111 severity: CheckSeverity::Info,
112 }
113 } else {
114 CheckResult {
115 name: name.to_string(),
116 passed: false,
117 message: "NAP configuration missing".to_string(),
118 severity: CheckSeverity::Error,
119 }
120 }
121 }
122
123 fn check_lore_installation(&self) -> CheckResult {
125 let name = "Lore Installation";
126
127 match verify_lore_installation() {
128 Ok(status) => {
129 if status.is_fully_compatible() {
130 CheckResult {
131 name: name.to_string(),
132 passed: true,
133 message: format!("Lore {} installed and compatible", status.pinned_version),
134 severity: CheckSeverity::Info,
135 }
136 } else {
137 CheckResult {
138 name: name.to_string(),
139 passed: false,
140 message: status.status_message(),
141 severity: CheckSeverity::Error,
142 }
143 }
144 }
145 Err(e) => CheckResult {
146 name: name.to_string(),
147 passed: false,
148 message: format!("Failed to check Lore installation: {}", e),
149 severity: CheckSeverity::Error,
150 },
151 }
152 }
153
154 fn check_lore_configuration(&self) -> CheckResult {
156 let name = "Lore Configuration";
157
158 let config_path = self.nap_home.join("lore").join("config").join("local.toml");
159
160 if config_path.exists() {
161 CheckResult {
162 name: name.to_string(),
163 passed: true,
164 message: "Lore configuration exists".to_string(),
165 severity: CheckSeverity::Info,
166 }
167 } else {
168 CheckResult {
169 name: name.to_string(),
170 passed: false,
171 message: "Lore configuration missing".to_string(),
172 severity: CheckSeverity::Warning,
173 }
174 }
175 }
176
177 fn check_lore_certificates(&self) -> CheckResult {
179 let name = "Lore Certificates";
180
181 let cert_dir = self.nap_home.join("lore").join("certs");
182 let cert_path = cert_dir.join("cert.pem");
183 let key_path = cert_dir.join("key.pem");
184
185 if cert_path.exists() && key_path.exists() {
186 CheckResult {
187 name: name.to_string(),
188 passed: true,
189 message: "Lore certificates exist".to_string(),
190 severity: CheckSeverity::Info,
191 }
192 } else {
193 CheckResult {
194 name: name.to_string(),
195 passed: false,
196 message: "Lore certificates missing".to_string(),
197 severity: CheckSeverity::Warning,
198 }
199 }
200 }
201
202 async fn check_lore_server_status(&self) -> CheckResult {
204 let name = "Lore Server Status";
205
206 let server_manager = ServerManager::new(&self.nap_home);
207
208 match server_manager.status().await {
209 Ok(status) => {
210 if status.is_ready() {
211 CheckResult {
212 name: name.to_string(),
213 passed: true,
214 message: format!("Lore server running on port {}", status.http_port),
215 severity: CheckSeverity::Info,
216 }
217 } else {
218 CheckResult {
219 name: name.to_string(),
220 passed: false,
221 message: status.status_message(),
222 severity: CheckSeverity::Warning,
223 }
224 }
225 }
226 Err(e) => CheckResult {
227 name: name.to_string(),
228 passed: false,
229 message: format!("Failed to check server status: {}", e),
230 severity: CheckSeverity::Error,
231 },
232 }
233 }
234
235 fn check_store_directories(&self) -> CheckResult {
237 let name = "Store Directories";
238
239 let immutable_dir = self.nap_home.join("lore").join("store").join("immutable");
240 let mutable_dir = self.nap_home.join("lore").join("store").join("mutable");
241
242 if immutable_dir.exists() && mutable_dir.exists() {
243 CheckResult {
244 name: name.to_string(),
245 passed: true,
246 message: "Store directories exist".to_string(),
247 severity: CheckSeverity::Info,
248 }
249 } else {
250 CheckResult {
251 name: name.to_string(),
252 passed: false,
253 message: "Store directories missing".to_string(),
254 severity: CheckSeverity::Warning,
255 }
256 }
257 }
258
259 async fn check_provider_connectivity(&self) -> CheckResult {
261 let name = "Provider Connectivity";
262
263 match reqwest::get("http://127.0.0.1:41339/health_check").await {
265 Ok(resp) => {
266 if resp.status().is_success() {
267 CheckResult {
268 name: name.to_string(),
269 passed: true,
270 message: "Provider connectivity OK".to_string(),
271 severity: CheckSeverity::Info,
272 }
273 } else {
274 CheckResult {
275 name: name.to_string(),
276 passed: false,
277 message: format!("Provider returned status: {}", resp.status()),
278 severity: CheckSeverity::Warning,
279 }
280 }
281 }
282 Err(e) => CheckResult {
283 name: name.to_string(),
284 passed: false,
285 message: format!("Provider connectivity failed: {}", e),
286 severity: CheckSeverity::Warning,
287 },
288 }
289 }
290
291 async fn repair_check(&self, check: &CheckResult) -> Result<RepairResult> {
293 match check.name.as_str() {
294 "NAP Configuration" => {
295 std::fs::create_dir_all(&self.nap_home)?;
296 Ok(RepairResult {
297 check_name: check.name.clone(),
298 success: true,
299 message: "Created NAP home directory".to_string(),
300 })
301 }
302 "Lore Installation" => {
303 let installer = LoreInstaller::new(None);
304 installer.install_all()?;
305 Ok(RepairResult {
306 check_name: check.name.clone(),
307 success: true,
308 message: "Installed Lore CLI and server".to_string(),
309 })
310 }
311 "Lore Configuration" => {
312 generate_local_config(&self.nap_home)?;
313 Ok(RepairResult {
314 check_name: check.name.clone(),
315 success: true,
316 message: "Generated Lore configuration".to_string(),
317 })
318 }
319 "Lore Certificates" => {
320 let cert_dir = self.nap_home.join("lore").join("certs");
321 generate_certificates(&cert_dir)?;
322 Ok(RepairResult {
323 check_name: check.name.clone(),
324 success: true,
325 message: "Generated Lore certificates".to_string(),
326 })
327 }
328 "Lore Server Status" => {
329 let server_manager = ServerManager::new(&self.nap_home);
330 server_manager.ensure_running().await?;
331 Ok(RepairResult {
332 check_name: check.name.clone(),
333 success: true,
334 message: "Started Lore server".to_string(),
335 })
336 }
337 "Store Directories" => {
338 let immutable_dir = self.nap_home.join("lore").join("store").join("immutable");
339 let mutable_dir = self.nap_home.join("lore").join("store").join("mutable");
340 std::fs::create_dir_all(&immutable_dir)?;
341 std::fs::create_dir_all(&mutable_dir)?;
342 Ok(RepairResult {
343 check_name: check.name.clone(),
344 success: true,
345 message: "Created store directories".to_string(),
346 })
347 }
348 _ => Ok(RepairResult {
349 check_name: check.name.clone(),
350 success: false,
351 message: "No repair available for this check".to_string(),
352 }),
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
359pub struct CheckResult {
360 pub name: String,
361 pub passed: bool,
362 pub message: String,
363 pub severity: CheckSeverity,
364}
365
366#[derive(Debug, Clone, PartialEq)]
368pub enum CheckSeverity {
369 Info,
370 Warning,
371 Error,
372}
373
374#[derive(Debug, Clone)]
376pub struct DoctorReport {
377 pub checks: Vec<CheckResult>,
378 pub nap_home: std::path::PathBuf,
379}
380
381impl DoctorReport {
382 pub fn overall_health(&self) -> HealthStatus {
384 let has_errors = self
385 .checks
386 .iter()
387 .any(|c| c.severity == CheckSeverity::Error && !c.passed);
388 let has_warnings = self
389 .checks
390 .iter()
391 .any(|c| c.severity == CheckSeverity::Warning && !c.passed);
392
393 if has_errors {
394 HealthStatus::Unhealthy
395 } else if has_warnings {
396 HealthStatus::Degraded
397 } else {
398 HealthStatus::Healthy
399 }
400 }
401
402 pub fn summary(&self) -> String {
404 let passed = self.checks.iter().filter(|c| c.passed).count();
405 let total = self.checks.len();
406 format!("{} / {} checks passed", passed, total)
407 }
408}
409
410#[derive(Debug, Clone, PartialEq)]
412pub enum HealthStatus {
413 Healthy,
414 Degraded,
415 Unhealthy,
416}
417
418#[derive(Debug, Clone)]
420pub struct RepairResult {
421 pub check_name: String,
422 pub success: bool,
423 pub message: String,
424}
425
426#[derive(Debug, Clone)]
428pub struct RepairReport {
429 pub repairs: Vec<RepairResult>,
430}
431
432impl RepairReport {
433 pub fn successful_count(&self) -> usize {
435 self.repairs.iter().filter(|r| r.success).count()
436 }
437
438 pub fn failed_count(&self) -> usize {
440 self.repairs.iter().filter(|r| !r.success).count()
441 }
442
443 pub fn summary(&self) -> String {
445 format!(
446 "{} successful, {} failed",
447 self.successful_count(),
448 self.failed_count()
449 )
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use tempfile::TempDir;
457
458 #[test]
459 fn test_doctor_creation() {
460 let temp_dir = TempDir::new().unwrap();
461 let doctor = NapDoctor::new(temp_dir.path());
462 assert_eq!(doctor.nap_home, temp_dir.path());
463 }
464
465 #[test]
466 fn test_check_result() {
467 let check = CheckResult {
468 name: "Test Check".to_string(),
469 passed: true,
470 message: "Test passed".to_string(),
471 severity: CheckSeverity::Info,
472 };
473 assert!(check.passed);
474 assert_eq!(check.severity, CheckSeverity::Info);
475 }
476
477 #[test]
478 fn test_health_status() {
479 let report = DoctorReport {
480 checks: vec![
481 CheckResult {
482 name: "Check 1".to_string(),
483 passed: true,
484 message: "OK".to_string(),
485 severity: CheckSeverity::Info,
486 },
487 CheckResult {
488 name: "Check 2".to_string(),
489 passed: true,
490 message: "OK".to_string(),
491 severity: CheckSeverity::Info,
492 },
493 ],
494 nap_home: std::path::PathBuf::from("/tmp"),
495 };
496 assert_eq!(report.overall_health(), HealthStatus::Healthy);
497 }
498}