feat: Introduce comprehensive printer and system diagnostics, including CUPS service status, job queue monitoring, disk space alerts with auto-cleanup, and a support ticket generation workflow.
This commit is contained in:
@@ -14,7 +14,7 @@ tokio = { version = "1.49.0", features = ["full"] }
|
|||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
maintainer = "Code2Lab support <support@code2lab.com>"
|
maintainer = "Code2Lab support <support@code2lab.com>"
|
||||||
copyright = "2024, Code2Lab"
|
copyright = "2026, Code2Lab"
|
||||||
license-file = ["C2LInspecz.md", "0"]
|
license-file = ["C2LInspecz.md", "0"]
|
||||||
assets = [
|
assets = [
|
||||||
["target/release/c2linspecz", "/usr/bin/c2linspecz", "755"],
|
["target/release/c2linspecz", "/usr/bin/c2linspecz", "755"],
|
||||||
|
|||||||
24
README.md
24
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** 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
|
## 🚀 Core Features
|
||||||
|
|
||||||
### 1. Automated System Audit
|
### 1. Automated System Audit
|
||||||
@@ -10,18 +13,29 @@ A comprehensive "One-Click" audit that analyzes your shop's infrastructure in re
|
|||||||
- **Printer Ecosystem**:
|
- **Printer Ecosystem**:
|
||||||
- **USB Printers (Cashier)**: Scans the physical USB bus to ensure local receipt printers are connected.
|
- **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.
|
- **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)
|
### 2. The "Spooler Shield" & CUPS Watchdog
|
||||||
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.
|
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:
|
No more manual JSON editing. Shop managers can use the **⚙️ Settings** tab directly in the app to:
|
||||||
- Update the Shop Server IP.
|
- Update the Shop Server IP.
|
||||||
- Add, Edit, or Remove Network and USB printers.
|
- Add, Edit, or Remove Network and USB printers.
|
||||||
- Save configurations instantly to the system path.
|
- Save configurations instantly to the system path.
|
||||||
|
|
||||||
### 4. Smart Escalation
|

|
||||||
|
|
||||||
|
|
||||||
|
### 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.
|
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
|
## 📦 Installation
|
||||||
|
|||||||
BIN
assets/dashboard.png
Normal file
BIN
assets/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
assets/settings.png
Normal file
BIN
assets/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
10
config.json
10
config.json
@@ -10,8 +10,12 @@
|
|||||||
"ip": "192.168.1.201"
|
"ip": "192.168.1.201"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "test printe",
|
"name": "test printer",
|
||||||
"ip": "192.168.1.202"
|
"ip": "192.168.1.202"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"ip": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"receipt_printers": [
|
"receipt_printers": [
|
||||||
@@ -19,6 +23,10 @@
|
|||||||
"name": "Main Cashier",
|
"name": "Main Cashier",
|
||||||
"usb_id": "1fc9:2016"
|
"usb_id": "1fc9:2016"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"usb_id": "32re:2023"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "",
|
"name": "",
|
||||||
"usb_id": ""
|
"usb_id": ""
|
||||||
|
|||||||
167
src/main.rs
167
src/main.rs
@@ -31,6 +31,7 @@ struct Config {
|
|||||||
struct DeviceStatus {
|
struct DeviceStatus {
|
||||||
name: String,
|
name: String,
|
||||||
online: bool,
|
online: bool,
|
||||||
|
pending_jobs: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Default)]
|
#[derive(Serialize, Clone, Default)]
|
||||||
@@ -55,6 +56,7 @@ struct NetworkStatus {
|
|||||||
server_online: bool,
|
server_online: bool,
|
||||||
kitchen_printers: Vec<DeviceStatus>,
|
kitchen_printers: Vec<DeviceStatus>,
|
||||||
internet_online: bool,
|
internet_online: bool,
|
||||||
|
cups_service_active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Default)]
|
#[derive(Serialize, Clone, Default)]
|
||||||
@@ -140,6 +142,11 @@ async fn run_diagnosis(config: &Config) -> ScanResult {
|
|||||||
.map(|d| d.available_space() as f64 / 1e9)
|
.map(|d| d.available_space() as f64 / 1e9)
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
// Storage Cleanup Trigger (if < 2GB free)
|
||||||
|
if disk_free < 2.0 {
|
||||||
|
cleanup_old_reports("/tmp/c2l_reports");
|
||||||
|
}
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
let lan_status = Command::new("ip")
|
let lan_status = Command::new("ip")
|
||||||
.arg("link")
|
.arg("link")
|
||||||
@@ -155,9 +162,22 @@ async fn run_diagnosis(config: &Config) -> ScanResult {
|
|||||||
|
|
||||||
let mut kitchen_stats = Vec::new();
|
let mut kitchen_stats = Vec::new();
|
||||||
for p in &config.kitchen_printers {
|
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 {
|
kitchen_stats.push(DeviceStatus {
|
||||||
name: p.name.clone(),
|
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 {
|
ScanResult {
|
||||||
timestamp: format!("{:?}", std::time::SystemTime::now()),
|
timestamp: format!("{:?}", std::time::SystemTime::now()),
|
||||||
hardware: HardwareStatus {
|
hardware: HardwareStatus {
|
||||||
@@ -205,17 +231,70 @@ async fn run_diagnosis(config: &Config) -> ScanResult {
|
|||||||
server_online: server_ping,
|
server_online: server_ping,
|
||||||
kitchen_printers: kitchen_stats,
|
kitchen_printers: kitchen_stats,
|
||||||
internet_online: wan_ping,
|
internet_online: wan_ping,
|
||||||
|
cups_service_active: cups_active,
|
||||||
},
|
},
|
||||||
printers: printer_results,
|
printers: printer_results,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_printer_queue() -> bool {
|
fn cleanup_old_reports(path: &str) {
|
||||||
Command::new("cancel")
|
// 1. Purge old PDFs
|
||||||
.args(["-a", "-x"])
|
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()
|
.status()
|
||||||
.map(|s| s.success())
|
.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 {
|
struct C2LApp {
|
||||||
@@ -224,6 +303,7 @@ struct C2LApp {
|
|||||||
is_scanning: Arc<Mutex<bool>>,
|
is_scanning: Arc<Mutex<bool>>,
|
||||||
rt: Runtime,
|
rt: Runtime,
|
||||||
last_error: Option<String>,
|
last_error: Option<String>,
|
||||||
|
ticket_status: Option<String>,
|
||||||
tab: Tab,
|
tab: Tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +321,7 @@ impl C2LApp {
|
|||||||
is_scanning: Arc::new(Mutex::new(false)),
|
is_scanning: Arc::new(Mutex::new(false)),
|
||||||
rt: Runtime::new().expect("Tokio runtime failed"),
|
rt: Runtime::new().expect("Tokio runtime failed"),
|
||||||
last_error: None,
|
last_error: None,
|
||||||
|
ticket_status: None,
|
||||||
tab: Tab::Dashboard,
|
tab: Tab::Dashboard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,6 +414,20 @@ impl C2LApp {
|
|||||||
"❌ OFFLINE"
|
"❌ 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| {
|
ui.vertical(|ui| {
|
||||||
@@ -356,7 +451,7 @@ impl C2LApp {
|
|||||||
.color(egui::Color32::RED),
|
.color(egui::Color32::RED),
|
||||||
);
|
);
|
||||||
if ui.button("🛡️ Clear Spooler").clicked() {
|
if ui.button("🛡️ Clear Spooler").clicked() {
|
||||||
fix_printer_queue();
|
fix_printer_queue(None);
|
||||||
self.trigger_scan();
|
self.trigger_scan();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +468,19 @@ impl C2LApp {
|
|||||||
"❌ Offline"
|
"❌ 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",
|
"RAM: {} / {} MB",
|
||||||
res.hardware.ram_used_mb, res.hardware.ram_total_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 {
|
} else {
|
||||||
ui.label("Waiting for first scan result...");
|
ui.label("Waiting for first scan result...");
|
||||||
if !scanning {
|
if !scanning {
|
||||||
|
|||||||
Reference in New Issue
Block a user