Skip to main content

nap_core/server/
doctor.rs

1// SPDX-FileCopyrightText: 2026 Digital Creations
2// SPDX-License-Identifier: MIT
3//! NAP doctor diagnostics and repair
4//!
5//! Performs comprehensive diagnostics on the NAP SDK environment and
6//! provides repair capabilities for common issues.
7
8use 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
17/// NAP doctor for diagnostics and repair
18pub struct NapDoctor {
19    nap_home: std::path::PathBuf,
20}
21
22impl NapDoctor {
23    /// Create a new NAP doctor
24    pub fn new(nap_home: &Path) -> Self {
25        Self {
26            nap_home: nap_home.to_path_buf(),
27        }
28    }
29
30    /// Run comprehensive diagnostics
31    pub async fn diagnose(&self) -> Result<DoctorReport> {
32        info!("Running NAP doctor diagnostics");
33
34        let mut checks = vec![];
35
36        // Check NAP configuration
37        checks.push(self.check_nap_configuration());
38
39        // Check Lore installation
40        checks.push(self.check_lore_installation());
41
42        // Check Lore configuration
43        checks.push(self.check_lore_configuration());
44
45        // Check Lore certificates
46        checks.push(self.check_lore_certificates());
47
48        // Check Lore server status
49        checks.push(self.check_lore_server_status().await);
50
51        // Check store directories
52        checks.push(self.check_store_directories());
53
54        // Check provider connectivity
55        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    /// Repair detected issues
67    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    /// Check NAP configuration
99    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    /// Check Lore installation
124    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    /// Check Lore configuration
155    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    /// Check Lore certificates
178    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    /// Check Lore server status
203    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    /// Check store directories
236    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    /// Check provider connectivity
260    async fn check_provider_connectivity(&self) -> CheckResult {
261        let name = "Provider Connectivity";
262
263        // For now, just check if we can reach localhost
264        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    /// Repair a specific check
292    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/// Result of a diagnostic check
358#[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/// Severity level of a check
367#[derive(Debug, Clone, PartialEq)]
368pub enum CheckSeverity {
369    Info,
370    Warning,
371    Error,
372}
373
374/// Complete diagnostic report
375#[derive(Debug, Clone)]
376pub struct DoctorReport {
377    pub checks: Vec<CheckResult>,
378    pub nap_home: std::path::PathBuf,
379}
380
381impl DoctorReport {
382    /// Get overall health status
383    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    /// Get a summary message
403    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/// Overall health status
411#[derive(Debug, Clone, PartialEq)]
412pub enum HealthStatus {
413    Healthy,
414    Degraded,
415    Unhealthy,
416}
417
418/// Result of a repair operation
419#[derive(Debug, Clone)]
420pub struct RepairResult {
421    pub check_name: String,
422    pub success: bool,
423    pub message: String,
424}
425
426/// Complete repair report
427#[derive(Debug, Clone)]
428pub struct RepairReport {
429    pub repairs: Vec<RepairResult>,
430}
431
432impl RepairReport {
433    /// Get number of successful repairs
434    pub fn successful_count(&self) -> usize {
435        self.repairs.iter().filter(|r| r.success).count()
436    }
437
438    /// Get number of failed repairs
439    pub fn failed_count(&self) -> usize {
440        self.repairs.iter().filter(|r| !r.success).count()
441    }
442
443    /// Get summary message
444    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}