Initial commit: C2LInspecz Automated Diagnostic Tool
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/.agent
|
||||
50
C2LInspecz.md
Normal file
50
C2LInspecz.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# **PRD Summary: C2LInspecz Diagnostic Tool**
|
||||
|
||||
## **1\. Project Overview**
|
||||
|
||||
**C2LInspecz** is the official diagnostic and health-monitoring utility by **Code2Lab**. It is a lightweight, high-performance Rust binary designed to identify, troubleshoot, and resolve issues within the POS ecosystem (Cashier Station, Local Server, Networking, and Peripherals).
|
||||
|
||||
## **2\. Core Philosophy: "Resolve Before You Spend"**
|
||||
|
||||
C2LInspecz acts as a financial and operational buffer. Its primary goal is to guide users through free self-repairs, only permitting a **billable onsite support ticket** once internal diagnostics have been exhausted.
|
||||
|
||||
## **3\. Technical Implementation (Rust Stack)**
|
||||
|
||||
* **Architecture:** Statically linked Rust binary (zero-dependency deployment).
|
||||
* **Execution:** Async scanning via `tokio` for near-instant results.
|
||||
* **UI:** `egui` (Immediate-mode GUI) for a responsive, low-latency interface on all hardware types.
|
||||
|
||||
## **4\. Key Functional Modules**
|
||||
|
||||
### **A. System Integrity Scan**
|
||||
|
||||
* **Health Check:** Monitors Local Server disk I/O, RAM pressure, and database service availability.
|
||||
* **Network Audit:** Validates internal LAN routes (Cashier $\\leftrightarrow$ Server) and external WAN connectivity to Code2Lab APIs.
|
||||
|
||||
### **B. The "Spooler Shield" (Printer Fix)**
|
||||
|
||||
* **Detection:** Identifies "Stuck" print jobs that freeze the cashier UI.
|
||||
* **Automated Fix:** A Rust-driven system command to force-stop the spooler, wipe the PRINTERS cache, and re-initialize the hardware.
|
||||
|
||||
### **C. The Billable Escalation Gate**
|
||||
|
||||
* **The "Onsite" Lock:** The ticket request button remains disabled until a full system scan is performed.
|
||||
* **Monetary Consent:** A mandatory modal informs the user of the service fee associated with onsite calls.
|
||||
* **Log Packaging:** Upon user confirmation, C2LInspecz bundles all system logs and scan results into a JSON payload for the Code2Lab Dev Team.
|
||||
|
||||
|
||||
## **5\. User Workflow**
|
||||
|
||||
| Stage | Action | Result |
|
||||
| :---- | :---- | :---- |
|
||||
| **1\. Inspect** | User clicks "Run C2LInspecz Scan" | System identifies a network blockage to the Kitchen Printer. |
|
||||
| **2\. Self-Fix** | User follows the visual "Guide" | User checks the LAN cable as instructed by the UI. |
|
||||
| **3\. Utility** | User triggers "Clear Print Queue" | Tool purges stuck jobs; printer resumes. **(Cost Saved)** |
|
||||
| **4\. Escalate** | User clicks "Request Onsite" | UI warns of fees \-\> User agrees \-\>Ticket \# generated. |
|
||||
|
||||
## **6\. Success Metrics**
|
||||
|
||||
* **Deflection Rate:** Percentage of users who resolve issues without opening a ticket.
|
||||
* **Technician Readiness:** Developers receive 100% accurate data, reducing onsite repair time by half.
|
||||
* **Uptime:** Minimal POS downtime due to the "Spooler Shield" automated fixes.
|
||||
|
||||
4335
Cargo.lock
generated
Normal file
4335
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "C2LInspecz"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.33.3"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
sysinfo = "0.38.0"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
19
config.json
Normal file
19
config.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"shop_server_ip": "192.168.1.10",
|
||||
"kitchen_printers": [
|
||||
{
|
||||
"name": "Kitchen #1",
|
||||
"ip": "192.168.1.200"
|
||||
},
|
||||
{
|
||||
"name": "Bar Printer",
|
||||
"ip": "192.168.1.201"
|
||||
}
|
||||
],
|
||||
"receipt_printers": [
|
||||
{
|
||||
"name": "Main Cashier",
|
||||
"usb_id": "1fc9:2016"
|
||||
}
|
||||
]
|
||||
}
|
||||
348
src/main.rs
Normal file
348
src/main.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use sysinfo::{Disks, System};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
struct NetworkDevice {
|
||||
name: String,
|
||||
ip: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
struct UsbDevice {
|
||||
name: String,
|
||||
usb_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
struct Config {
|
||||
shop_server_ip: String,
|
||||
kitchen_printers: Vec<NetworkDevice>,
|
||||
receipt_printers: Vec<UsbDevice>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DeviceStatus {
|
||||
name: String,
|
||||
online: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ScanResult {
|
||||
timestamp: String,
|
||||
hardware: HardwareStatus,
|
||||
network: NetworkStatus,
|
||||
printers: Vec<PrinterStatus>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HardwareStatus {
|
||||
ram_used_mb: u64,
|
||||
ram_total_mb: u64,
|
||||
cpu_usage: f32,
|
||||
disk_free_gb: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NetworkStatus {
|
||||
lan_connected: bool,
|
||||
server_online: bool,
|
||||
kitchen_printers: Vec<DeviceStatus>,
|
||||
internet_online: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PrinterStatus {
|
||||
name: String,
|
||||
usb_detected: bool,
|
||||
cups_ready: bool,
|
||||
pending_jobs: usize,
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
async fn ping_check(target: &str) -> bool {
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("ping")
|
||||
.args(["-n", "1", "-w", "1000", target])
|
||||
.output()
|
||||
} else {
|
||||
Command::new("ping")
|
||||
.args(["-c", "1", "-W", "1", target])
|
||||
.output()
|
||||
};
|
||||
|
||||
match output {
|
||||
Ok(res) => res.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_diagnosis(config: &Config) -> ScanResult {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
// Hardware
|
||||
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;
|
||||
sys.refresh_cpu_usage();
|
||||
let cpu_load: f32 =
|
||||
sys.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
|
||||
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let root_disk = disks
|
||||
.iter()
|
||||
.find(|d| d.mount_point().to_string_lossy() == "/");
|
||||
let disk_free = root_disk
|
||||
.map(|d| d.available_space() as f64 / 1e9)
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Network
|
||||
let lan_status = Command::new("ip")
|
||||
.arg("link")
|
||||
.output()
|
||||
.map(|out| {
|
||||
let s = String::from_utf8_lossy(&out.stdout);
|
||||
(s.contains(": en") || s.contains(": eth")) && s.contains("LOWER_UP")
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let server_ping = ping_check(&config.shop_server_ip).await;
|
||||
let wan_ping = ping_check("8.8.8.8").await;
|
||||
|
||||
let mut kitchen_stats = Vec::new();
|
||||
for p in &config.kitchen_printers {
|
||||
kitchen_stats.push(DeviceStatus {
|
||||
name: p.name.clone(),
|
||||
online: ping_check(&p.ip).await,
|
||||
});
|
||||
}
|
||||
|
||||
// Receipt Printers (USB)
|
||||
let usb_output = Command::new("lsusb")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
.unwrap_or_default();
|
||||
let lpstat_output = Command::new("lpstat")
|
||||
.arg("-p")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
.unwrap_or_default();
|
||||
let lpq_output = Command::new("lpq")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let jobs_count = if lpq_output.contains("no entries") {
|
||||
0
|
||||
} else {
|
||||
lpq_output.lines().count().saturating_sub(2)
|
||||
};
|
||||
|
||||
let mut printer_results = Vec::new();
|
||||
for p in &config.receipt_printers {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
ScanResult {
|
||||
timestamp: format!("{:?}", std::time::SystemTime::now()),
|
||||
hardware: HardwareStatus {
|
||||
ram_used_mb: used_ram,
|
||||
ram_total_mb: total_ram,
|
||||
cpu_usage: cpu_load,
|
||||
disk_free_gb: disk_free,
|
||||
},
|
||||
network: NetworkStatus {
|
||||
lan_connected: lan_status,
|
||||
server_online: server_ping,
|
||||
kitchen_printers: kitchen_stats,
|
||||
internet_online: wan_ping,
|
||||
},
|
||||
printers: printer_results,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
_ => {
|
||||
println!("ERROR: Failed to clear queue automatically. Please check printer power.");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<String> = 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user