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 install_dir = dirs::home_dir().unwrap().join(".local").join("bin");
304                let installer = LoreInstaller::new(&install_dir);
305                installer.install_all()?;
306                Ok(RepairResult {
307                    check_name: check.name.clone(),
308                    success: true,
309                    message: "Installed Lore CLI and server".to_string(),
310                })
311            }
312            "Lore Configuration" => {
313                generate_local_config(&self.nap_home)?;
314                Ok(RepairResult {
315                    check_name: check.name.clone(),
316                    success: true,
317                    message: "Generated Lore configuration".to_string(),
318                })
319            }
320            "Lore Certificates" => {
321                let cert_dir = self.nap_home.join("lore").join("certs");
322                generate_certificates(&cert_dir)?;
323                Ok(RepairResult {
324                    check_name: check.name.clone(),
325                    success: true,
326                    message: "Generated Lore certificates".to_string(),
327                })
328            }
329            "Lore Server Status" => {
330                let server_manager = ServerManager::new(&self.nap_home);
331                server_manager.ensure_running().await?;
332                Ok(RepairResult {
333                    check_name: check.name.clone(),
334                    success: true,
335                    message: "Started Lore server".to_string(),
336                })
337            }
338            "Store Directories" => {
339                let immutable_dir = self.nap_home.join("lore").join("store").join("immutable");
340                let mutable_dir = self.nap_home.join("lore").join("store").join("mutable");
341                std::fs::create_dir_all(&immutable_dir)?;
342                std::fs::create_dir_all(&mutable_dir)?;
343                Ok(RepairResult {
344                    check_name: check.name.clone(),
345                    success: true,
346                    message: "Created store directories".to_string(),
347                })
348            }
349            _ => Ok(RepairResult {
350                check_name: check.name.clone(),
351                success: false,
352                message: "No repair available for this check".to_string(),
353            }),
354        }
355    }
356}
357
358/// Result of a diagnostic check
359#[derive(Debug, Clone)]
360pub struct CheckResult {
361    pub name: String,
362    pub passed: bool,
363    pub message: String,
364    pub severity: CheckSeverity,
365}
366
367/// Severity level of a check
368#[derive(Debug, Clone, PartialEq)]
369pub enum CheckSeverity {
370    Info,
371    Warning,
372    Error,
373}
374
375/// Complete diagnostic report
376#[derive(Debug, Clone)]
377pub struct DoctorReport {
378    pub checks: Vec<CheckResult>,
379    pub nap_home: std::path::PathBuf,
380}
381
382impl DoctorReport {
383    /// Get overall health status
384    pub fn overall_health(&self) -> HealthStatus {
385        let has_errors = self
386            .checks
387            .iter()
388            .any(|c| c.severity == CheckSeverity::Error && !c.passed);
389        let has_warnings = self
390            .checks
391            .iter()
392            .any(|c| c.severity == CheckSeverity::Warning && !c.passed);
393
394        if has_errors {
395            HealthStatus::Unhealthy
396        } else if has_warnings {
397            HealthStatus::Degraded
398        } else {
399            HealthStatus::Healthy
400        }
401    }
402
403    /// Get a summary message
404    pub fn summary(&self) -> String {
405        let passed = self.checks.iter().filter(|c| c.passed).count();
406        let total = self.checks.len();
407        format!("{} / {} checks passed", passed, total)
408    }
409}
410
411/// Overall health status
412#[derive(Debug, Clone, PartialEq)]
413pub enum HealthStatus {
414    Healthy,
415    Degraded,
416    Unhealthy,
417}
418
419/// Result of a repair operation
420#[derive(Debug, Clone)]
421pub struct RepairResult {
422    pub check_name: String,
423    pub success: bool,
424    pub message: String,
425}
426
427/// Complete repair report
428#[derive(Debug, Clone)]
429pub struct RepairReport {
430    pub repairs: Vec<RepairResult>,
431}
432
433impl RepairReport {
434    /// Get number of successful repairs
435    pub fn successful_count(&self) -> usize {
436        self.repairs.iter().filter(|r| r.success).count()
437    }
438
439    /// Get number of failed repairs
440    pub fn failed_count(&self) -> usize {
441        self.repairs.iter().filter(|r| !r.success).count()
442    }
443
444    /// Get summary message
445    pub fn summary(&self) -> String {
446        format!(
447            "{} successful, {} failed",
448            self.successful_count(),
449            self.failed_count()
450        )
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457    use tempfile::TempDir;
458
459    #[test]
460    fn test_doctor_creation() {
461        let temp_dir = TempDir::new().unwrap();
462        let doctor = NapDoctor::new(temp_dir.path());
463        assert_eq!(doctor.nap_home, temp_dir.path());
464    }
465
466    #[test]
467    fn test_check_result() {
468        let check = CheckResult {
469            name: "Test Check".to_string(),
470            passed: true,
471            message: "Test passed".to_string(),
472            severity: CheckSeverity::Info,
473        };
474        assert!(check.passed);
475        assert_eq!(check.severity, CheckSeverity::Info);
476    }
477
478    #[test]
479    fn test_health_status() {
480        let report = DoctorReport {
481            checks: vec![
482                CheckResult {
483                    name: "Check 1".to_string(),
484                    passed: true,
485                    message: "OK".to_string(),
486                    severity: CheckSeverity::Info,
487                },
488                CheckResult {
489                    name: "Check 2".to_string(),
490                    passed: true,
491                    message: "OK".to_string(),
492                    severity: CheckSeverity::Info,
493                },
494            ],
495            nap_home: std::path::PathBuf::from("/tmp"),
496        };
497        assert_eq!(report.overall_health(), HealthStatus::Healthy);
498    }
499}