diff --git a/Cargo.lock b/Cargo.lock index 81a2669..72b5273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "C2LInspecz" -version = "0.1.0" -dependencies = [ - "eframe", - "serde", - "serde_json", - "sysinfo", - "tokio", -] - [[package]] name = "ab_glyph" version = "0.2.32" @@ -507,6 +496,17 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "c2linspecz" +version = "0.1.0" +dependencies = [ + "eframe", + "serde", + "serde_json", + "sysinfo", + "tokio", +] + [[package]] name = "calloop" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index 203212f..12e7f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [package] -name = "C2LInspecz" +name = "c2linspecz" version = "0.1.0" edition = "2024" +authors = ["Code2Lab Team "] +description = "Automated POS Diagnostic & Health Monitoring Tool" [dependencies] eframe = "0.33.3" @@ -9,3 +11,12 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" sysinfo = "0.38.0" tokio = { version = "1.49.0", features = ["full"] } + +[package.metadata.deb] +maintainer = "Code2Lab support " +copyright = "2024, Code2Lab" +license-file = ["C2LInspecz.md", "0"] +assets = [ + ["target/release/c2linspecz", "/usr/bin/c2linspecz", "755"], + ["config.json", "/etc/c2linspecz/config.json", "644"] +] diff --git a/README.md b/README.md index 002c9af..56159e1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ -# C2LInspecz +# C2LInspecz - Automated Smartsales System Health -Smartsales System Diagnosis tool \ No newline at end of file +**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. + +## 🚀 Core Features + +### 1. Automated System Audit +A comprehensive "One-Click" audit that analyzes your shop's infrastructure in real-time: +- **Network Integrity**: Verifies LAN connectivity, Internet access, and Shop Server reachability. +- **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. + +### 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. + +### 3. 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 +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 + +C2LInspecz is distributed as a native Debian package for seamless POS integration. + +1. Download the latest `.deb` package. +2. Install via terminal: + ```bash + sudo dpkg -i c2linspecz_0.1.0_amd64.deb + ``` +3. Launch **"C2LInspecz"** from your system's Applications menu. + +## 🛠 Developer Guide + +### Build Requirements +- Rust (Cargo) +- `dpkg-deb` tool + +### Packaging the Installer +To generate a new release installer: +```bash +cargo build --release +./scripts/build_deb.sh +``` +The final package will be ready at `target/c2linspecz_0.1.0_amd64.deb`. + +--- +© 2026 **Code2Lab Team** | [support@code2lab.com](mailto:support@code2lab.com) \ No newline at end of file diff --git a/config.json b/config.json index d730337..83fc7c9 100644 --- a/config.json +++ b/config.json @@ -8,12 +8,20 @@ { "name": "Bar Printer", "ip": "192.168.1.201" + }, + { + "name": "test printe", + "ip": "192.168.1.202" } ], "receipt_printers": [ { "name": "Main Cashier", "usb_id": "1fc9:2016" + }, + { + "name": "", + "usb_id": "" } ] } \ No newline at end of file diff --git a/scripts/build_deb.sh b/scripts/build_deb.sh new file mode 100755 index 0000000..39ed8d3 --- /dev/null +++ b/scripts/build_deb.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Manual .deb packaging script for C2LInspecz + +set -e + +PKG_NAME="c2linspecz" +VERSION="0.1.0" +ARCH="amd64" +BUILD_DIR="target/debian_pkg" + +echo "Building .deb package for $PKG_NAME v$VERSION..." + +# Clean up previous build +rm -rf $BUILD_DIR +mkdir -p $BUILD_DIR/DEBIAN +mkdir -p $BUILD_DIR/usr/bin +mkdir -p $BUILD_DIR/usr/share/applications +mkdir -p $BUILD_DIR/etc/c2linspecz + +# Copy binary (must be built with cargo build --release first) +if [ ! -f target/release/c2linspecz ]; then + echo "Error: target/release/c2linspecz not found. Please run 'cargo build --release' first." + exit 1 +fi +cp target/release/c2linspecz $BUILD_DIR/usr/bin/ + +# Copy config template +cp config.json $BUILD_DIR/etc/c2linspecz/ + +# Create desktop entry +cat < $BUILD_DIR/usr/share/applications/c2linspecz.desktop +[Desktop Entry] +Name=C2LInspecz +Comment=Automated POS Diagnostic & Health Monitoring Tool +Exec=/usr/bin/c2linspecz +Terminal=false +Type=Application +Categories=Utility;System; +Icon=utilities-system-monitor +EOF + +# Create control file +cat < $BUILD_DIR/DEBIAN/control +Package: $PKG_NAME +Version: $VERSION +Section: utils +Priority: optional +Architecture: $ARCH +Maintainer: Code2Lab Team +Description: Automated POS Diagnostic & Health Monitoring Tool + This tool is designed to identify, troubleshoot, and resolve issues within the POS ecosystem. +EOF + +# Build the package +dpkg-deb --build $BUILD_DIR "target/${PKG_NAME}_${VERSION}_${ARCH}.deb" + +echo "Package generated at target/${PKG_NAME}_${VERSION}_${ARCH}.deb" diff --git a/src/main.rs b/src/main.rs index 6c5ad82..301f1ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,37 +1,39 @@ +use eframe::egui; use serde::{Deserialize, Serialize}; use std::fs::File; -use std::io::{self, Read, Write}; +use std::io::{Read, Write}; use std::process::Command; +use std::sync::{Arc, Mutex}; use std::time::Duration; use sysinfo::{Disks, System}; -use tokio::time::sleep; +use tokio::runtime::Runtime; -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Default)] struct NetworkDevice { name: String, ip: String, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Default)] struct UsbDevice { name: String, usb_id: String, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Default)] struct Config { shop_server_ip: String, kitchen_printers: Vec, receipt_printers: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Clone, Default)] struct DeviceStatus { name: String, online: bool, } -#[derive(Serialize)] +#[derive(Serialize, Clone, Default)] struct ScanResult { timestamp: String, hardware: HardwareStatus, @@ -39,7 +41,7 @@ struct ScanResult { printers: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Clone, Default)] struct HardwareStatus { ram_used_mb: u64, ram_total_mb: u64, @@ -47,7 +49,7 @@ struct HardwareStatus { disk_free_gb: f64, } -#[derive(Serialize)] +#[derive(Serialize, Clone, Default)] struct NetworkStatus { lan_connected: bool, server_online: bool, @@ -55,7 +57,7 @@ struct NetworkStatus { internet_online: bool, } -#[derive(Serialize)] +#[derive(Serialize, Clone, Default)] struct PrinterStatus { name: String, usb_detected: bool, @@ -65,11 +67,39 @@ struct PrinterStatus { impl Config { fn load() -> Self { - let mut file = File::open("config.json").expect("config.json not found"); - let mut data = String::new(); - file.read_to_string(&mut data) - .expect("Unable to read config.json"); - serde_json::from_str(&data).expect("JSON format error") + let paths = [ + "config.json".to_string(), + "/etc/c2linspecz/config.json".to_string(), + std::env::current_exe() + .ok() + .and_then(|p| { + p.parent() + .map(|parent| parent.join("config.json").to_string_lossy().into_owned()) + }) + .unwrap_or_default(), + ]; + + for path in paths { + if let Ok(mut file) = File::open(&path) { + let mut data = String::new(); + if file.read_to_string(&mut data).is_ok() { + if let Ok(config) = serde_json::from_str(&data) { + return config; + } + } + } + } + Config::default() + } + + fn save(&self) -> bool { + let path = "config.json"; // Default to current dir for now, or fallback to standard + if let Ok(mut file) = File::create(path) { + if let Ok(data) = serde_json::to_string_pretty(self) { + return file.write_all(data.as_bytes()).is_ok(); + } + } + false } } @@ -98,16 +128,15 @@ async fn run_diagnosis(config: &Config) -> ScanResult { let total_ram = sys.total_memory() / 1024 / 1024; let used_ram = sys.used_memory() / 1024 / 1024; sys.refresh_cpu_usage(); - sleep(Duration::from_millis(200)).await; + tokio::time::sleep(Duration::from_millis(100)).await; sys.refresh_cpu_usage(); let cpu_load: f32 = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::() / sys.cpus().len() as f32; let disks = Disks::new_with_refreshed_list(); - let root_disk = disks + let disk_free = disks .iter() - .find(|d| d.mount_point().to_string_lossy() == "/"); - let disk_free = root_disk + .find(|d| d.mount_point().to_string_lossy() == "/") .map(|d| d.available_space() as f64 / 1e9) .unwrap_or(0.0); @@ -132,7 +161,7 @@ async fn run_diagnosis(config: &Config) -> ScanResult { }); } - // Receipt Printers (USB) + // Printers let usb_output = Command::new("lsusb") .output() .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) @@ -158,9 +187,8 @@ async fn run_diagnosis(config: &Config) -> ScanResult { printer_results.push(PrinterStatus { name: p.name.clone(), usb_detected: usb_output.contains(&p.usb_id), - cups_ready: lpstat_output.contains("enabled") - && (lpstat_output.contains("idle") || lpstat_output.contains("printing")), - pending_jobs: jobs_count, // CUPS queue is often shared or we check per-printer if needed, but for now we use total stuck jobs as the trigger + cups_ready: lpstat_output.contains("enabled"), + pending_jobs: jobs_count, }); } @@ -183,166 +211,264 @@ async fn run_diagnosis(config: &Config) -> ScanResult { } fn fix_printer_queue() -> bool { - println!("\n[SPOOLER SHIELD] Attempting to clear stuck jobs..."); - // 1. Purge all jobs - let purge = Command::new("cancel").args(["-a", "-x"]).status(); - match purge { - Ok(status) if status.success() => { - println!("SUCCESS: All pending print jobs have been purged."); - // 2. Restart cups if possible (might need sudo, but we'll try) - // Command::new("systemctl").args(["restart", "cups"]).status().ok(); - true + Command::new("cancel") + .args(["-a", "-x"]) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +struct C2LApp { + config: Config, + result: Arc>>, + is_scanning: Arc>, + rt: Runtime, + last_error: Option, + tab: Tab, +} + +#[derive(PartialEq)] +enum Tab { + Dashboard, + Settings, +} + +impl C2LApp { + fn new(_cc: &eframe::CreationContext<'_>) -> Self { + C2LApp { + config: Config::load(), + result: Arc::new(Mutex::new(None)), + is_scanning: Arc::new(Mutex::new(false)), + rt: Runtime::new().expect("Tokio runtime failed"), + last_error: None, + tab: Tab::Dashboard, } - _ => { - println!("ERROR: Failed to clear queue automatically. Please check printer power."); - false + } + + fn trigger_scan(&self) { + let config = self.config.clone(); + let result_store = self.result.clone(); + let scanning_status = self.is_scanning.clone(); + + self.rt.spawn(async move { + *scanning_status.lock().unwrap() = true; + let fresh_result = run_diagnosis(&config).await; + *result_store.lock().unwrap() = Some(fresh_result); + *scanning_status.lock().unwrap() = false; + }); + } +} + +impl eframe::App for C2LApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.horizontal(|ui| { + ui.selectable_value(&mut self.tab, Tab::Dashboard, "📊 Dashboard"); + ui.selectable_value(&mut self.tab, Tab::Settings, "⚙️ Settings"); + }); + ui.separator(); + + match self.tab { + Tab::Dashboard => self.ui_dashboard(ui), + Tab::Settings => self.ui_settings(ui), + } + }); + + // Auto-refresh scan result + ctx.request_repaint_after(Duration::from_millis(500)); + } +} + +impl C2LApp { + fn ui_dashboard(&mut self, ui: &mut egui::Ui) { + ui.heading("C2LInspecz - Automated Smartsales System Health"); + + let scanning = *self.is_scanning.lock().unwrap(); + if ui + .add_enabled(!scanning, egui::Button::new("🔍 Run Full Audit")) + .clicked() + { + self.trigger_scan(); + } + + if scanning { + ui.add( + egui::ProgressBar::new(0.5) + .animate(true) + .text("Performing System Audit..."), + ); + } + + ui.separator(); + + let result_opt = self.result.lock().unwrap().clone(); + if let Some(res) = result_opt { + ui.group(|ui| { + ui.label(format!("Last Scan: {}", res.timestamp)); + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.set_min_width(200.0); + ui.heading("🌐 Network"); + ui.label(format!( + "LAN: {}", + if res.network.lan_connected { + "✅ OK" + } else { + "❌ Unplugged" + } + )); + ui.label(format!( + "WAN: {}", + if res.network.internet_online { + "✅ Online" + } else { + "❌ Offline" + } + )); + ui.label(format!( + "Server: {}", + if res.network.server_online { + "✅ REACHABLE" + } else { + "❌ OFFLINE" + } + )); + }); + + ui.vertical(|ui| { + ui.heading("🖨️ Printers"); + for p in &res.printers { + ui.label(format!( + "{}: {}", + p.name, + if p.usb_detected { + "✅ Found" + } else { + "❌ NOT DETECTED" + } + )); + if p.pending_jobs > 0 { + ui.label( + egui::RichText::new(format!( + "⚠️ {} jobs stuck!", + p.pending_jobs + )) + .color(egui::Color32::RED), + ); + if ui.button("🛡️ Clear Spooler").clicked() { + fix_printer_queue(); + self.trigger_scan(); + } + } + } + ui.add_space(5.0); + ui.label("Network Printers:"); + for kp in &res.network.kitchen_printers { + ui.label(format!( + "{}: {}", + kp.name, + if kp.online { + "✅ Online" + } else { + "❌ Offline" + } + )); + } + }); + }); + }); + + ui.separator(); + ui.collapsing("💻 Hardware Info", |ui| { + ui.label(format!("CPU Usage: {:.1}%", res.hardware.cpu_usage)); + ui.add(egui::ProgressBar::new(res.hardware.cpu_usage / 100.0)); + ui.label(format!( + "RAM: {} / {} MB", + res.hardware.ram_used_mb, res.hardware.ram_total_mb + )); + ui.label(format!("Disk Free: {:.1} GB", res.hardware.disk_free_gb)); + }); + } else { + ui.label("Waiting for first scan result..."); + if !scanning { + self.trigger_scan(); + } + } + } + + fn ui_settings(&mut self, ui: &mut egui::Ui) { + ui.heading("Shop Configuration"); + ui.label("Configure local IPs and device IDs here."); + + ui.add_space(10.0); + ui.label("Shop Server IP:"); + ui.text_edit_singleline(&mut self.config.shop_server_ip); + + ui.separator(); + ui.label("Network Printers (Kitchen/Bar):"); + let mut to_remove_kp = None; + for (i, p) in self.config.kitchen_printers.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.text_edit_singleline(&mut p.name).on_hover_text("Name"); + ui.text_edit_singleline(&mut p.ip) + .on_hover_text("IP Address"); + if ui.button("🗑").clicked() { + to_remove_kp = Some(i); + } + }); + } + if let Some(i) = to_remove_kp { + self.config.kitchen_printers.remove(i); + } + if ui.button("➕ Add Network Printer").clicked() { + self.config.kitchen_printers.push(NetworkDevice::default()); + } + + ui.separator(); + ui.label("USB Printers (Cashier):"); + let mut to_remove_rp = None; + for (i, p) in self.config.receipt_printers.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.text_edit_singleline(&mut p.name); + ui.text_edit_singleline(&mut p.usb_id) + .on_hover_text("USB ID (e.g. 1fc9:2016)"); + if ui.button("🗑").clicked() { + to_remove_rp = Some(i); + } + }); + } + if let Some(i) = to_remove_rp { + self.config.receipt_printers.remove(i); + } + if ui.button("➕ Add USB Printer").clicked() { + self.config.receipt_printers.push(UsbDevice::default()); + } + + ui.add_space(20.0); + if ui.button("💾 Save Configuration").clicked() { + if self.config.save() { + self.last_error = Some("Configuration saved successfully!".to_string()); + } else { + self.last_error = Some("Failed to save configuration.".to_string()); + } + } + + if let Some(msg) = &self.last_error { + ui.label(msg); } } } -fn send_support_ticket(config: &Config, result: &ScanResult) { - println!("\n[ONSITE ESCALATION]"); - println!( - "WARNING: This service may be billable if the issue is a hardware failure (cables, broken hardware)." - ); - print!("Do you wish to proceed with generating a Ticket? (y/N): "); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - if input.trim().to_lowercase() != "y" { - println!("Ticket cancelled."); - return; - } - - println!("Packaging logs..."); - let payload = serde_json::json!({ - "config": config, - "diagnostics": result, - "system_info": "Linux x86_64", - }); - - // In a real app, this would be an API call - println!("SUCCESS: Support Ticket #C2L-{:04} generated.", 9999); - println!("Log Payload: {}", serde_json::to_string(&payload).unwrap()); - println!("The Code2Lab team has been notified. Please keep your device online."); -} - -#[tokio::main] -async fn main() { - let config = Config::load(); - - println!("===================================================="); - println!(" C2LInspecz - Automated System Health Check"); - println!(" Status: INITIALIZING DIAGNOSTICS..."); - println!("===================================================="); - - // 1. Automatic Diagnosis - println!("> Running System Audit..."); - let mut result = run_diagnosis(&config).await; - - // Detailed Status Logs - println!( - " [1/3] Network: LAN: {}, WAN: {}", - if result.network.lan_connected { - "OK" - } else { - "FAIL (Unplugged)" - }, - if result.network.internet_online { - "OK" - } else { - "FAIL (No Internet)" - } - ); - - print!( - " [2/3] Communication: Shop Server: {}", - if result.network.server_online { - "OK" - } else { - "FAIL (Offline)" - } - ); - for p in &result.network.kitchen_printers { - print!(", {}: {}", p.name, if p.online { "OK" } else { "FAIL" }); - } - println!(); - - print!(" [3/3] Printers:"); - for p in &result.printers { - print!( - " [{}: USB: {}, Jobs: {}]", - p.name, - if p.usb_detected { "OK" } else { "FAIL" }, - p.pending_jobs - ); - } - println!(); - - // 2. Automatic Auto-Fix (Printer Spooler) - let any_stuck = result.printers.iter().any(|p| p.pending_jobs > 0); - if any_stuck { - println!("> Stuck jobs detected. Triggering Spooler Shield..."); - if fix_printer_queue() { - println!("> Auto-fix applied. Re-verifying systems..."); - let fresh_scan = run_diagnosis(&config).await; - result.printers = fresh_scan.printers; - } - } - - // 3. Evaluation - let mut critical_issues: Vec = Vec::new(); - - if !result.network.lan_connected { - critical_issues.push("HARDWARE: Ethernet cable is UNPLUGGED or DAMAGED.".to_string()); - } else if !result.network.server_online { - critical_issues.push("COMMUNICATION: Local Shop Server is UNREACHABLE.".to_string()); - } - - for p in &result.network.kitchen_printers { - if !p.online { - critical_issues.push(format!("NETWORK: Kitchen Printer '{}' is OFFLINE.", p.name)); - } - } - - for p in &result.printers { - if !p.usb_detected { - critical_issues.push(format!( - "HARDWARE: Receipt Printer '{}' is NOT DETECTED.", - p.name - )); - } else if p.pending_jobs > 0 { - critical_issues.push(format!( - "SPOOLER: Jobs remain STUCK for printer '{}'.", - p.name - )); - } - } - - // 4. Reporting - println!("\n[FINAL HEALTH REPORT]"); - if critical_issues.is_empty() { - println!("✅ SUCCESS: All systems are operational."); - println!("The tool has verified your hardware and cleared minor software glitches."); - } else { - println!("❌ FAILURES DETECTED:"); - for issue in &critical_issues { - println!(" - {}", issue); - } - - println!("\n----------------------------------------------------"); - println!("The tool could not resolve these physical or network issues."); - println!("Would you like to open a ticket for Onsite Support?"); - print!("\nOpen Support Ticket #C2L-9999? (y/N): "); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - if input.trim().to_lowercase() == "y" { - send_support_ticket(&config, &result); - } else { - println!("Process ended. Please contact your manager if issues persist."); - } - } +fn main() -> eframe::Result { + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([600.0, 500.0]) + .with_title("C2LInspecz"), + ..Default::default() + }; + eframe::run_native( + "c2linspecz", + native_options, + Box::new(|cc| Ok(Box::new(C2LApp::new(cc)))), + ) }