Initial commit: C2LInspecz Automated Diagnostic Tool

This commit is contained in:
Zin Bo Thit
2026-02-09 13:38:21 +06:30
commit 59ddfdd094
6 changed files with 4765 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
/.agent

50
C2LInspecz.md Normal file
View 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

File diff suppressed because it is too large Load Diff

11
Cargo.toml Normal file
View 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
View 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
View 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.");
}
}
}