mod extended; use std::{ env, fs, io::{self, Write}, path::{Path, PathBuf}, process::Command, time::{SystemTime, UNIX_EPOCH}, }; const DATA: &str = "/mnt/intake-data"; fn prompt(q: &str) -> String { print!("{}", q); let _ = io::stdout().flush(); let mut s = String::new(); let _ = io::stdin().read_line(&mut s); s.trim().to_string() } fn run(program: &str, args: &[&str]) -> Result<(), String> { let st = Command::new(program) .args(args) .status() .map_err(|e| format!("{}: {}", program, e))?; if st.success() { Ok(()) } else { Err(format!("{} exited with {}", program, st)) } } fn capture(program: &str, args: &[&str]) -> Result { let o = Command::new(program) .args(args) .output() .map_err(|e| e.to_string())?; if !o.status.success() { return Err(String::from_utf8_lossy(&o.stderr).to_string()); } Ok(String::from_utf8_lossy(&o.stdout).trim().to_string()) } fn sqlquote(s: &str) -> String { format!("'{}'", s.replace('\'', "''")) } fn db_path(data: &Path) -> PathBuf { data.join("database/intake.sqlite3") } fn sql(data: &Path, statement: &str) -> Result { capture( "sqlite3", &[db_path(data).to_str().ok_or("Invalid DB path")?, statement], ) } fn init(data: &Path) -> Result<(), String> { for part in ["database", "reports", "checklists", "config"] { fs::create_dir_all(data.join(part)).map_err(|e| e.to_string())?; } let probe = data.join(".write-test"); fs::write(&probe, b"ok").map_err(|e| format!("Persistent partition not writable: {}", e))?; let _ = fs::remove_file(probe); sql(data,"PRAGMA journal_mode=DELETE; CREATE TABLE IF NOT EXISTS scans (id INTEGER PRIMARY KEY AUTOINCREMENT, asset_id TEXT NOT NULL, timestamp INTEGER NOT NULL, report_path TEXT NOT NULL UNIQUE, inspection_path TEXT, inspection_status TEXT NOT NULL DEFAULT 'NOT_TESTED', sync_status TEXT NOT NULL DEFAULT 'PENDING', synced_inventory_id TEXT);")?; Ok(()) } fn valid_id(s: &str) -> bool { !s.is_empty() && s.len() <= 64 && s.bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') } fn latest(data: &Path, id: &str) -> Option { let root = data.join("reports").join(id); let mut matches = Vec::new(); for d in fs::read_dir(root).ok()?.flatten() { let p = d.path().join("report.json"); if p.is_file() { let mt = p.metadata().ok()?.modified().ok()?; matches.push((mt, p)); } } matches.sort_by_key(|(m, _)| *m); matches.pop().map(|(_, p)| p) } fn get_time() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } fn scan(data: &Path, quick: bool) { let id = prompt("Asset ID: "); if !valid_id(&id) { println!("Invalid ID (letters, digits, underscore and hyphen only)"); return; } let out = data.join("reports"); let outstr = out.to_string_lossy().to_string(); let mut cmd = Command::new("/usr/local/bin/pc-intake"); cmd.args(["--asset-id", &id, "--output", &outstr]); if quick { cmd.arg("--quick"); } else { cmd.arg("--compile"); } let checklist = data.join("checklists").join(format!("{}.txt", id)); if checklist.exists() { cmd.args(["--checklist", &checklist.to_string_lossy()]); } println!("Running {} scan...", if quick { "quick" } else { "full" }); match cmd.status() { Ok(status) if status.success() => { if let Some(report) = latest(data, &id) { let result=sql(data,&format!("INSERT OR IGNORE INTO scans(asset_id,timestamp,report_path) VALUES({},{},{});",sqlquote(&id),get_time(),sqlquote(&report.to_string_lossy()))); match result { Ok(_) => { println!("Saved: {}", report.display()); println!("Inventory sync: PENDING (not uploaded)"); } Err(e) => println!("Database write failed: {}. JSON report remains saved.", e), } } else { println!("Scanner exited successfully, but no report found"); } } Ok(status) => println!("Scanner failed: {}", status), Err(e) => println!("Could not launch scanner: {}", e), } } const CHECKS: &[(&str, &str)] = &[ ( "display", "Show colors/brightness and inspect for dead pixels", ), ( "keyboard", "Check every key, including modifiers and function keys", ), ( "touchpad", "Verify movement, taps, buttons and scrolling; N/A for desktop", ), ( "wifi", "Connect to a network and open a local or remote test service", ), ("audio", "Play left/right sound and check for distortion"), ("microphone", "Record voice and play it back"), ( "webcam", "Preview image and verify focus and quality; N/A if absent", ), ( "usb_ports", "Insert a known-good test device in each physical port", ), ( "charging", "Verify AC adapter, charging status, battery runtime; N/A for desktop", ), ("thermals", "Verify fans, heat and sustained load"), ( "stability", "Check for crashes, unexpected resets or kernel errors", ), ]; fn technician(data: &Path) { let id = prompt("Asset ID for inspection: "); if !valid_id(&id) { println!("Invalid ID"); return; } let mut rows = Vec::new(); for (key, desc) in CHECKS { println!("\n{}: {}", key, desc); if *key == "audio" { println!("Optional speaker test (if installed): speaker-test -c 2 -t wav"); } if *key == "webcam" { println!( "Optional camera detection: ls /dev/video*; preview requires graphical session" ); } loop { let ans = prompt("[p]ass / [f]ail / [u]ntested / [n]ot applicable: ").to_lowercase(); if let Some(st) = match ans.as_str() { "p" => Some("PASS"), "f" => Some("FAIL"), "u" | "" => Some("NOT_TESTED"), "n" => Some("NOT_APPLICABLE"), _ => None, } { rows.push((key.to_string(), st.to_string())); break; } println!("Enter p, f, u or n"); } } let target = data.join("checklists").join(format!("{}.txt", id)); let mut raw = String::new(); for (k, v) in &rows { raw.push_str(&format!("{}={}\n", k, v)); } if let Err(e) = fs::write(&target, raw) { println!("Could not save checklist: {}", e); return; } let fail = rows.iter().any(|(_, s)| s == "FAIL"); let unknown = rows.iter().any(|(_, s)| s == "NOT_TESTED"); let verdict = if fail { "FAIL" } else if unknown { "INCOMPLETE" } else { "COMPLETED" }; let _=sql(data,&format!("UPDATE scans SET inspection_path={},inspection_status={} WHERE id=(SELECT MAX(id) FROM scans WHERE asset_id={});",sqlquote(&target.to_string_lossy()),sqlquote(verdict),sqlquote(&id))); println!( "Inspection {}. Saved {}; re-scan to embed checklist in hardware report.", verdict, target.display() ); } fn recent(data: &Path) { let query = "SELECT id || ' | ' || asset_id || ' | ' || inspection_status || ' | ' || sync_status || ' | ' || report_path FROM scans ORDER BY id DESC LIMIT 20;"; match sql(data, query) { Ok(rows) if rows.is_empty() => println!("No scans yet."), Ok(rows) => println!("{}", rows), Err(err) => println!("Database error: {}", err), } } fn help() { println!("PC Intake AIO v1.7.0 — offline-first technician controller"); println!("Usage: intake-controller [--data PATH] [--self-check]"); } fn main() { let args: Vec = env::args().collect(); if args.iter().any(|x| x == "--help") { help(); return; } if args.iter().any(|x| x == "--self-check") { for tool in [ "sqlite3", "smartctl", "ffmpeg", "mpv", "glmark2", "stress-ng", "memtester", "speaker-test", ] { println!( "{}: {}", tool, if Command::new("sh") .args(["-c", &format!("command -v {} >/dev/null", tool)]) .status() .map(|s| s.success()) .unwrap_or(false) { "FOUND" } else { "MISSING" } ); } return; } let data = if let Some(p) = args.iter().position(|x| x == "--data") { PathBuf::from(args.get(p + 1).unwrap_or(&DATA.to_owned())) } else { PathBuf::from(DATA) }; if let Err(e) = init(&data) { eprintln!("FATAL: {} — refusing to scan without persistent storage", e); std::process::exit(1) } loop { println!("\n==== PC INTAKE AIO v1.7.0 ====\nStorage: {}\n1 Full scan and local queue\n2 Quick inventory\n3 Guided technician checklist\n4 Recent scans and sync states\n5 Check diagnostic tools\n6 Safely power off\n7 Hardware details (UUID / MAC / RAM slots / screen)\n8 Guided audio / webcam / Wi-Fi test\n9 Installed-system maintenance instructions\n10 CPU and RAM stress test (opt-in)\n",data.display()); match prompt("Select: ").as_str() { "1" => scan(&data, false), "2" => scan(&data, true), "3" => technician(&data), "4" => recent(&data), "5" => { let _ = Command::new( env::current_exe().unwrap_or_else(|_| PathBuf::from("intake-controller")), ) .arg("--self-check") .status(); } "6" => { let _ = run("sync", &[]); let _ = run("systemctl", &["poweroff"]); break; } "7" => extended::device_details(&data), "8" => extended::guided_tests(&data), "9" => extended::maintenance_guidance(), "10" => extended::stress_test(&data), _ => println!("Unknown selection"), } } } #[cfg(test)] mod tests { use super::*; #[test] fn id_validation() { assert!(valid_id("PC-0123")); assert!(!valid_id("../../evil")); } #[test] fn sql_escape() { assert_eq!(sqlquote("O'Reilly"), "'O''Reilly'"); } }