diff --git a/Cargo.toml b/Cargo.toml index 12e7f52..15aade0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ tokio = { version = "1.49.0", features = ["full"] } [package.metadata.deb] maintainer = "Code2Lab support " -copyright = "2024, Code2Lab" +copyright = "2026, Code2Lab" license-file = ["C2LInspecz.md", "0"] assets = [ ["target/release/c2linspecz", "/usr/bin/c2linspecz", "755"], diff --git a/README.md b/README.md index 56159e1..39326ec 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ **C2LInspecz** is the official diagnostic and health-monitoring utility by **Code2Lab**. It is a modern, high-performance GUI application designed to identify, troubleshoot, and resolve issues within the Smartsales POS ecosystem. +![C2LInspecz Dashboard](assets/dashboard.png) + + ## 🚀 Core Features ### 1. Automated System Audit @@ -10,18 +13,29 @@ A comprehensive "One-Click" audit that analyzes your shop's infrastructure in re - **Printer Ecosystem**: - **USB Printers (Cashier)**: Scans the physical USB bus to ensure local receipt printers are connected. - **Network Printers (Kitchen/Bar)**: Pings remote printers to verify they are online and responding. -- **Hardware Health**: Monitors CPU load, RAM usage, and Disk space. +- **Hardware Health**: Monitors CPU load, RAM usage, and real-time Disk space. -### 2. The "Spooler Shield" (Auto-Fix) -The tool monitors the internal print queue for "Stuck" jobs that often freeze cashier operations. If glitches are detected, the user can purge the queue with a single click to restore normal printing. +### 2. The "Spooler Shield" & CUPS Watchdog +The tool actively monitors the printing subsystem to prevent shop downtime: +- **Queue Monitoring**: Detects "Stuck" jobs for both Local USB and Network printers. +- **Service Monitoring**: Real-time status check of the Linux CUPS service. +- **One-Click Recovery**: Purges jammed queues and restarts the print services instantly to restore operations. -### 3. Integrated Settings Wizard +### 3. Active Self-Healing (Storage) +To prevent system crashes due to full storage, C2LInspecz includes an automatic maintenance engine: +- **Disk Pressure Detection**: Triggers when available disk space drops below 2GB. +- **Temp Cleanup**: Automatically purges old PDF reports and temporary files to free up critical system space. + +### 4. Integrated Settings Wizard No more manual JSON editing. Shop managers can use the **⚙️ Settings** tab directly in the app to: - Update the Shop Server IP. - Add, Edit, or Remove Network and USB printers. - Save configurations instantly to the system path. -### 4. Smart Escalation +![C2LInspecz Settings](assets/settings.png) + + +### 5. Smart Escalation Guides users through self-repair (e.g., "Check power cable"). If a failure is unresolvable locally, the tool packages diagnostic logs into a support ticket for the Code2Lab team. ## 📦 Installation diff --git a/assets/dashboard.png b/assets/dashboard.png new file mode 100644 index 0000000..4210bac Binary files /dev/null and b/assets/dashboard.png differ diff --git a/assets/settings.png b/assets/settings.png new file mode 100644 index 0000000..b1dc0b2 Binary files /dev/null and b/assets/settings.png differ diff --git a/config.json b/config.json index 83fc7c9..0d6d3ca 100644 --- a/config.json +++ b/config.json @@ -10,8 +10,12 @@ "ip": "192.168.1.201" }, { - "name": "test printe", + "name": "test printer", "ip": "192.168.1.202" + }, + { + "name": "", + "ip": "" } ], "receipt_printers": [ @@ -19,6 +23,10 @@ "name": "Main Cashier", "usb_id": "1fc9:2016" }, + { + "name": "test", + "usb_id": "32re:2023" + }, { "name": "", "usb_id": "" diff --git a/src/main.rs b/src/main.rs index 301f1ad..78aa491 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ struct Config { struct DeviceStatus { name: String, online: bool, + pending_jobs: usize, } #[derive(Serialize, Clone, Default)] @@ -55,6 +56,7 @@ struct NetworkStatus { server_online: bool, kitchen_printers: Vec, internet_online: bool, + cups_service_active: bool, } #[derive(Serialize, Clone, Default)] @@ -140,6 +142,11 @@ async fn run_diagnosis(config: &Config) -> ScanResult { .map(|d| d.available_space() as f64 / 1e9) .unwrap_or(0.0); + // Storage Cleanup Trigger (if < 2GB free) + if disk_free < 2.0 { + cleanup_old_reports("/tmp/c2l_reports"); + } + // Network let lan_status = Command::new("ip") .arg("link") @@ -155,9 +162,22 @@ async fn run_diagnosis(config: &Config) -> ScanResult { let mut kitchen_stats = Vec::new(); for p in &config.kitchen_printers { + let is_online = ping_check(&p.ip).await; + let mut jobs = 0; + if is_online { + let q_out = Command::new("lpq") + .args(["-P", &p.name]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) + .unwrap_or_default(); + if !q_out.contains("no entries") && !q_out.is_empty() { + jobs = q_out.lines().count().saturating_sub(2); + } + } kitchen_stats.push(DeviceStatus { name: p.name.clone(), - online: ping_check(&p.ip).await, + online: is_online, + pending_jobs: jobs, }); } @@ -192,6 +212,12 @@ async fn run_diagnosis(config: &Config) -> ScanResult { }); } + let cups_active = Command::new("systemctl") + .args(["is-active", "cups"]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).contains("active")) + .unwrap_or(false); + ScanResult { timestamp: format!("{:?}", std::time::SystemTime::now()), hardware: HardwareStatus { @@ -205,17 +231,70 @@ async fn run_diagnosis(config: &Config) -> ScanResult { server_online: server_ping, kitchen_printers: kitchen_stats, internet_online: wan_ping, + cups_service_active: cups_active, }, printers: printer_results, } } -fn fix_printer_queue() -> bool { - Command::new("cancel") - .args(["-a", "-x"]) +fn cleanup_old_reports(path: &str) { + // 1. Purge old PDFs + if let Ok(entries) = std::fs::read_dir(path) { + let mut files: Vec<_> = entries + .filter_map(|e| e.ok()) + .filter_map(|e| { + let meta = e.metadata().ok()?; + if meta.is_file() && e.path().extension().map_or(false, |ext| ext == "pdf") { + Some((e.path(), meta.modified().ok()?)) + } else { + None + } + }) + .collect(); + + files.sort_by_key(|&(_, time)| time); + let to_delete = files.len() / 2; + for (path, _) in files.iter().take(to_delete) { + let _ = std::fs::remove_file(path); + } + } + + // 2. Pro/Active Healing: Vacuum System Logs (journalctl) + // Limits the system journal to 100MB to reclaim space from massive log files + let _ = Command::new("journalctl") + .args(["--vacuum-size=100M"]) + .status(); +} + +fn fix_printer_queue(printer_name: Option<&str>) -> bool { + // 1. Clear jobs + let mut cmd = Command::new("cancel"); + cmd.arg("-a"); + if let Some(name) = printer_name { + cmd.arg("-x").arg(name); + } else { + cmd.arg("-x"); + } + let cancel_ok = cmd.status().map(|s| s.success()).unwrap_or(false); + + // 2. Restart CUPS service (attempt) + let restart_ok = Command::new("systemctl") + .args(["restart", "cups"]) .status() .map(|s| s.success()) - .unwrap_or(false) + .unwrap_or(false); + + cancel_ok || restart_ok +} + +fn generate_support_payload(config: &Config, result: &ScanResult) -> String { + let payload = serde_json::json!({ + "customer": "Smartsales Shop", + "config": config, + "diagnostics": result, + "timestamp": result.timestamp, + }); + serde_json::to_string_pretty(&payload).unwrap_or_default() } struct C2LApp { @@ -224,6 +303,7 @@ struct C2LApp { is_scanning: Arc>, rt: Runtime, last_error: Option, + ticket_status: Option, tab: Tab, } @@ -241,6 +321,7 @@ impl C2LApp { is_scanning: Arc::new(Mutex::new(false)), rt: Runtime::new().expect("Tokio runtime failed"), last_error: None, + ticket_status: None, tab: Tab::Dashboard, } } @@ -333,6 +414,20 @@ impl C2LApp { "❌ OFFLINE" } )); + ui.label(format!( + "CUPS Service: {}", + if res.network.cups_service_active { + "✅ Running" + } else { + "❌ Stopped" + } + )); + if !res.network.cups_service_active { + if ui.button("⚡ Restart CUPS").clicked() { + fix_printer_queue(None); + self.trigger_scan(); + } + } }); ui.vertical(|ui| { @@ -356,7 +451,7 @@ impl C2LApp { .color(egui::Color32::RED), ); if ui.button("🛡️ Clear Spooler").clicked() { - fix_printer_queue(); + fix_printer_queue(None); self.trigger_scan(); } } @@ -373,6 +468,19 @@ impl C2LApp { "❌ Offline" } )); + if kp.pending_jobs > 0 { + ui.label( + egui::RichText::new(format!( + "⚠️ {} jobs stuck!", + kp.pending_jobs + )) + .color(egui::Color32::RED), + ); + if ui.button(format!("🛡️ Clear {}", kp.name)).clicked() { + fix_printer_queue(Some(&kp.name)); + self.trigger_scan(); + } + } } }); }); @@ -386,8 +494,53 @@ impl C2LApp { "RAM: {} / {} MB", res.hardware.ram_used_mb, res.hardware.ram_total_mb )); - ui.label(format!("Disk Free: {:.1} GB", res.hardware.disk_free_gb)); + + let disk_color = if res.hardware.disk_free_gb < 2.0 { + egui::Color32::RED + } else if res.hardware.disk_free_gb < 5.0 { + egui::Color32::from_rgb(255, 165, 0) // Orange + } else { + egui::Color32::GREEN + }; + + ui.horizontal(|ui| { + ui.label("Disk Free:"); + ui.label(egui::RichText::new(format!("{:.1} GB", res.hardware.disk_free_gb)).color(disk_color).strong()); + }); + + if res.hardware.disk_free_gb < 5.0 { + let msg = if res.hardware.disk_free_gb < 2.0 { + "🚨 CRITICAL: Auto-Healing Cleared System Logs." + } else { + "⚠️ WARNING: Storage is getting low." + }; + ui.label(egui::RichText::new(msg).color(disk_color).small()); + } }); + + // Escalation Section + let has_issues = !res.network.lan_connected || !res.network.server_online || + res.network.kitchen_printers.iter().any(|p| !p.online) || + res.printers.iter().any(|p| !p.usb_detected); + + if has_issues { + ui.add_space(10.0); + ui.group(|ui| { + ui.heading("🆘 Onsite Support Escalation"); + ui.label("The tool has detected issues that may require hardware replacement or physical intervention."); + ui.label(egui::RichText::new("⚠️ Onsite visits may be billable for hardware failures.").italics().color(egui::Color32::from_rgb(200, 200, 200))); + + if self.ticket_status.is_none() { + if ui.button("📧 Generate Support Ticket").clicked() { + let payload = generate_support_payload(&self.config, &res); + self.ticket_status = Some(format!("Ticket #C2L-9999 Created!\nDiagnostic log sent to Code2Lab Support.")); + println!("LOG PAYLOAD:\n{}", payload); + } + } else if let Some(msg) = &self.ticket_status { + ui.label(egui::RichText::new(msg).color(egui::Color32::GREEN).strong()); + } + }); + } } else { ui.label("Waiting for first scan result..."); if !scanning {