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:
Zin Bo Thit
2026-02-10 16:50:44 +06:30
parent 476977dc83
commit 4a3a00772d
6 changed files with 189 additions and 14 deletions

View File

@@ -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"],

View File

@@ -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.
![C2LInspecz Dashboard](assets/dashboard.png)
## 🚀 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 ![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. 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
assets/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -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": ""

View File

@@ -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 {