From 81f06cf6b41f636a5bcf1c78e46040aa39c78f5b Mon Sep 17 00:00:00 2001 From: Denis Stoecklmeier Date: Wed, 14 Oct 2026 20:14:36 +0200 Subject: [PATCH] =?UTF-8?q?USB-AIO=20f=C3=BCr=20automatische=20Ger=C3=A4te?= =?UTF-8?q?pr=C3=BCfung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/tests.yml | 26 + .gitignore | 20 + README.md | 104 + WERKSTATT-ABLAUF.md | 92 + config.example/inventory.json | 5 + controller/Cargo.lock | 7 + controller/Cargo.toml | 5 + controller/src/extended.rs | 194 ++ controller/src/main.rs | 28 + gui/Cargo.lock | 3267 ++++++++++++++++++++++++++++++++ gui/Cargo.toml | 9 + gui/build.rs | 39 + gui/src/diagnostics.rs | 483 +++++ gui/src/inventory.rs | 72 + gui/src/main.rs | 1936 +++++++++++++++++++ gui/src/report.rs | 187 ++ gui/src/wifi.rs | 825 ++++++++ gui/src/workflow.rs | 893 +++++++++ live/autologin.conf | 7 + live/intake-login.sh | 19 + live/intake-prepare.service | 14 + live/intake-prepare.sh | 51 + scanner/Cargo.lock | 7 + scanner/Cargo.toml | 11 + scanner/src/main.rs | 628 ++++++ scripts/build-iso.sh | 22 + scripts/build-release.sh | 13 + scripts/create-profile.sh | 62 + scripts/db-import.py | 17 + scripts/db-view.py | 10 + scripts/install-usb.sh | 80 + scripts/intake-session.sh | 28 + scripts/inventory-sync.py | 424 +++++ scripts/release-manifest.py | 78 + scripts/report-view.py | 9 + tests/smoke.sh | 15 + tests/test_inventory_sync.py | 174 ++ tests/test_release_manifest.py | 44 + tests/test_station.py | 41 + workshop.example.json | 10 + 40 files changed, 9956 insertions(+) create mode 100644 .gitea/workflows/tests.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 WERKSTATT-ABLAUF.md create mode 100644 config.example/inventory.json create mode 100644 controller/Cargo.lock create mode 100644 controller/Cargo.toml create mode 100644 controller/src/extended.rs create mode 100644 controller/src/main.rs create mode 100644 gui/Cargo.lock create mode 100644 gui/Cargo.toml create mode 100644 gui/build.rs create mode 100644 gui/src/diagnostics.rs create mode 100644 gui/src/inventory.rs create mode 100644 gui/src/main.rs create mode 100644 gui/src/report.rs create mode 100644 gui/src/wifi.rs create mode 100644 gui/src/workflow.rs create mode 100644 live/autologin.conf create mode 100644 live/intake-login.sh create mode 100644 live/intake-prepare.service create mode 100644 live/intake-prepare.sh create mode 100644 scanner/Cargo.lock create mode 100644 scanner/Cargo.toml create mode 100644 scanner/src/main.rs create mode 100644 scripts/build-iso.sh create mode 100644 scripts/build-release.sh create mode 100755 scripts/create-profile.sh create mode 100755 scripts/db-import.py create mode 100644 scripts/db-view.py create mode 100755 scripts/install-usb.sh create mode 100755 scripts/intake-session.sh create mode 100644 scripts/inventory-sync.py create mode 100644 scripts/release-manifest.py create mode 100644 scripts/report-view.py create mode 100755 tests/smoke.sh create mode 100644 tests/test_inventory_sync.py create mode 100644 tests/test_release_manifest.py create mode 100644 tests/test_station.py create mode 100644 workshop.example.json diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml new file mode 100644 index 0000000..4deea20 --- /dev/null +++ b/.gitea/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Systempakete installieren + run: | + sudo apt-get update + sudo apt-get install -y pkg-config libx11-dev libxkbcommon-dev libwayland-dev libgl1-mesa-dev + - name: Rust installieren + uses: dtolnay/rust-toolchain@stable + - name: Rust testen + run: | + cargo test --locked --manifest-path scanner/Cargo.toml + cargo test --locked --manifest-path controller/Cargo.toml + cargo test --locked --manifest-path gui/Cargo.toml + - name: Python und Rauchtests + run: | + python3 -m unittest discover -s tests -p 'test_*.py' + bash tests/smoke.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e4973a --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/build/ +/**/target/ +__pycache__/ +*.py[cod] +*.log +*.iso +.idea/ +.vscode/ +.DS_Store + +# Lokale Zugangsdaten und persistente Werkstattdaten +/config/ +/database/ +/reports/ +/inspections/ +/final-reports/ +/outbox/ +inventory-api-key +inventory-server.crt +workshop-wifi.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..879a615 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# CS-R USB-AIO + +Der USB-Stick unterstützt die Annahme und technische Prüfung gespendeter Computer. +Er startet als eigenständiges Arch-Linux-Livesystem, erkennt das aktuelle Gerät über +seine Hardware-ID und verbindet es mit der zentralen Inventarverwaltung. + +## Ablauf in der Werkstatt + +1. Vom USB-Stick starten. +2. Beim ersten Einsatz unter **Einstellungen** das Werkstatt-WLAN verbinden und + **WLAN speichern** aktiviert lassen. +3. Das Gerät wird über DMI-UUID oder Hersteller und Seriennummer erkannt. +4. Die Inventarverwaltung liefert die vorhandene Gerätenummer oder vergibt eine neue. +5. Geräte im Status **Eingang** erhalten automatisch einen Kurzscan mit Modell, CPU, + RAM und Datenträgern. +6. Danach kann der Techniker das Gerät ausschalten oder die Inspektion starten. +7. Prüfstände und Ergebnisse werden als **Inspektion** synchronisiert. Offline + gespeicherte Ergebnisse bleiben in der Warteschlange und werden später erneut gesendet. + +Die Oberfläche konzentriert sich auf **Gerät**, **Inspektion** und **Einstellungen**. +Reparaturen, Berichte und die Synchronisationswarteschlange sind über das Menü erreichbar. + +## Funktionen + +- automatische Hardware-ID und serverseitig vergebene Gerätenummer +- Schutz vor doppelten Datensätzen +- gespeichertes Werkstatt-WLAN mit geschütztem Passwort +- geräteabhängige Tests für Akku, Touchpad, Webcam und mehrere Laufwerke +- getrennte Ergebnisse für Hardwarefehler, nicht verfügbaren Test, + Einrichtungsproblem, nicht geprüft und nicht vorhanden +- Wiederaufnahme nach Neustart oder Stromunterbrechung +- Reparaturnotizen und erneute Prüfung betroffener Komponenten +- druckbarer Abschlussbericht mit Messwerten und Freigabe +- dauerhafte Offline-Warteschlange mit sicheren Wiederholungen +- zentrale Sprache, Testdauer und Grenzwerte +- verifizierbarer Build mit Versions- und Prüfsummenmanifest + +## Projektaufbau + +| Pfad | Inhalt | +| --- | --- | +| `gui/` | Bedienoberfläche und geführte Tests | +| `scanner/` | Hardwareerkennung und Messwerte | +| `controller/` | Geräteklassifizierung und Auswertung | +| `scripts/` | Build, Synchronisation und lokale Datenwerkzeuge | +| `live/` | Start und Vorbereitung des Livesystems | +| `tests/` | Python- und Rauchtests | +| `config.example/` | Beispiel für die Inventarverbindung | + +Der Inventory-Server wird in einem eigenen Repository gepflegt. Dieses Repository +enthält ausschließlich den USB-Client. + +## Entwicklung und Prüfung + +Voraussetzungen sind Rust, Cargo und Python 3. Auf Arch Linux werden für den ISO-Bau +zusätzlich `archiso`, GRUB, Dateisystemwerkzeuge und `libarchive` benötigt. + +```bash +bash scripts/build-release.sh +``` + +Der Befehl führt die Rust-, Python- und Rauchtests aus, baut alle drei Programme im +Release-Modus und erzeugt `build/release.json` mit Build-ID und SHA-256-Prüfsummen. + +## ISO erstellen + +Das Zielverzeichnis muss neu und leer sein: + +```bash +bash scripts/build-iso.sh "$PWD/build/usb-aio-2.6.0" +``` + +Der Build prüft Profil, Binärdateien, Kernelpfade und die FAT32-Größengrenze. Ein +erfolgreicher Softwaretest ersetzt keinen Starttest auf echter UEFI- und BIOS-Hardware. + +## USB-Stick installieren + +Dieser Befehl löscht den ausgewählten Datenträger vollständig: + +```bash +lsblk -o NAME,PATH,TYPE,SIZE,TRAN,MODEL,SERIAL,MOUNTPOINTS +sudo bash scripts/install-usb.sh /dev/sdX /absoluter/pfad/zur.iso +``` + +Das Installationsskript akzeptiert nur ein vollständiges USB-Blockgerät, lehnt den +Systemdatenträger ab und verlangt die exakte Bestätigung `ERASE /dev/sdX`. + +## Konfiguration auf `INTAKE_DATA` + +Die dauerhaften Dateien liegen auf der Datenpartition des Sticks: + +```text +config/inventory.json +config/inventory-api-key +config/inventory-server.crt +config/workshop-wifi.json +config/workshop.json +``` + +API-Schlüssel und WLAN-Passwort gehören nie in Git. Ein privates CA-Zertifikat darf +nur als öffentliches Zertifikat gespeichert werden. Die Datenpartition ist nicht +verschlüsselt und muss wie ein Werkstatt-Zugang behandelt werden. + +Weitere Einzelheiten stehen in [WERKSTATT-ABLAUF.md](WERKSTATT-ABLAUF.md). diff --git a/WERKSTATT-ABLAUF.md b/WERKSTATT-ABLAUF.md new file mode 100644 index 0000000..4b6a9c4 --- /dev/null +++ b/WERKSTATT-ABLAUF.md @@ -0,0 +1,92 @@ +# Werkstattablauf + +## Geräteerkennung + +Beim Start liest das Livesystem geschützte DMI-Felder einmal als `root` und stellt +sie der unprivilegierten Anwendung nur lesbar bereit. Bevorzugt wird eine gültige +System-UUID. Fehlt sie, wird eine gehashte Kombination aus Hersteller und +Seriennummer verwendet. Platzhalter wie `To be filled by O.E.M.` sind ungültig. + +Die Hardware-ID wird an `POST /api/v1/devices/resolve/` gesendet. Der Server ordnet +sie einem vorhandenen Gerät zu oder vergibt atomar eine neue Gerätenummer. UUID und +Seriennummer können als Aliase dasselbe Gerät bezeichnen. Widersprüche werden zur +manuellen Prüfung abgelehnt. + +## Automatische Erfassung + +Für neue Geräte und Geräte in **Eingang** startet ein Kurzscan. Erfasst werden +Hersteller, Modell, CPU, installierter RAM sowie Größe und Art aller Laufwerke. Die +Daten werden revisionsgesichert über den Intake-Endpunkt geschrieben. Ein Gerät in +einem späteren Prozessschritt wird durch den automatischen Scan nicht überschrieben. + +## Inspektion + +Jeder Prüfschritt besitzt eine Aktion, eine verständliche Anleitung, ein sichtbares +Ergebnis und eine Wiederholungsmöglichkeit. Verfügbare Schritte umfassen Display, +Tastatur, Touchpad, WLAN, Lautsprecher, Mikrofon, Kamera, USB-Anschlüsse, Laden, +Temperatur, Stabilität und SMART-Daten jedes internen Laufwerks. + +Ergebnisse unterscheiden: + +- bestanden +- Hardwarefehler +- Test nicht verfügbar +- Einrichtungsproblem +- nicht geprüft +- nicht vorhanden +- Bestätigung erforderlich + +Ein fehlendes Werkzeug oder eine fehlende Berechtigung wird niemals als +Hardwaredefekt gewertet. Optionale Hardware muss ausdrücklich als nicht vorhanden +bestätigt werden. + +Beim Start einer Prüfung wird der Zustand **Inspektion** an den Server gesendet. +Zwischenstände und der Abschluss verwenden unveränderliche Anforderungs-IDs, damit +ein Verbindungsabbruch keine doppelten Einträge erzeugt. Ein Abschluss benötigt +einen Technikernamen und vollständig bewertete Prüfschritte. + +## Unterbrechung und Offlinebetrieb + +Der lokale Prüfstand wird atomar gespeichert. Nach Neustart öffnet die Anwendung +die letzte unvollständige Prüfung mit Messwerten und Notizen. Noch nicht bestätigte +Uploads liegen in `outbox/inspections/`; bestätigte Uploads werden nach +`outbox/sent/` verschoben. Beim nächsten Start werden auch gespeicherte Prüfungen +des zuvor bearbeiteten Computers wieder in die Warteschlange aufgenommen. + +## Reparaturen und Bericht + +Eine Reparatur enthält Bauteil, Tätigkeit, Ersatzteil, Techniker und Zeitpunkt. Der +vorherige Befund bleibt erhalten und der betroffene Test wird erneut geöffnet. Der +HTML-Abschlussbericht enthält Spezifikationen, Laufwerks- und Akkuzustand, +Messwerte, Mängel, Reparaturverlauf und technische Freigabe. + +## WLAN und Inventarverbindung + +Das Werkstatt-WLAN wird erst nach erfolgreicher Verbindung gespeichert. Die Datei +`config/workshop-wifi.json` ist nur für den Besitzer lesbar und kann in den +Einstellungen ersetzt oder gelöscht werden. Das Passwort wird über die +Standardeingabe an NetworkManager übergeben und erscheint nicht in Prozessargumenten. + +Die Inventarverbindung benötigt: + +```json +{"url": "https://inventory.example.org"} +``` + +Der API-Schlüssel liegt separat in `config/inventory-api-key`. Für einen privaten +HTTPS-Server kann `config/inventory-server.crt` die öffentliche CA enthalten. +Zertifikats- und Hostnamenprüfung bleiben aktiv. + +## Abnahme eines neuen Builds + +Vor dem Werkstatteinsatz sind mindestens diese Punkte auf echter Hardware zu prüfen: + +1. UEFI- und BIOS-Start +2. persistente Daten nach Neustart +3. automatische Wiederverbindung mit dem WLAN +4. richtige Gerätezuordnung und Dublettenwarnung +5. Intake-Synchronisation ohne Überschreiben späterer Zustände +6. Ein- und Ausgabegeräte sowie jedes Laufwerk +7. Offline-Warteschlange und Wiederholung nach Verbindungsabbruch +8. druckbarer Bericht +9. kontrolliertes Herunterfahren diff --git a/config.example/inventory.json b/config.example/inventory.json new file mode 100644 index 0000000..607ecec --- /dev/null +++ b/config.example/inventory.json @@ -0,0 +1,5 @@ +{ + "url": "https://inventory.example.org", + "device_type_id": 1, + "status_id": 1 +} \ No newline at end of file diff --git a/controller/Cargo.lock b/controller/Cargo.lock new file mode 100644 index 0000000..d112d83 --- /dev/null +++ b/controller/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "intake-controller" +version = "1.7.0" diff --git a/controller/Cargo.toml b/controller/Cargo.toml new file mode 100644 index 0000000..59749a7 --- /dev/null +++ b/controller/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "intake-controller" +version = "1.7.0" +edition = "2021" +[dependencies] diff --git a/controller/src/extended.rs b/controller/src/extended.rs new file mode 100644 index 0000000..0b8e588 --- /dev/null +++ b/controller/src/extended.rs @@ -0,0 +1,194 @@ +//! Optional technician workstations; no write access to internal devices. +use std::{fs, io::{self, Write}, path::Path, process::Command, time::{SystemTime, UNIX_EPOCH}}; + +fn ask(prompt: &str) -> String { + print!("{}", prompt); + let _ = io::stdout().flush(); + let mut answer = String::new(); + if io::stdin().read_line(&mut answer).is_err() { return String::new(); } + answer.trim().to_string() +} +fn read_trim(path: &str) -> Option { + fs::read_to_string(path).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) +} +fn output(exe: &str, args: &[&str]) -> Option { + let r = Command::new(exe).args(args).output().ok()?; + if r.status.success() { Some(String::from_utf8_lossy(&r.stdout).trim().to_owned()) } else { None } +} +fn escape_json(s: &str) -> String { + let mut result = String::from("\""); + for c in s.chars() { match c { + '"' => result.push_str("\\\""), '\\' => result.push_str("\\\\"), + '\n' => result.push_str("\\n"), '\r' => result.push_str("\\r"), '\t' => result.push_str("\\t"), + c if c.is_control() => result.push_str(&format!("\\u{:04x}", c as u32)), + c => result.push(c), + }} + result.push('"'); result +} +fn valid_asset(id: &str) -> bool { + !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') +} +fn normalize_hostname(model: &str, asset: &str) -> String { + let source = format!("{}-{}", model, asset).to_ascii_lowercase(); + let mut result = String::new(); + let mut last_dash = false; + for c in source.chars() { + if c.is_ascii_alphanumeric() { result.push(c); last_dash = false; } + else if !last_dash && !result.is_empty() { result.push('-'); last_dash = true; } + } + result.trim_matches('-').chars().take(63).collect::().trim_end_matches('-').to_owned() +} +fn net_interfaces() -> Vec<(String, String, String, String)> { + let mut list = Vec::new(); + if let Ok(entries) = fs::read_dir("/sys/class/net") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().into_owned(); + if name == "lo" { continue; } + let base = format!("/sys/class/net/{name}"); + let kind = if Path::new(&format!("{base}/wireless")).exists() { "Wi-Fi" } else { "LAN/other" }; + let carrier = read_trim(&format!("{base}/carrier")).unwrap_or_else(|| "unknown".into()); + let mac = read_trim(&format!("{base}/address")).unwrap_or_else(|| "unknown".into()); + list.push((name, kind.to_string(), carrier, mac)); + } + } + list.sort(); list +} +fn ethernet_connected() -> bool { net_interfaces().iter().any(|(_,kind,carrier,_)| kind == "LAN/other" && carrier == "1") } +fn memory_dmi() -> (String, String) { + let text = match output("dmidecode", &["-t", "memory"]) { + Some(s) => s, None => return ("UNKNOWN (dmidecode unavailable or permissions missing)".into(), "UNKNOWN".into()), + }; + let mut slots = 0usize; + let mut occupied = 0usize; + let mut max_cap = String::from("UNKNOWN"); + let mut in_device = false; + for line in text.lines() { + let t = line.trim(); + if t == "Memory Device" { slots += 1; in_device = true; continue; } + if t == "Physical Memory Array" { in_device = false; continue; } + if t.starts_with("Maximum Capacity:") && max_cap == "UNKNOWN" { max_cap = t.trim_start_matches("Maximum Capacity:").trim().into(); } + if in_device && t.starts_with("Size:") && !t.contains("No Module Installed") && !t.ends_with("Unknown") { occupied += 1; in_device = false; } + } + let summary = if slots == 0 { "UNKNOWN".into() } else { format!("{occupied}/{slots} occupied; {} free", slots.saturating_sub(occupied)) }; + (summary, max_cap) +} +fn display_info() -> String { + let mut displays = Vec::new(); + if let Ok(entries) = fs::read_dir("/sys/class/drm") { + for entry in entries.flatten() { + let path = entry.path(); + if !path.join("status").is_file() { continue; } + let status = fs::read_to_string(path.join("status")).unwrap_or_default(); + if status.trim() != "connected" { continue; } + let name = entry.file_name().to_string_lossy().to_string(); + let modes = fs::read_to_string(path.join("modes")).unwrap_or_default(); + let mode = modes.lines().next().unwrap_or("resolution unknown"); + let size = fs::read(path.join("edid")).ok().and_then(|e| { + if e.len() >= 23 && e[21] > 0 && e[22] > 0 { + let diag = ((e[21] as f64).powi(2) + (e[22] as f64).powi(2)).sqrt() / 2.54; + Some(format!("{diag:.1} inch (EDID estimate)")) + } else { None } + }).unwrap_or_else(|| "size UNKNOWN".into()); + displays.push(format!("{name}: {mode}, {size}")); + } + } + if displays.is_empty() { "UNKNOWN (no active DRM connector)".into() } else { displays.join("; ") } +} +fn save(data: &Path, asset: &str, stem: &str, content: &str) -> Result<(),String> { + let folder = data.join("reports").join(asset).join("supplemental"); + fs::create_dir_all(&folder).map_err(|e| e.to_string())?; + let path = folder.join(stem); + fs::write(&path, content).map_err(|e|e.to_string())?; + println!("Saved to {}", path.display()); + Ok(()) +} +pub fn device_details(data: &Path) { + let asset = ask("Asset ID / Geraetenummer: "); + if !valid_asset(&asset) { println!("Invalid asset ID"); return; } + let manufacturer = read_trim("/sys/class/dmi/id/sys_vendor").unwrap_or_else(|| "UNKNOWN".into()); + let model = read_trim("/sys/class/dmi/id/product_name").unwrap_or_else(|| "UNKNOWN".into()); + let uuid = read_trim("/sys/class/dmi/id/product_uuid").unwrap_or_else(|| "UNKNOWN".into()); + let cpu = read_trim("/proc/cpuinfo").and_then(|s|s.lines().find_map(|line|line.strip_prefix("model name").and_then(|v|v.split_once(':').map(|(_,v)|v.trim().to_owned())))).unwrap_or_else(|| "UNKNOWN".into()); + let (slots, max_ram) = memory_dmi(); + let display = display_info(); + let host = normalize_hostname(&model, &asset); + let connections = net_interfaces(); + println!("\nManufacturer: {manufacturer}\nModel: {model}\nCPU: {cpu}\nUUID: {uuid}\nRAM slots: {slots}\nMax RAM (BIOS estimate): {max_ram}\nDisplay: {display}\nProposed hostname: {host}"); + for (name,kind,carrier,mac) in &connections { println!("Interface {name}: {kind}, carrier={carrier}, MAC={mac}"); } + println!("Storage sizes and device type are collected by the main scanner; don't include the boot USB."); + let mut json = format!("{{\"asset_id\":{},\"manufacturer\":{},\"model\":{},\"cpu\":{},\"uuid\":{},\"ram_slots\":{},\"ram_max\":{},\"display\":{},\"proposed_hostname\":{},\"network\":[",escape_json(&asset),escape_json(&manufacturer),escape_json(&model),escape_json(&cpu),escape_json(&uuid),escape_json(&slots),escape_json(&max_ram),escape_json(&display),escape_json(&host)); + for (i,(name,kind,carrier,mac)) in connections.iter().enumerate() { + if i>0 { json.push(','); } + json.push_str(&format!("{{\"interface\":{},\"type\":{},\"carrier\":{},\"mac\":{}}}",escape_json(name),escape_json(kind),escape_json(carrier),escape_json(mac))); + } + json.push_str("]}\n"); + if let Err(e)=save(data,&asset,"hardware-details.json",&json){ eprintln!("Save failed: {e}"); } +} +fn check_prompt(label: &str) -> &'static str { + println!("{label}"); + loop { match ask("Result [p]ass / [f]ail / [u]ntested / [n]ot applicable: ").to_lowercase().as_str() { + "p" => return "PASS", "f" => return "FAIL", "u" | "" => return "NOT_TESTED", "n" => return "NOT_APPLICABLE", _ => println!("Use p/f/u/n"), + }} +} +pub fn guided_tests(data: &Path) { + let asset=ask("Asset ID: "); if !valid_asset(&asset) { println!("Invalid asset ID"); return; } + println!("\nNETWORK PRECHECK: Before Wi-Fi test, unplug Ethernet if one is connected."); + if ethernet_connected() { + println!("WARNING: Wired network link detected. Unplug LAN cable before Wi-Fi test."); + let _=ask("After unplugging LAN press Enter to recheck..."); + if ethernet_connected() { println!("LAN link remains active: Wi-Fi connectivity cannot be confirmed until disconnected."); } + } else { println!("No active wired link detected. Wireless testing may proceed."); } + let wifi = if ethernet_connected() { "NOT_TESTED" } else { + println!("Wi-Fi interfaces: {:?}",net_interfaces().iter().filter(|x|x.1=="Wi-Fi").map(|x|&x.0).collect::>()); + println!("Connect using your network manager and verify actual traffic over Wi-Fi. Interface detection alone is not enough."); + check_prompt("Wi-Fi connection and data transfer verified?") + }; + println!("\nAUDIO: We'll attempt a stereo test sound if speaker-test exists. Confirm left/right audibly."); + if ask("Run short speaker test? [y/N] ").eq_ignore_ascii_case("y") { + let result=Command::new("speaker-test").args(["-c","2","-t","wav","-l","1"]).status(); + println!("speaker-test outcome: {:?} (not proof that sound was audible)",result); + } + let audio=check_prompt("Speakers: both channels audible, without distortion?"); + println!("\nMICROPHONE: make a short recording then play it back, confirming sound clarity."); + let mic=check_prompt("Microphone recorded and replayed clearly?"); + println!("\nCAMERA: look for /dev/video*, and preview live picture if a desktop is running."); + if let Ok(devices)=fs::read_dir("/dev") { + for dev in devices.flatten() { let n=dev.file_name().to_string_lossy().to_string(); if n.starts_with("video") {println!("Found /dev/{n}");} } + } + let webcam=check_prompt("Webcam picture inspected and working?"); + let mut json=format!("{{\"asset_id\":{},\"timestamp\":{},\"wifi\":{},\"audio\":{},\"microphone\":{},\"webcam\":{}}}\n",escape_json(&asset),SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),escape_json(wifi),escape_json(audio),escape_json(mic),escape_json(webcam)); + if let Err(e)=save(data,&asset,"guided-tests.json",&json){eprintln!("Save failed: {e}");} + json.clear(); +} +pub fn maintenance_guidance() { + println!("\nMAINTENANCE OF INSTALLED OPERATING SYSTEM"); + println!("The live USB does NOT automatically run updates on an internal disk. Only run maintenance after booting the installed system and confirming its Linux distribution."); + println!("For Debian/Ubuntu/Mint installed systems:"); + println!(" sudo apt update && sudo apt upgrade && sudo apt autoremove && sudo apt clean"); + println!("For Arch Linux installed systems:"); + println!(" sudo pacman -Syu"); + println!("Do not chroot or rename another machine's OS without selecting and validating its target disk. Proposed hostname is stored in hardware-details.json for controlled provisioning."); + let _=ask("Press Enter to return to menu..."); +} +#[cfg(test)] mod tests { + use super::*; + #[test] fn valid_asset_ids(){assert!(valid_asset("PC-0042")); assert!(!valid_asset("../evil"));} + #[test] fn hostnames(){assert_eq!(normalize_hostname("EliteBook 745 G6","PC-0001"),"elitebook-745-g6-pc-0001");} + #[test] fn escape(){assert_eq!(escape_json("a\"b"),"\"a\\\"b\"");} +} +/// Read-only CPU stress plus allocated userspace memory checks. Not a full RAM test. +pub fn stress_test(data: &Path) { + let asset=ask("Asset ID: "); if !valid_asset(&asset) { println!("Invalid asset ID"); return; } + println!("\nThis uses stress-ng (120 seconds) and memtester on up to 50% of MemAvailable, capped at 8 GiB."); + println!("Do not use on a machine with unstable cooling. This cannot test all physical RAM or prove absence of faults."); + if !ask("Run stress tests now? Type YES: ").eq("YES") {println!("Cancelled");return;} + let cpu=Command::new("stress-ng").args(["--cpu","0","--timeout","120s","--metrics-brief"]).status(); + println!("CPU stress result: {cpu:?}"); + let mem_kib=read_trim("/proc/meminfo").and_then(|t|t.lines().find(|l|l.starts_with("MemAvailable:")).and_then(|line| line.split_whitespace().nth(1)).and_then(|n|n.parse::().ok())).unwrap_or(0); + let mem_mib=std::cmp::min(mem_kib/2048,8192); + let ram=if mem_mib>=128 { Command::new("memtester").args([&format!("{mem_mib}M"),"1"]).status() } else { println!("Not enough free RAM for safe test");return; }; + println!("RAM stress result: {ram:?}"); + let status=|r: &io::Result|match r {Ok(s) if s.success()=>"PASS",Ok(_)=>"FAIL",Err(_)=>"UNAVAILABLE"}; + let report=format!("{{\"asset_id\":{},\"cpu_stress\":{},\"ram_stress\":{},\"ram_test_mib\":{},\"coverage\":\"PARTIAL_USERSPACE\"}}\n",escape_json(&asset),escape_json(status(&cpu)),escape_json(status(&ram)),mem_mib); + if let Err(e)=save(data,&asset,"stress-test.json",&report){eprintln!("Save failed: {e}");} +} diff --git a/controller/src/main.rs b/controller/src/main.rs new file mode 100644 index 0000000..16f08c3 --- /dev/null +++ b/controller/src/main.rs @@ -0,0 +1,28 @@ +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'");}} diff --git a/gui/Cargo.lock b/gui/Cargo.lock new file mode 100644 index 0000000..075470c --- /dev/null +++ b/gui/Cargo.lock @@ -0,0 +1,3267 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix 1.1.5", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.5", + "rustix 1.1.5", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow 0.14.2", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.52.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +dependencies = [ + "ahash", + "emath", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-wgpu" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00fd5d06d8405397e64a928fa0ef3934b3c30273ea7603e3dc4627b1f7a1a82" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "thiserror 1.0.69", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6" +dependencies = [ + "ahash", + "arboard", + "egui", + "log", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow 0.14.2", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "epaint" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.5", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glow" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.2", + "cfg_aliases 0.2.2", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases 0.2.2", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.2", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "gpu-allocator" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.2", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.13.2", + "com", + "libc", + "libloading", + "thiserror 1.0.69", + "widestring", + "winapi", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "intake-gui" +version = "2.6.0" +dependencies = [ + "eframe", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2 0.5.3", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2", + "dispatch", + "libc", + "objc2 0.5.3", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.14.5", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.5", + "thiserror 2.0.21", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.5", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "wgpu" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.13.2", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.13.2", + "cfg_aliases 0.1.1", + "core-graphics-types", + "glow 0.13.1", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +dependencies = [ + "bitflags 2.13.2", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.2", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.5", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/gui/Cargo.toml b/gui/Cargo.toml new file mode 100644 index 0000000..7bdfb3a --- /dev/null +++ b/gui/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "intake-gui" +version = "2.6.0" +edition = "2021" + +[dependencies] +eframe = { version = "0.29.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/gui/build.rs b/gui/build.rs new file mode 100644 index 0000000..688f46f --- /dev/null +++ b/gui/build.rs @@ -0,0 +1,39 @@ +use std::{env, fs, path::Path}; +fn main() { + println!("cargo:rerun-if-env-changed=CSR_BUILD_ID"); + println!("cargo:rerun-if-changed=src"); + println!("cargo:rerun-if-changed=../scripts/inventory-sync.py"); + println!("cargo:rerun-if-changed=Cargo.lock"); + fn hash_dir(path: &Path, hash: &mut u64) { + let mut files: Vec<_> = fs::read_dir(path) + .unwrap() + .flatten() + .map(|e| e.path()) + .collect(); + files.sort(); + for p in files { + if p.is_dir() { + hash_dir(&p, hash); + } else { + for b in fs::read(p).unwrap() { + *hash = (*hash ^ b as u64).wrapping_mul(1099511628211); + } + } + } + } + let mut hash = 14695981039346656037u64; + hash_dir(Path::new("src"), &mut hash); + for file in [ + "../scripts/inventory-sync.py", + "Cargo.toml", + "Cargo.lock", + "build.rs", + ] { + for b in fs::read(file).unwrap() { + hash = (hash ^ b as u64).wrapping_mul(1099511628211); + } + } + let id = env::var("CSR_BUILD_ID") + .unwrap_or_else(|_| format!("{}-{hash:016x}", env::var("CARGO_PKG_VERSION").unwrap())); + println!("cargo:rustc-env=CSR_BUILD_ID={id}"); +} diff --git a/gui/src/diagnostics.rs b/gui/src/diagnostics.rs new file mode 100644 index 0000000..a40d2a2 --- /dev/null +++ b/gui/src/diagnostics.rs @@ -0,0 +1,483 @@ +//! Read-only or temporary hardware diagnostics. Never mark a technician check PASS automatically. +use std::{ + fs, + path::Path, + process::{Command, Stdio}, + thread, + time::{Duration, Instant}, +}; + +fn available(tool: &str) -> bool { + Command::new("sh") + .args(["-c", "command -v \"$1\" >/dev/null 2>&1", "sh", tool]) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} +fn exec(tool: &str, args: &[&str]) -> Result { + let out = Command::new("timeout") + .args(["--kill-after=3s", "150s", tool]) + .args(args) + .output() + .map_err(|e| format!("{tool}: {e}"))?; + let log = format!( + "{}\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + if out.status.success() { + Ok(log) + } else { + Err(format!("{tool} ({})\n{log}", out.status)) + } +} +fn temps() -> Vec<(String, f64)> { + let mut values = Vec::new(); + let Ok(chips) = fs::read_dir("/sys/class/hwmon") else { + return values; + }; + for entry in chips.flatten() { + let base = entry.path(); + let chip = fs::read_to_string(base.join("name")) + .unwrap_or_default() + .trim() + .to_owned(); + let Ok(files) = fs::read_dir(&base) else { + continue; + }; + for file in files.flatten() { + let name = file.file_name().to_string_lossy().to_string(); + if !name.starts_with("temp") || !name.ends_with("_input") { + continue; + } + let Ok(raw) = fs::read_to_string(file.path()) else { + continue; + }; + let Ok(milli) = raw.trim().parse::() else { + continue; + }; + let c = milli / 1000.0; + if !(-20.0..=150.0).contains(&c) { + continue; + } + values.push((format!("{chip}:{name}"), c)); + } + } + values +} +fn thermal(settings: &crate::workflow::Settings) -> (bool, String) { + if !available("stress-ng") { + return (false, "UNAVAILABLE: stress-ng fehlt im Live-System".into()); + } + let baseline = temps(); + let start = Instant::now(); + let mut child = match Command::new("stress-ng") + .args([ + "--cpu", + "0", + "--timeout", + &format!("{}s", settings.stress_seconds), + "--metrics-brief", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(c) => c, + Err(e) => return (false, format!("stress-ng konnte nicht starten: {e}")), + }; + let mut samples = 0usize; + let mut peak: Option = None; + loop { + for (_, t) in temps() { + peak = Some(peak.map_or(t, |p| p.max(t))); + samples += 1; + } + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) => {} + Err(e) => return (false, format!("Prozessprüfung fehlgeschlagen: {e}")), + } + if start.elapsed() > Duration::from_secs(settings.stress_seconds + 15) { + let _ = child.kill(); + break; + } + thread::sleep(Duration::from_secs(2)); + } + let output = match child.wait_with_output() { + Ok(o) => o, + Err(e) => return (false, format!("Ausgabe fehlt: {e}")), + }; + let mut log = format!( + "Dauer: {:.0}s\nSensorwerte zu Beginn: {:?}\nMesspunkte: {}\nMaximal beobachtet: {}\n{}", + start.elapsed().as_secs_f64(), + baseline, + samples, + peak.map(|p| format!("{p:.1} °C")) + .unwrap_or("UNAVAILABLE".into()), + String::from_utf8_lossy(&output.stderr) + ); + if let Some(p) = peak { + log.push_str(&format!("\nPEAK_TEMPERATURE_C={p}")); + } + if let Some(p) = peak { + if p >= settings.max_temperature_c { + log.push_str( + "\nWARNUNG: Werkstatt-Temperaturgrenze überschritten; manuelle Prüfung nötig.", + ); + } + } + if samples == 0 { + log.push_str("\nWARNUNG: Kein Temperatursensor auslesbar."); + } + ( + output.status.success() + && samples > 0 + && peak + .map(|p| p < settings.max_temperature_c) + .unwrap_or(false) + && start.elapsed() >= Duration::from_secs(settings.stress_seconds.saturating_sub(2)), + log, + ) +} +fn stability(settings: &crate::workflow::Settings) -> (bool, String) { + if !available("stress-ng") { + return (false, "UNAVAILABLE: stress-ng fehlt".into()); + } + let mem_kib = fs::read_to_string("/proc/meminfo") + .ok() + .and_then(|s| { + s.lines() + .find(|l| l.starts_with("MemAvailable:")) + .and_then(|l| l.split_whitespace().nth(1)) + .and_then(|v| v.parse::().ok()) + }) + .unwrap_or(0); + // Conservatively cap test allocation; never allocate all free RAM in a live USB GUI. + if mem_kib < 256 * 1024 { + return ( + false, + "Insufficient available RAM for a bounded stress test / Zu wenig freier RAM".into(), + ); + } + let ram_mib = ((mem_kib / 1024) / 4).clamp(128, 2048); + let mut child = match Command::new("stress-ng") + .args([ + "--cpu", + "0", + "--vm", + "1", + "--vm-bytes", + &format!("{ram_mib}M"), + "--verify", + "--timeout", + &format!("{}s", settings.stress_seconds), + "--metrics-brief", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(c) => c, + Err(e) => return (false, format!("Start fehlgeschlagen: {e}")), + }; + let start = Instant::now(); + let mut peak: Option = None; + loop { + for (_, v) in temps() { + peak = Some(peak.map_or(v, |p| p.max(v))); + } + match child.try_wait() { + Ok(Some(_)) => break, + Ok(None) => {} + Err(e) => return (false, format!("Prozessfehler: {e}")), + } + if start.elapsed() > Duration::from_secs(settings.stress_seconds + 15) { + let _ = child.kill(); + break; + } + thread::sleep(Duration::from_secs(2)); + } + match child.wait_with_output(){Ok(o)=>(o.status.success() && peak.map(|p|p=Duration::from_secs(settings.stress_seconds.saturating_sub(2)),format!("CPU-/RAM-Test {:.0}s, RAM-Verbrauch ca. {} MiB, Temp-Spitze {}\nPEAK_TEMPERATURE_C={}\n{}",start.elapsed().as_secs_f64(),ram_mib,peak.map(|p|format!("{p:.1}°C")).unwrap_or("UNKNOWN".into()),peak.map(|p|p.to_string()).unwrap_or_default(),String::from_utf8_lossy(&o.stderr))),Err(e)=>(false,format!("Ausgabe fehlgeschlagen: {e}"))} +} +fn microphone(settings: &crate::workflow::Settings) -> (bool, String) { + if !available("arecord") || !available("aplay") { + return ( + false, + "UNAVAILABLE: alsa-utils / arecord / aplay fehlen".into(), + ); + } + let path = format!( + "/tmp/csr-audio-{}-{}.wav", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let record = exec( + "arecord", + &[ + "-q", + "-d", + &settings.recording_seconds.to_string(), + "-f", + "S16_LE", + "-r", + "16000", + "-c", + "1", + &path, + ], + ); + let result = match record { + Err(e) => (false, format!("Mikrofonaufnahme fehlgeschlagen:\n{e}")), + Ok(_) => match exec("aplay", &["-q", &path]) { + Ok(_) => ( + true, + format!( + "{} Sekunden aufgenommen und abgespielt. Bitte Tonqualität bestätigen.", + settings.recording_seconds + ), + ), + Err(e) => (false, format!("Wiedergabe fehlgeschlagen: {e}")), + }, + }; + if Path::new(&path).exists() { + let _ = fs::remove_file(&path); + } + result +} +fn camera(settings: &crate::workflow::Settings) -> (bool, String) { + if !Path::new("/dev/video0").exists() { + return (false, "UNAVAILABLE: keine Kamera unter /dev/video0".into()); + } + if !available("ffplay") { + return (false, "UNAVAILABLE: ffplay fehlt".into()); + } + let out = exec( + "timeout", + &[ + &format!("{}s", settings.camera_seconds), + "ffplay", + "-hide_banner", + "-loglevel", + "error", + "-f", + "video4linux2", + "-i", + "/dev/video0", + ], + ); + match out { + Ok(s) => ( + true, + format!("Vorschau beendet. Sichtprüfung erforderlich.\n{s}"), + ), + Err(s) => { + // timeout exits 124 after a normal 25s bounded preview. + if s.contains("124") { + ( + true, + format!( + "{} Sekunden Vorschau beendet. Sichtprüfung erforderlich.", + settings.camera_seconds + ), + ) + } else { + (false, s) + } + } + } +} +fn run_legacy(tool: &str, settings: &crate::workflow::Settings) -> (bool, String) { + match tool { + "audio" => match exec("speaker-test", &["-c", "2", "-t", "wav", "-l", "1"]) { + Ok(s) => ( + true, + format!("Linker/rechter Testkanal ausgegeben. Bitte Hörtest bestätigen.\n{s}"), + ), + Err(s) => (false, s), + }, + "microphone" => microphone(settings), + "camera" => camera(settings), + "thermals" => thermal(settings), + "stability" => stability(settings), + "wifi" => { + let service = exec("nmcli", &["general", "status"]); + match service { + Err(s) => (false, format!("NetworkManager nicht bereit: {s}")), + Ok(status) => { + let dev = exec("nmcli", &["device", "status"]).unwrap_or_else(|e| e); + let scan = exec( + "nmcli", + &["-f", "IN-USE,SSID,SIGNAL", "device", "wifi", "list"], + ) + .unwrap_or_else(|e| e); + (true,format!("Status:\n{status}\nGeräte:\n{dev}\nNetze:\n{scan}\nLAN trennen und WLAN-Verbindung aktiv testen; Sichtbarkeit ist noch kein PASS.")) + } + } + } + "video" => ( + true, + "Kameratest bitte über Kameravorschau durchführen.".into(), + ), + _ => (false, "Unbekannter Diagnosetest".into()), + } +} + +#[derive(Clone, Debug)] +pub struct Diagnostic { + pub outcome: crate::workflow::Outcome, + pub text: String, +} +pub fn run(tool: &str, settings: &crate::workflow::Settings) -> Diagnostic { + use crate::workflow::Outcome; + let required: &[&str] = match tool { + "audio" => &["speaker-test"], + "microphone" => &["arecord", "aplay"], + "webcam" => &["ffplay", "timeout"], + "thermals" | "stability" => &["stress-ng"], + "usb_ports" => &["lsusb"], + _ => &[], + }; + if !available("timeout") { + return Diagnostic { + outcome: Outcome::Unavailable, + text: "timeout tool missing / fehlt".into(), + }; + } + let missing: Vec<_> = required.iter().filter(|p| !available(p)).copied().collect(); + if !missing.is_empty() { + return Diagnostic { + outcome: Outcome::Unavailable, + text: format!("Missing tools / Fehlende Werkzeuge: {}", missing.join(", ")), + }; + } + if let Some(disk) = tool.strip_prefix("disk:") { + if !disk + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') + { + return Diagnostic { + outcome: Outcome::SetupProblem, + text: "Invalid disk name".into(), + }; + } + if !available("smartctl") { + return Diagnostic { + outcome: Outcome::Unavailable, + text: "smartctl missing / fehlt".into(), + }; + } + return match Command::new("timeout") + .args(["--kill-after=3s", "30s", "smartctl"]) + .args(["-j", "-H", "-A", &format!("/dev/{disk}")]) + .output() + { + Ok(o) => { + let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap_or_default(); + let outcome = match v + .pointer("/smart_status/passed") + .and_then(serde_json::Value::as_bool) + { + Some(false) => Outcome::HardwareFailure, + Some(true) => Outcome::NeedsReview, + None => Outcome::SetupProblem, + }; + Diagnostic { + outcome, + text: format!( + "{}\n{}", + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr) + ), + } + } + Err(e) => Diagnostic { + outcome: Outcome::SetupProblem, + text: e.to_string(), + }, + }; + } + if tool == "charging" { + let mut lines = Vec::new(); + if let Ok(entries) = fs::read_dir("/sys/class/power_supply") { + for e in entries.flatten() { + let p = e.path(); + let read = |k: &str| { + fs::read_to_string(p.join(k)) + .unwrap_or_default() + .trim() + .to_owned() + }; + lines.push(format!("{}: type={} status={} charge={}% online={} energy_full={} energy_full_design={} charge_full={} charge_full_design={}",e.file_name().to_string_lossy(),read("type"),read("status"),read("capacity"),read("online"),read("energy_full"),read("energy_full_design"),read("charge_full"),read("charge_full_design"))); + } + } + return Diagnostic { + outcome: if lines.is_empty() { + Outcome::Unavailable + } else { + Outcome::NeedsReview + }, + text: if lines.is_empty() { + "No power supply data / Keine Akkudaten".into() + } else { + lines.join("\n") + }, + }; + } + if tool == "usb_ports" { + return match exec("lsusb", &[]) { + Ok(text) => Diagnostic { + outcome: Outcome::NeedsReview, + text, + }, + Err(text) => Diagnostic { + outcome: Outcome::SetupProblem, + text, + }, + }; + } + let (ok, text) = run_legacy(if tool == "webcam" { "camera" } else { tool }, settings); + // Command failures are setup problems, never proof of a broken component. + Diagnostic { + outcome: if text.starts_with("UNAVAILABLE") + || (tool == "thermals" && text.contains("Messpunkte: 0")) + { + Outcome::Unavailable + } else if peak_temperature(&text) + .map(|v| v >= settings.max_temperature_c) + .unwrap_or(false) + { + Outcome::NeedsReview + } else if ok { + Outcome::NeedsReview + } else { + Outcome::SetupProblem + }, + text, + } +} + +pub fn peak_temperature(text: &str) -> Option { + text.lines().find_map(|l| { + l.strip_prefix("PEAK_TEMPERATURE_C=") + .and_then(|s| s.parse::().ok()) + .filter(|v| v.is_finite()) + }) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn peak_is_machine_readable_and_finite() { + assert_eq!( + peak_temperature("log\nPEAK_TEMPERATURE_C=94.5\nend"), + Some(94.5) + ); + assert_eq!(peak_temperature("PEAK_TEMPERATURE_C=NaN"), None); + } +} diff --git a/gui/src/inventory.rs b/gui/src/inventory.rs new file mode 100644 index 0000000..11eb26d --- /dev/null +++ b/gui/src/inventory.rs @@ -0,0 +1,72 @@ +use serde::Deserialize; +use std::{path::Path, process::Command}; +const BRIDGE: &str = include_str!("../../scripts/inventory-sync.py"); +#[derive(Clone, Debug, Deserialize)] +pub struct QueueEntry { + pub asset_id: String, + pub inventory_number: Option, + pub status: String, + pub attempts: u64, + pub next_attempt: u64, + pub error: String, + pub report_path: String, +} +fn call(data: &Path, args: &[&str]) -> Result { + let out = Command::new("python3") + .args(["-c", BRIDGE, "--data"]) + .arg(data) + .args(args) + .output() + .map_err(|e| e.to_string())?; + let log = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + if out.status.success() { + Ok(log) + } else { + Err(log) + } +} +pub fn queue(data: &Path) -> Result, String> { + serde_json::from_str(&call(data, &["--list"])?).map_err(|e| e.to_string()) +} +pub fn sync(data: &Path, retry: bool) -> Result { + call(data, if retry { &["--retry"] } else { &[] }) +} + +pub fn check(data: &Path) -> Result { + call(data, &["--check"]) +} + +#[derive(Deserialize)] +pub struct HardwareIdentity { + pub hwid: String, + pub local_id: String, +} +pub fn identify(identity: &crate::workflow::Identity) -> Result { + let report = serde_json::json!({"system_uuid": identity.uuid, "serial_number": identity.serial, + "manufacturer": identity.manufacturer}); + let output = Command::new("python3") + .args(["-c", BRIDGE, "--identity-json", &report.to_string()]) + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).trim().to_owned()); + } + serde_json::from_slice(&output.stdout).map_err(|e| e.to_string()) +} + +pub fn station( + data: &Path, + action: &str, + report: Option<&Path>, +) -> Result { + let mut args = vec!["--station", action]; + let path = report.map(|p| p.to_string_lossy().into_owned()); + if let Some(p) = &path { + args.extend(["--report", p]); + } + serde_json::from_str(&call(data, &args)?).map_err(|e| e.to_string()) +} diff --git a/gui/src/main.rs b/gui/src/main.rs new file mode 100644 index 0000000..ae5ef4e --- /dev/null +++ b/gui/src/main.rs @@ -0,0 +1,1936 @@ +use eframe::egui::{self, Color32}; +use serde_json::Value; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, + sync::mpsc::{self, Receiver}, + thread, + time::{Duration, Instant}, +}; +mod diagnostics; +mod inventory; +mod report; +mod wifi; +mod workflow; +use workflow::{Identity, Inspection, Outcome, Settings}; +const BUILD: &str = env!("CSR_BUILD_ID"); +const GREEN: Color32 = Color32::from_rgb(70, 218, 168); +const ORANGE: Color32 = Color32::from_rgb(246, 188, 101); +fn tr(en: bool, de: &'static str, english: &'static str) -> &'static str { + if en { + english + } else { + de + } +} +#[derive(Clone, Copy, PartialEq)] +enum Page { + Intake, + Inspection, + Repairs, + Reports, + Queue, + Settings, +} +enum Work { + Scan(Result<(PathBuf, Value), String>), + Diagnostic(String, diagnostics::Diagnostic), + Queue(Result, String>), + Sync(Result), + InventoryCheck(Result), + Station(String, Result), + Power(Result<(), String>), +} +#[derive(Default)] +struct Interactive { + key: String, + colors_seen: Vec, + color: usize, + keys: Vec, + moves: u64, + clicks: u64, + scrolls: u64, +} +struct App { + data: PathBuf, + page: Page, + asset: String, + identity: Identity, + hardware_id: String, + duplicates: Vec, + known: Vec, + inspection: Option, + settings: Settings, + settings_draft: Settings, + status: String, + receiver: Option>, + wifi: wifi::Wifi, + settings_wifi: wifi::Wifi, + interactive: Option, + queue: Vec, + auto_sync: bool, + next_sync: Instant, + repair_key: String, + repair_text: String, + export_path: Option, + dirty: bool, + last_edit: Instant, + storage_error: bool, + station_device: Option, + boot_pending: bool, + flush_pending: bool, + intake_upload_pending: bool, + inspection_upload_pending: bool, + automatic_scan: bool, + station_ready: bool, + next_station_attempt: Instant, +} +fn scanner_path() -> PathBuf { + if let Some(p) = std::env::var_os("CSR_SCANNER") { + return PathBuf::from(p); + } + // Prefer the scanner beside this build, then the installed live-system scanner. + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + let sibling = parent.join("pc-intake"); + if sibling.exists() { + return sibling; + } + } + } + let local = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scanner/target/release/pc-intake"); + if local.exists() { + local + } else { + PathBuf::from("/usr/local/bin/pc-intake") + } +} +fn setup(data: &Path) -> Result<(), String> { + for dir in [ + "database", + "reports", + "checklists", + "config", + "inspections", + "final-reports", + ] { + fs::create_dir_all(data.join(dir)).map_err(|e| e.to_string())?; + } + workflow::atomic_write(&data.join(".write-test"), b"ok")?; + fs::remove_file(data.join(".write-test")).map_err(|e| e.to_string())?; + workflow::init_index(data) +} +fn label(key: &str, en: bool) -> String { + match key { + "display" => tr(en, "Bildschirm", "Display"), + "keyboard" => tr(en, "Tastatur", "Keyboard"), + "touchpad" => tr(en, "Touchpad", "Touchpad"), + "wifi" => tr(en, "WLAN", "Wi-Fi"), + "audio" => tr(en, "Lautsprecher", "Speakers"), + "microphone" => tr(en, "Mikrofon", "Microphone"), + "webcam" => tr(en, "Kamera", "Camera"), + "usb_ports" => tr(en, "USB-Anschlüsse", "USB ports"), + "charging" => tr(en, "Akku und Laden", "Battery and charging"), + "thermals" => tr(en, "Temperaturen", "Thermals"), + "stability" => tr(en, "Stabilität", "Stability"), + _ => { + return format!( + "{} {}", + tr(en, "Laufwerk", "Drive"), + key.trim_start_matches("disk:") + ) + } + } + .into() +} +fn guidance(key: &str, en: bool) -> &'static str { + match key { + "display"=>tr(en,"Alle Farben auf Pixelfehler, Flecken und Flackern prüfen. Pfeiltasten wechseln die Farbe.","Inspect every colour for dead pixels, marks and flicker. Arrow keys change the colour."), + "keyboard"=>tr(en,"Jede Taste drücken. Erkannte Tasten erscheinen im Test. Fn-Tasten zusätzlich manuell prüfen.","Press every key. Recognised keys appear in the test. Check Fn keys manually too."), + "touchpad"=>tr(en,"Im Testfeld nur das Touchpad verwenden. Bewegung, beide Tasten und Scrollen prüfen. Mausereignisse beweisen keine Touchpad-Funktion.","Use only the touchpad inside the test area. Check movement, both buttons and scrolling. Mouse events alone do not prove the touchpad works."), + "wifi"=>tr(en,"LAN-Kabel abziehen, WLAN wählen, verbinden und Datenübertragung testen.","Unplug Ethernet, select Wi-Fi, connect and test data transfer."), + "audio"=>tr(en,"Linken und rechten Kanal anhören. Beide müssen klar und ohne Verzerrung sein.","Listen to the left and right channels. Both must be clear without distortion."), + "microphone"=>tr(en,"Einen Satz sprechen, danach die Wiedergabe auf Lautstärke und Klarheit prüfen.","Speak a sentence, then check the playback for volume and clarity."), + "webcam"=>tr(en,"Im Vorschaufenster Bild, Schärfe und Aussetzer prüfen. Fenster schließt nach der eingestellten Dauer.","Check the preview for an image, focus and dropouts. It closes after the configured duration."), + "usb_ports"=>tr(en,"Erste Geräteliste erfassen. Einen bekannten USB-Stick in jeden Anschluss stecken und jeweils erneut prüfen. Änderungen werden angezeigt; Anschluss in den Notizen festhalten.","Capture the initial device list. Insert a known USB device into each port and rerun. Changes are shown; record each port in the notes."), + "charging"=>tr(en,"Mit Netzteil messen, danach ohne Netzteil erneut messen. Ladezustand und Betrieb prüfen. Akkugesundheit steht im Scanbericht.","Measure with the charger connected, then unplug it and rerun. Check charging state and operation. Battery health appears in the scan report."), + "thermals"=>tr(en,"Lasttest starten und Lüfter beobachten. Messwerte und Werkstattgrenze prüfen. Hohe Temperatur erfordert Prüfung, nicht automatisch einen Hardwaredefekt.","Run the load test and observe the fans. Review measurements against the workshop threshold. High temperature needs investigation, not an automatic hardware-failure verdict."), + "stability"=>tr(en,"CPU-/RAM-Test starten. Auf Absturz, Neustart und Fehler achten. Dies ersetzt keinen vollständigen Offline-RAM-Test.","Run the CPU/RAM test. Watch for crashes, resets and errors. This does not replace a complete offline memory test."), + _=>tr(en,"SMART-Daten dieses Laufwerks lesen und Warnungen prüfen. Der Test verändert keine Laufwerksdaten.","Read this drive's SMART data and review warnings. The test does not modify drive data."), +} +} +impl App { + fn new(data: PathBuf) -> Self { + let init = setup(&data); + let loaded = Settings::load(&data); + let status = match (&init, &loaded) { + (Err(e), _) | (_, Err(e)) => format!("Storage/settings error / Speicherfehler: {e}"), + _ => "Ready / Bereit".into(), + }; + let settings = loaded.unwrap_or_default(); + let known = workflow::known_assets(&data); + let identity = Identity::detect(); + let duplicates = workflow::duplicates(&data, "", &identity); + let identification = inventory::identify(&identity); + let hardware_id = identification + .as_ref() + .map(|i| i.hwid.clone()) + .unwrap_or_default(); + let auto_asset = duplicates + .first() + .cloned() + .unwrap_or_else(|| identification.map(|i| i.local_id).unwrap_or_default()); + let mut wifi = wifi::Wifi::default(); + wifi.set_test_url(&settings.test_url); + let flush_pending = fs::read_dir(data.join("outbox/inspections")) + .map(|mut it| it.next().is_some()) + .unwrap_or(false); + let boot_pending = data.join("config/inventory.json").exists() && !hardware_id.is_empty(); + let mut settings_wifi = wifi::Wifi::default(); + settings_wifi.configure_saved(&data); + settings_wifi.reconnect_saved(); + Self { + data, + page: Page::Intake, + asset: auto_asset, + identity, + hardware_id, + duplicates, + known, + inspection: None, + settings_draft: settings.clone(), + settings, + status, + receiver: None, + wifi, + settings_wifi, + interactive: None, + queue: Vec::new(), + auto_sync: false, + next_sync: Instant::now(), + repair_key: "display".into(), + repair_text: String::new(), + export_path: None, + dirty: false, + last_edit: Instant::now(), + storage_error: init.is_err(), + station_device: None, + boot_pending, + flush_pending, + intake_upload_pending: false, + inspection_upload_pending: false, + automatic_scan: false, + station_ready: false, + next_station_attempt: Instant::now(), + } + } + fn busy(&self) -> bool { + self.receiver.is_some() + || self.wifi.busy() + || self.settings_wifi.busy() + || self.interactive.is_some() + } + fn en(&self) -> bool { + self.settings.language == "en" + } + fn changed(&mut self) { + if let Some(s) = &mut self.inspection { + if !s.sync_phase.is_empty() { + s.sync_phase = "IN_PROGRESS".into(); + } + s.changed(); + } + self.dirty = true; + self.last_edit = Instant::now(); + self.export_path = None; + } + fn persist(&mut self) -> bool { + if let Some(s) = &self.inspection { + match s.save(&self.data) { + Ok(()) => { + self.dirty = false; + self.storage_error = false; + if !s.sync_phase.is_empty() { + self.inspection_upload_pending = true; + } + true + } + Err(e) => { + self.status = format!("Save failed / Speichern fehlgeschlagen: {e}"); + self.storage_error = true; + false + } + } + } else { + true + } + } + fn open_asset(&mut self, resume: bool) { + if self.busy() || !workflow::valid_id(&self.asset) { + return; + } + if self.dirty && !self.persist() { + return; + } + let path = self + .data + .join("inspections") + .join(format!("{}.json", self.asset)); + let result = if path.exists() { + Inspection::load(&self.data, &self.asset) + } else if resume { + Ok(workflow::migrate( + &self.data, + &self.asset, + self.settings.clone(), + )) + } else { + Ok(Inspection::new( + &self.asset, + self.identity.clone(), + self.settings.clone(), + )) + }; + match result { + Ok(mut s) => { + if s.settings != self.settings { + s.settings = self.settings.clone(); + s.changed(); + } + self.repair_key = s.order.first().cloned().unwrap_or_else(|| "display".into()); + self.repair_text.clear(); + self.inspection = Some(s); + self.export_path = None; + self.wifi = wifi::Wifi::default(); + self.wifi.set_test_url(&self.settings.test_url); + if self.persist() { + self.page = Page::Inspection; + self.status = "Inspection loaded / Prüfung geladen".into(); + } + } + Err(e) => self.status = e, + } + } + fn start_scan(&mut self, full: bool) { + if self.busy() || self.inspection.is_none() { + return; + } + if let Some(s) = &mut self.inspection { + s.changed(); + } + self.export_path = None; + if !self.persist() { + return; + } + let data = self.data.clone(); + let asset = self.inspection.as_ref().unwrap().asset.clone(); + let scanner = scanner_path(); + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + self.status = "Hardware scan running / Hardware-Scan läuft …".into(); + thread::spawn(move || { + let result = (|| { + let mut command = Command::new("timeout"); + command + .args(["--kill-after=5s", "600s"]) + .arg(scanner) + .args(["--asset-id", &asset, "--output"]) + .arg(data.join("reports")); + command.arg(if full { "--compile" } else { "--quick" }); + let out = command.output().map_err(|e| format!("Scanner: {e}"))?; + if !out.status.success() { + return Err(String::from_utf8_lossy(&out.stderr).into_owned()); + } + workflow::latest_report(&data, &asset) + .ok_or_else(|| "No valid report produced".into()) + })(); + let _ = tx.send(Work::Scan(result)); + }); + } + fn start_test(&mut self, key: &str) { + if self.busy() || self.inspection.is_none() { + return; + } + let s = self.inspection.as_mut().unwrap(); + s.settings = self.settings.clone(); + s.record( + key, + "started", + Outcome::NotTested, + "Test started / Test gestartet".into(), + ); + self.export_path = None; + if !self.persist() { + return; + } + if ["display", "keyboard", "touchpad"].contains(&key) { + self.interactive = Some(Interactive { + key: key.into(), + colors_seen: vec![0], + ..Default::default() + }); + return; + } + let settings = self.settings.clone(); + let key = key.to_owned(); + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + self.status = "Test running / Test läuft …".into(); + thread::spawn(move || { + let result = diagnostics::run(&key, &settings); + let _ = tx.send(Work::Diagnostic(key, result)); + }); + } + fn record_diagnostic(&mut self, key: &str, mut result: diagnostics::Diagnostic) { + if let Some(s) = &mut self.inspection { + if ["usb_ports", "charging"].contains(&key) { + if let Some(previous) = s + .checks + .get(key) + .and_then(|c| c.history.iter().rev().find(|e| e.kind == "diagnostic")) + { + let added: Vec<_> = result + .text + .lines() + .filter(|line| !previous.text.lines().any(|p| p == *line)) + .collect(); + self.status = format!("Changed / Geändert: {}", added.join(" · ")); + } + } + if result.text.len() > 100_000 { + result + .text + .truncate(result.text.floor_char_boundary(100_000)); + } + s.build = BUILD.into(); + s.record(key, "diagnostic", result.outcome, result.text); + } + self.export_path = None; + self.persist(); + } + fn refresh_queue(&mut self) { + if self.busy() { + return; + } + let data = self.data.clone(); + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + thread::spawn(move || { + let _ = tx.send(Work::Queue(inventory::queue(&data))); + }); + } + fn upload(&mut self, retry: bool) { + if self.busy() { + return; + } + let data = self.data.clone(); + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + self.status = "Uploading / Upload läuft …".into(); + thread::spawn(move || { + let _ = tx.send(Work::Sync(inventory::sync(&data, retry))); + }); + } + fn poll(&mut self) { + self.settings_wifi.poll(); + // Settings connectivity checks never become inspection results. + let _ = self.settings_wifi.take_test_log(); + self.wifi.poll(); + if let Some(log) = self.wifi.take_test_log() { + self.record_diagnostic( + "wifi", + diagnostics::Diagnostic { + outcome: if log.contains("STATUS: FEHLER") { + Outcome::SetupProblem + } else { + Outcome::NeedsReview + }, + text: log, + }, + ); + } + let event = self.receiver.as_ref().map(|r| r.try_recv()); + match event { + Some(Ok(work)) => { + self.receiver = None; + match work { + Work::Scan(Ok((p, v))) => { + if let Some(s) = &mut self.inspection { + s.set_report(p, v); + } + self.persist(); + self.known = workflow::known_assets(&self.data); + self.duplicates = + workflow::duplicates(&self.data, &self.asset, &self.identity); + if !self.storage_error { + self.status = "Scan saved / Scan gespeichert".into(); + } + if self.automatic_scan { + self.automatic_scan = false; + self.intake_upload_pending = true; + self.next_station_attempt = Instant::now(); + } else { + self.refresh_queue(); + } + } + Work::Scan(Err(e)) => { + self.automatic_scan = false; + self.status = format!("Scan failed / Scan fehlgeschlagen: {e}") + } + Work::Diagnostic(key, result) => { + self.status = format!( + "{}: {}", + label(&key, self.en()), + result.outcome.label(self.en()) + ); + self.record_diagnostic(&key, result); + } + Work::Queue(Ok(q)) => self.queue = q, + Work::Queue(Err(e)) => self.status = e, + Work::Power(result) => { + if let Err(e) = result { + self.status = e; + } + } + Work::Station(action, result) => match result { + Ok(result) => { + self.flush_pending |= result["pending"].as_u64().unwrap_or(0) > 0; + let device = result["device"].clone(); + let intake = device["stage"] == "INTAKE"; + self.station_device = Some(device); + self.status = tr( + self.en(), + "Mit Inventory synchronisiert", + "Synced with inventory", + ) + .into(); + self.station_ready = action != "boot" || !intake; + if action == "boot" && intake { + self.automatic_scan = true; + self.start_scan(false); + } + } + Err(e) => { + self.status = format!( + "{}: {e}", + tr( + self.en(), + "Offline / Synchronisierung ausstehend", + "Offline / sync pending" + ) + ); + self.next_station_attempt = Instant::now() + Duration::from_secs(20); + match action.as_str() { + "boot" => self.boot_pending = true, + "intake" => self.intake_upload_pending = true, + "flush" => self.flush_pending = true, + _ => self.inspection_upload_pending = true, + } + } + }, + Work::InventoryCheck(result) => self.status = result.unwrap_or_else(|e| e), + Work::Sync(result) => { + self.status = result.unwrap_or_else(|e| e); + self.next_sync = + Instant::now() + Duration::from_secs(self.settings.retry_seconds); + self.refresh_queue(); + } + } + } + Some(Err(mpsc::TryRecvError::Disconnected)) => { + self.receiver = None; + self.status = + "Worker interrupted; retry / Aktion unterbrochen; erneut versuchen".into(); + } + _ => {} + } + if self.dirty && self.last_edit.elapsed() > Duration::from_millis(400) { + self.persist(); + } + if !self.busy() && !self.dirty && Instant::now() >= self.next_station_attempt { + if self.boot_pending { + self.boot_pending = false; + if self.inspection.is_none() { + self.open_asset(true); + self.page = Page::Intake; + } + self.station_action("boot"); + } else if self.intake_upload_pending { + self.intake_upload_pending = false; + self.station_action("intake"); + } else if self.flush_pending { + self.flush_pending = false; + self.station_action("flush"); + } else if self.inspection_upload_pending { + self.inspection_upload_pending = false; + let phase = self + .inspection + .as_ref() + .map(|s| s.sync_phase.as_str()) + .unwrap_or(""); + let action = match phase { + "STARTED" => "start", + "COMPLETE" => "complete", + _ => "progress", + }; + self.station_action(action); + } + } + if self.auto_sync && !self.busy() && Instant::now() >= self.next_sync { + self.next_sync = Instant::now() + Duration::from_secs(self.settings.retry_seconds); + self.upload(false); + } + } + fn station_action(&mut self, action: &str) { + if self.busy() { + return; + } + let data = self.data.clone(); + let path = if action == "intake" { + self.inspection.as_ref().and_then(|s| s.report_path.clone()) + } else if ["start", "progress", "complete"].contains(&action) { + self.inspection + .as_ref() + .map(|s| data.join("inspections").join(format!("{}.json", s.asset))) + } else { + None + }; + let action = action.to_owned(); + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + self.status = tr( + self.en(), + "Inventory wird synchronisiert …", + "Syncing inventory …", + ) + .into(); + thread::spawn(move || { + let result = inventory::station(&data, &action, path.as_deref()); + let _ = tx.send(Work::Station(action, result)); + }); + } + fn begin_inspection(&mut self) { + if self.busy() { + return; + } + if self.inspection.is_none() { + self.open_asset(true); + } + if let Some(s) = &mut self.inspection { + if s.sync_phase.is_empty() { + s.sync_phase = "STARTED".into(); + s.changed(); + } + } + self.persist(); + self.page = Page::Inspection; + if self.inspection.as_ref().is_some_and(|s| s.report.is_none()) { + self.start_scan(false); + } + } + fn intake_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + ui.add_space(12.0); + ui.heading(tr(en, "Gerät bereitmachen", "Prepare this device")); + ui.label(tr( + en, + "Erkennen → Erfassen → Weiter zum nächsten Gerät oder prüfen", + "Identify → Capture → Move to the next device or inspect", + )); + ui.add_space(24.0); + ui.columns(2, |cols| { + egui::Frame::group(cols[0].style()) + .inner_margin(20.0) + .show(&mut cols[0], |ui| { + ui.small(tr(en, "DIESES GERÄT", "THIS DEVICE")); + ui.heading(format!( + "{} {}", + self.identity.manufacturer, self.identity.model + )); + if let Some(d) = &self.station_device { + ui.heading(format!("#{}", d["number"])); + let stage = match d["stage"].as_str().unwrap_or("") { + "INTAKE" => "Eingang", + "INSPECTION" => "Inspektion", + "REPAIR" => "Reparatur", + "STORAGE" => "Lagerung", + "ISSUED" => "Ausgegeben", + _ => "—", + }; + ui.label(format!("{}: {stage}", tr(en, "Arbeitsschritt", "Stage"))); + ui.label(format!( + "Status: {}", + d["status"]["label"].as_str().unwrap_or("—") + )); + ui.add_space(16.0); + ui.label(format!("RAM: {} GB", d["ram_gb"])); + ui.label(format!("SSD: {} GB · HDD: {} GB", d["ssd_gb"], d["hdd_gb"])); + } else { + ui.label(tr( + en, + "Inventory-Zuordnung ausstehend", + "Waiting for inventory lookup", + )); + } + ui.add_space(12.0); + egui::CollapsingHeader::new(tr(en, "Hardware-Kennung", "Hardware identity")) + .show(ui, |ui| { + ui.small(&self.hardware_id); + ui.small(&self.identity.serial); + }); + }); + egui::Frame::group(cols[1].style()) + .inner_margin(20.0) + .show(&mut cols[1], |ui| { + ui.small(tr(en, "NÄCHSTER SCHRITT", "NEXT STEP")); + if self.busy() { + ui.spinner(); + } + ui.label(if self.station_ready { + tr( + en, + "Gerätedaten synchronisiert.", + "Device information synced.", + ) + } else { + tr( + en, + "Neue Geräte werden automatisch erfasst und synchronisiert.", + "New devices are captured and synced automatically.", + ) + }); + ui.add_space(20.0); + let allowed_stage = self + .station_device + .as_ref() + .map(|d| { + matches!( + d["stage"].as_str(), + Some("INTAKE" | "INSPECTION" | "REPAIR") + ) + }) + .unwrap_or(true); + if ui + .add_enabled( + !self.busy() && !self.hardware_id.is_empty() && allowed_stage, + egui::Button::new(tr( + en, + "Prüfung starten / fortsetzen", + "Start / resume inspection", + )) + .min_size(egui::vec2(ui.available_width(), 48.0)), + ) + .clicked() + { + self.begin_inspection(); + } + ui.add_space(12.0); + if ui + .add_enabled( + !self.busy() + && self.station_ready + && !self.inspection_upload_pending + && !self.intake_upload_pending, + egui::Button::new(tr( + en, + "Fertig — herunterfahren", + "Done — shut down", + )) + .min_size(egui::vec2(ui.available_width(), 48.0)), + ) + .clicked() + { + if self.persist() { + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + thread::spawn(move || { + let result = Command::new("systemctl") + .arg("poweroff") + .output() + .map_err(|e| e.to_string()) + .and_then(|o| { + if o.status.success() { + Ok(()) + } else { + Err(String::from_utf8_lossy(&o.stderr).into_owned()) + } + }); + let _ = tx.send(Work::Power(result)); + }); + } + } + ui.add_space(16.0); + if !self.station_ready || self.inspection_upload_pending || self.intake_upload_pending || self.flush_pending { + ui.colored_label(ORANGE,tr(en,"Noch nicht vollständig synchronisiert. Ergebnisse bleiben auf diesem USB gespeichert.","Not fully synced yet. Results remain saved on this USB.")); + if ui.add_enabled(!self.busy(),egui::Button::new(tr(en,"Offline speichern und herunterfahren","Save offline and shut down"))).clicked() && self.persist() { + let data=self.data.clone(); + let inspection=self.inspection.as_ref().filter(|s|!s.sync_phase.is_empty()).map(|s|data.join("inspections").join(format!("{}.json",s.asset))); + let (tx,rx)=mpsc::channel();self.receiver=Some(rx); + thread::spawn(move || { + let result=(|| { + if let Some(path)=inspection {inventory::station(&data,"queue",Some(&path))?;} + let output=Command::new("systemctl").arg("poweroff").output().map_err(|e|e.to_string())?; + if output.status.success(){Ok(())}else{Err(String::from_utf8_lossy(&output.stderr).into_owned())} + })();let _=tx.send(Work::Power(result)); + }); + } + } + if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr( + en, + "Verbindung erneut prüfen", + "Retry connection", + )), + ) + .clicked() + { + self.boot_pending = true; + self.next_station_attempt = Instant::now(); + } + if ui + .button(tr(en, "WLAN / Einstellungen", "Wi-Fi / settings")) + .clicked() + { + self.page = Page::Settings; + } + if self.hardware_id.is_empty() { + ui.colored_label( + ORANGE, + tr( + en, + "Keine gültige Hardware-Kennung. Aktuellen USB neu starten.", + "No valid hardware identity. Reboot the updated USB.", + ), + ); + } + }); + }); + } + fn inspection_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + let Some(s) = self.inspection.as_ref() else { + ui.label(tr(en, "Zuerst ein Gerät erfassen.", "Open a device first.")); + return; + }; + let conflict = self.identity.conflicts(&s.identity); + let has_report = s.report.is_some(); + let asset = s.asset.clone(); + let index = s.step.min(s.order.len()); + let len = s.order.len(); + let key = s.order.get(index).cloned(); + ui.heading(format!( + "{} · {}", + tr(en, "Prüfung", "Inspection"), + self.station_device + .as_ref() + .and_then(|d| d["number"].as_u64()) + .map(|n| format!("#{n}")) + .unwrap_or(asset) + )); + if self + .inspection + .as_ref() + .is_some_and(|s| s.sync_phase.is_empty()) + { + if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr(en, "Prüfung starten", "Start inspection")), + ) + .clicked() + { + self.begin_inspection(); + } + return; + } + if conflict { + ui.colored_label(ORANGE,tr(en,"Dieser Bericht gehört zu anderer Hardware. Historie kann gelesen werden; Live-Tests sind gesperrt.","This record belongs to different hardware. History is available; live tests are disabled.")); + } + if !has_report { + ui.label(tr(en,"Zuerst Hardware erfassen. Modell, Seriennummer und Laufwerke werden automatisch erkannt.","First scan the hardware. Model, serial number and drives are detected automatically.")); + } + if ui + .add_enabled( + !self.busy() && !conflict, + egui::Button::new(if has_report { + tr(en, "Hardware erneut erfassen", "Rescan hardware") + } else { + tr(en, "Hardware erfassen", "Scan hardware") + }), + ) + .clicked() + { + self.start_scan(false); + } + if ui + .add_enabled( + !self.busy() && !conflict, + egui::Button::new(tr( + en, + "Vollständiger Scan mit CPU-/RAM-/Compile-Benchmarks", + "Full scan with CPU/RAM/compile benchmarks", + )), + ) + .clicked() + { + self.start_scan(true); + } + if !has_report { + return; + } + ui.add( + egui::ProgressBar::new(index as f32 / len.max(1) as f32).text(format!( + "{} / {}", + index + 1, + len + 1 + )), + ); + if let Some(key) = key { + ui.heading(label(&key, en)); + ui.label(guidance(&key, en)); + let c = self.inspection.as_ref().unwrap().checks[&key].clone(); + if !c.suggestion.is_empty() { + ui.colored_label(ORANGE, &c.suggestion); + } + if key == "microphone" || key == "webcam" { + ui.label(format!( + "{} s", + if key == "microphone" { + self.settings.recording_seconds + } else { + self.settings.camera_seconds + } + )); + } + if key == "thermals" || key == "stability" { + ui.label(format!( + "{} s · {} °C", + self.settings.stress_seconds, self.settings.max_temperature_c + )); + } + ui.add_enabled_ui(!conflict, |ui| { + if key == "wifi" { + self.wifi.set_language(en); + let was_busy = self.wifi.busy(); + self.wifi.ui(ui, self.receiver.is_some()); + if !was_busy && self.wifi.busy() { + if let Some(s) = &mut self.inspection { + s.settings = self.settings.clone(); + s.record( + "wifi", + "started", + Outcome::NotTested, + "Wi-Fi action started / WLAN-Aktion gestartet".into(), + ); + } + self.export_path = None; + self.persist(); + } + } else if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr( + en, + "Test starten / erneut testen", + "Start test / retry", + )), + ) + .clicked() + { + self.start_test(&key); + } + }); + if !c.evidence.is_empty() { + egui::CollapsingHeader::new(tr( + en, + "Beobachtete Messwerte / Ausgabe", + "Observed measurements / output", + )) + .default_open(false) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut c.evidence.clone()) + .desired_rows(6) + .desired_width(f32::INFINITY) + .interactive(false), + ); + }); + } + ui.separator(); + ui.label(tr( + en, + "Ergebnis nach Prüfung bestätigen. Fehlendes Werkzeug ist kein Hardwaredefekt.", + "Confirm the result after inspection. A missing tool is not a hardware failure.", + )); + let old = c.outcome; + let mut chosen = old; + ui.add_enabled_ui(!self.busy(), |ui| { + ui.horizontal_wrapped(|ui| { + for outcome in [Outcome::Pass, Outcome::HardwareFailure] { + ui.selectable_value(&mut chosen, outcome, outcome.label(en)); + } + egui::ComboBox::from_id_salt("other-result") + .selected_text(tr(en, "Weiteres Ergebnis", "Other result")) + .show_ui(ui, |ui| { + for outcome in Outcome::ALL { + ui.selectable_value(&mut chosen, outcome, outcome.label(en)); + } + }); + }); + }); + if chosen != old { + let s = self.inspection.as_mut().unwrap(); + let notes = s.checks[&key].notes.clone(); + s.record(&key, "technician_result", chosen, notes); + self.export_path = None; + self.persist(); + } + ui.label(tr( + en, + "Notizen zu diesem Test (automatisch gespeichert)", + "Test notes (saved automatically)", + )); + if ui + .add( + egui::TextEdit::multiline( + &mut self + .inspection + .as_mut() + .unwrap() + .checks + .get_mut(&key) + .unwrap() + .notes, + ) + .desired_rows(3) + .desired_width(f32::INFINITY), + ) + .changed() + { + self.changed(); + } + egui::CollapsingHeader::new(tr(en, "Versuche und Reparaturen", "Attempts and repairs")) + .show(ui, |ui| { + for e in c.history.iter().rev() { + ui.label(format!( + "{} · {} · {}", + e.timestamp, + e.kind, + e.outcome.label(en) + )); + ui.label(&e.text); + ui.separator(); + } + }); + } else { + self.approval_ui(ui); + } + ui.separator(); + ui.add_enabled_ui(!self.busy(), |ui| { + ui.horizontal_wrapped(|ui| { + if ui + .add_enabled(index > 0, egui::Button::new(tr(en, "← Zurück", "← Back"))) + .clicked() + { + self.inspection.as_mut().unwrap().step = index - 1; + self.persist(); + } + if ui + .add_enabled(index < len, egui::Button::new(tr(en, "Weiter →", "Next →"))) + .clicked() + { + self.inspection.as_mut().unwrap().step = index + 1; + self.persist(); + } + if ui + .button(tr(en, "Zur Zusammenfassung", "Go to summary")) + .clicked() + { + self.inspection.as_mut().unwrap().step = len; + self.persist(); + } + }); + }); + } + fn approval_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + ui.heading(tr( + en, + "Zusammenfassung und Freigabe", + "Summary and approval", + )); + let s = self.inspection.as_ref().unwrap(); + for k in &s.order { + ui.label(format!( + "{}: {}", + label(k, en), + s.checks[k].outcome.label(en) + )); + } + let blockers = s.blockers(); + for b in &blockers { + ui.colored_label(ORANGE, b); + } + ui.label(tr(en, "Technikername", "Technician name")); + if ui + .text_edit_singleline(&mut self.inspection.as_mut().unwrap().technician) + .changed() + { + self.changed(); + } + ui.label(tr(en, "Gesamtnotizen", "Overall notes")); + if ui + .add( + egui::TextEdit::multiline(&mut self.inspection.as_mut().unwrap().notes) + .desired_rows(3), + ) + .changed() + { + self.changed(); + } + let complete = self.inspection.as_ref().is_some_and(|s| { + !s.technician.trim().is_empty() + && s.checks + .values() + .all(|c| !matches!(c.outcome, Outcome::NotTested | Outcome::NeedsReview)) + }); + if ui + .add_enabled( + complete && !self.busy(), + egui::Button::new(tr( + en, + "Prüfung abschließen und synchronisieren", + "Complete inspection and sync", + )), + ) + .clicked() + { + let s = self.inspection.as_mut().unwrap(); + s.sync_phase = "COMPLETE".into(); + s.changed(); + self.persist(); + self.next_station_attempt = Instant::now(); + self.page = Page::Intake; + } + let can_approve = blockers.is_empty() && !self.busy(); + if ui + .add_enabled( + can_approve, + egui::Button::new(tr(en, "Gerät ausdrücklich freigeben", "Approve device")), + ) + .clicked() + { + match self.inspection.as_mut().unwrap().approve() { + Ok(()) => { + self.persist(); + } + Err(e) => self.status = e, + } + } + if let Some(a) = &self.inspection.as_ref().unwrap().approval { + ui.colored_label( + GREEN, + format!( + "{}: {}", + tr(en, "Freigegeben durch", "Approved by"), + a.technician + ), + ); + } + if ui + .button(tr(en, "Druckbericht erstellen", "Create printable report")) + .clicked() + { + self.export_report(); + } + } + fn repairs_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + ui.heading(tr( + en, + "Reparaturen und Nachprüfung", + "Repairs and retesting", + )); + let Some(s) = self.inspection.as_ref() else { + ui.label(tr(en, "Zuerst ein Gerät öffnen.", "Open a device first.")); + return; + }; + let order = s.order.clone(); + egui::ComboBox::from_id_salt("repair-component") + .selected_text(label(&self.repair_key, en)) + .show_ui(ui, |ui| { + for k in &order { + ui.selectable_value(&mut self.repair_key, k.clone(), label(k, en)); + } + }); + ui.label(tr( + en, + "Reparatur, Ersatzteil und Techniker dokumentieren", + "Record the repair, replacement part and technician", + )); + ui.add(egui::TextEdit::multiline(&mut self.repair_text).desired_rows(4)); + if ui + .add_enabled( + !self.busy() && !self.repair_text.trim().is_empty(), + egui::Button::new(tr( + en, + "Reparatur speichern und Nachprüfung öffnen", + "Save repair and open retest", + )), + ) + .clicked() + { + let key = self.repair_key.clone(); + let text = std::mem::take(&mut self.repair_text); + let s = self.inspection.as_mut().unwrap(); + s.record(&key, "repair", Outcome::NotTested, text); + s.step = s.order.iter().position(|k| k == &key).unwrap_or(0); + self.export_path = None; + if self.persist() { + self.page = Page::Inspection; + } + } + let s = self.inspection.as_ref().unwrap(); + for k in &s.order { + let c = &s.checks[k]; + if c.outcome == Outcome::HardwareFailure || !c.history.is_empty() { + egui::CollapsingHeader::new(format!("{} · {}", label(k, en), c.outcome.label(en))) + .show(ui, |ui| { + for e in c.history.iter().rev() { + ui.label(format!( + "{} · {} · {}", + e.timestamp, + e.kind, + e.outcome.label(en) + )); + ui.label(&e.text); + } + }); + } + } + } + fn export_report(&mut self) { + if !self.persist() { + return; + } + if self.busy() { + self.status = "Wait for the running test / Laufenden Test abwarten".into(); + return; + } + if let Some(s) = &self.inspection { + match report::export(&self.data, s) { + Ok(p) => { + self.status = format!("Report / Bericht: {}", p.display()); + self.export_path = Some(p); + } + Err(e) => self.status = e, + } + } + } + fn reports_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + ui.heading(tr(en, "Berichte", "Reports")); + if self.inspection.is_none() { + ui.label(tr( + en, + "Gerät unter Erfassung öffnen, um Bericht und Historie zu laden.", + "Open a device in Intake to load its report and history.", + )); + return; + } + self.approval_ui(ui); + if let Some(p) = self.export_path.clone() { + ui.label(p.display().to_string()); + if ui + .button(tr( + en, + "Im Browser öffnen / drucken", + "Open in browser / print", + )) + .clicked() + { + if let Err(e) = Command::new("xdg-open").arg(&p).spawn() { + self.status = e.to_string(); + } + } + ui.label(tr( + en, + "Im Browser Drucken oder Als PDF speichern wählen.", + "Choose Print or Save as PDF in the browser.", + )); + } + if let Some(s) = &self.inspection { + if let Some(r) = &s.report { + egui::CollapsingHeader::new(tr(en, "Hardwaredaten", "Hardware data")).show( + ui, + |ui| { + ui.monospace(serde_json::to_string_pretty(r).unwrap_or_default()); + }, + ); + } + } + } + fn queue_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + ui.heading(tr(en, "Synchronisierung", "Synchronization")); + let pending = fs::read_dir(self.data.join("outbox/inspections")) + .map(|r| r.count()) + .unwrap_or(0); + ui.label(format!( + "{}: {pending}", + tr( + en, + "Ausstehende Prüfungsereignisse", + "Pending inspection events" + ) + )); + if ui + .add_enabled( + !self.busy() && pending > 0, + egui::Button::new(tr( + en, + "Prüfungen jetzt synchronisieren", + "Sync inspections now", + )), + ) + .clicked() + { + self.flush_pending = true; + self.next_station_attempt = Instant::now(); + } + ui.label(tr(en,"Berichte bleiben offline gespeichert. Der Server ordnet per HWID zu und vergibt neue Gerätenummern automatisch. Konfiguration: config/inventory.json und inventory-api-key.","Reports remain saved offline. The server matches hardware IDs and assigns new device numbers automatically. Configure config/inventory.json and inventory-api-key.")); + ui.horizontal_wrapped(|ui| { + if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr(en, "Aktualisieren", "Refresh")), + ) + .clicked() + { + self.refresh_queue(); + } + if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr( + en, + "Ausstehende jetzt hochladen / erneut versuchen", + "Upload pending / retry now", + )), + ) + .clicked() + { + self.upload(true); + } + }); + if ui + .checkbox( + &mut self.auto_sync, + tr( + en, + "Automatisch versuchen, solange diese App läuft", + "Retry automatically while this app is running", + ), + ) + .changed() + { + self.next_sync = Instant::now(); + } + for q in &self.queue { + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.strong(format!( + "{} · {} · {} {}", + q.asset_id, + q.status, + q.attempts, + tr(en, "Versuche", "attempts") + )); + if let Some(number) = q.inventory_number { + ui.label(format!( + "{}: {number}", + tr(en, "Inventory-Gerätenummer", "Inventory device number") + )); + } + if !q.error.is_empty() { + ui.colored_label(ORANGE, &q.error); + } + ui.small(format!( + "{} · Next retry UTC Unix: {}", + q.report_path, q.next_attempt + )); + }); + } + if self.queue.is_empty() { + ui.label(tr( + en, + "Keine Einträge. Aktualisieren sucht auch ältere Berichte.", + "No entries. Refresh also discovers older reports.", + )); + } + } + fn settings_ui(&mut self, ui: &mut egui::Ui) { + let en = self.en(); + ui.heading(tr(en, "Werkstatteinstellungen", "Workshop settings")); + ui.label(format!("Build: {BUILD}")); + ui.label(self.data.display().to_string()); + egui::CollapsingHeader::new(tr(en, "WLAN verbinden", "Connect Wi-Fi")) + .default_open(true) + .show(ui, |ui| { + self.settings_wifi.set_language(en); + self.settings_wifi.set_test_url(&self.settings.test_url); + self.settings_wifi + .ui(ui, self.receiver.is_some() || self.wifi.busy()); + }); + ui.separator(); + ui.strong(tr(en, "Inventory-Verbindung", "Inventory connection")); + let config = fs::read(self.data.join("config/inventory.json")) + .ok() + .and_then(|b| serde_json::from_slice::(&b).ok()); + ui.label( + config + .as_ref() + .and_then(|v| v.get("url")) + .and_then(Value::as_str) + .unwrap_or(tr( + en, + "Kein Inventory-Server konfiguriert", + "No inventory server configured", + )), + ); + if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr(en, "API-Verbindung prüfen", "Check API connection")), + ) + .clicked() + { + let data = self.data.clone(); + let (tx, rx) = mpsc::channel(); + self.receiver = Some(rx); + self.status = tr( + en, + "API-Verbindung wird geprüft …", + "Checking API connection …", + ) + .into(); + thread::spawn(move || { + let _ = tx.send(Work::InventoryCheck(inventory::check(&data))); + }); + } + ui.small(tr( + en, + "Prüft HTTPS und API-Schlüssel, ohne Berichte hochzuladen.", + "Checks HTTPS and the API key without uploading reports.", + )); + ui.separator(); + egui::CollapsingHeader::new(tr( + en, + "Weitere Werkstatteinstellungen", + "Advanced workshop settings", + )) + .show(ui, |ui| { + egui::ComboBox::from_id_salt("language") + .selected_text(&self.settings_draft.language) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.settings_draft.language, "de".into(), "Deutsch"); + ui.selectable_value(&mut self.settings_draft.language, "en".into(), "English"); + }); + ui.label(tr(en, "WLAN-Testserver", "Wi-Fi test server")); + ui.text_edit_singleline(&mut self.settings_draft.test_url); + ui.horizontal(|ui| { + ui.label(tr( + en, + "CPU-/RAM-Testdauer (Sekunden)", + "CPU/RAM duration (seconds)", + )); + ui.add( + egui::DragValue::new(&mut self.settings_draft.stress_seconds).range(10..=600), + ); + }); + ui.horizontal(|ui| { + ui.label(tr( + en, + "Mikrofonaufnahme (Sekunden)", + "Microphone recording (seconds)", + )); + ui.add( + egui::DragValue::new(&mut self.settings_draft.recording_seconds).range(1..=30), + ); + }); + ui.horizontal(|ui| { + ui.label(tr( + en, + "Kameravorschau (Sekunden)", + "Camera preview (seconds)", + )); + ui.add( + egui::DragValue::new(&mut self.settings_draft.camera_seconds).range(5..=120), + ); + }); + ui.horizontal(|ui| { + ui.label(tr(en, "Temperaturgrenze °C", "Temperature threshold °C")); + ui.add( + egui::DragValue::new(&mut self.settings_draft.max_temperature_c) + .range(40.0..=110.0), + ); + }); + ui.horizontal(|ui| { + ui.label(tr( + en, + "Minimale Akkugesundheit %", + "Minimum battery health %", + )); + ui.add( + egui::DragValue::new(&mut self.settings_draft.min_battery_health_pct) + .range(0..=100), + ); + }); + ui.horizontal(|ui| { + ui.label(tr( + en, + "Upload-Wiederholung (Sekunden)", + "Upload retry interval (seconds)", + )); + ui.add( + egui::DragValue::new(&mut self.settings_draft.retry_seconds).range(10..=3600), + ); + }); + if ui + .add_enabled( + !self.busy(), + egui::Button::new(tr(en, "Einstellungen speichern", "Save settings")), + ) + .clicked() + { + let result = self.settings_draft.validate().and_then(|_| { + workflow::save_json( + &self.data.join("config/workshop.json"), + &self.settings_draft, + ) + }); + match result { + Ok(()) => { + self.settings = self.settings_draft.clone(); + self.wifi.set_test_url(&self.settings.test_url); + if let Some(s) = &mut self.inspection { + s.settings = self.settings.clone(); + s.changed(); + } + self.persist(); + if !self.storage_error { + self.status = "Settings saved / Einstellungen gespeichert".into(); + } + } + Err(e) => self.status = e, + } + } + }); + } + fn display_ui(&mut self, ctx: &egui::Context) { + let en = self.en(); + let colors = [ + Color32::BLACK, + Color32::WHITE, + Color32::RED, + Color32::GREEN, + Color32::BLUE, + Color32::GRAY, + Color32::YELLOW, + ]; + let test = self.interactive.as_mut().unwrap(); + if ctx.input(|i| i.key_pressed(egui::Key::ArrowRight) || i.key_pressed(egui::Key::Space)) { + test.color = (test.color + 1) % colors.len(); + } + if ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { + test.color = (test.color + colors.len() - 1) % colors.len(); + } + if !test.colors_seen.contains(&test.color) { + test.colors_seen.push(test.color); + } + let color = colors[test.color]; + let ink = if matches!(test.color, 1 | 3 | 5 | 6) { + Color32::BLACK + } else { + Color32::WHITE + }; + let mut close = ctx.input(|i| i.key_pressed(egui::Key::Escape)); + ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); + egui::CentralPanel::default() + .frame(egui::Frame::none().fill(color)) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new(format!("{} / 7", test.color + 1)).color(ink)); + ui.label( + egui::RichText::new(tr( + en, + "← / → oder Leertaste: Farbe wechseln · Esc: beenden", + "← / → or Space: change colour · Esc: finish", + )) + .color(ink), + ); + if ui.button(tr(en, "Nächste Farbe", "Next colour")).clicked() { + test.color = (test.color + 1) % colors.len(); + } + if ui.button(tr(en, "Test beenden", "Finish test")).clicked() { + close = true; + } + }); + }); + if close { + let test = self.interactive.take().unwrap(); + ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); + self.record_diagnostic( + "display", + diagnostics::Diagnostic { + outcome: Outcome::NeedsReview, + text: format!("Colours viewed / Farben betrachtet: {:?}", test.colors_seen), + }, + ); + } + } + fn interactive_ui(&mut self, ctx: &egui::Context) { + if self.interactive.as_ref().map(|t| t.key.as_str()) == Some("display") { + ctx.request_repaint(); + return; + } + let en = self.en(); + let test = self.interactive.as_mut().unwrap(); + let key = test.key.clone(); + let mut close = ctx.input(|i| i.key_pressed(egui::Key::Escape)); + let colors = [ + Color32::BLACK, + Color32::WHITE, + Color32::RED, + Color32::GREEN, + Color32::BLUE, + Color32::GRAY, + Color32::YELLOW, + ]; + egui::Window::new(label(&key, en)) + .default_size([720.0, 500.0]) + .collapsible(false) + .show(ctx, |ui| { + ui.label(guidance(&key, en)); + if key == "display" { + if ctx.input(|i| { + i.key_pressed(egui::Key::ArrowRight) || i.key_pressed(egui::Key::Space) + }) { + test.color = (test.color + 1) % colors.len(); + } + if ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft)) { + test.color = (test.color + colors.len() - 1) % colors.len(); + } + ui.horizontal_wrapped(|ui| { + for (i, c) in colors.iter().enumerate() { + if ui + .add(egui::Button::new(format!("{}", i + 1)).fill(*c)) + .clicked() + { + test.color = i; + } + } + }); + if !test.colors_seen.contains(&test.color) { + test.colors_seen.push(test.color); + } + let (rect, _) = ui.allocate_exact_size( + egui::vec2(ui.available_width(), 320.0), + egui::Sense::hover(), + ); + ui.painter().rect_filled(rect, 0.0, colors[test.color]); + ui.label(format!("{} / 7", test.colors_seen.len())); + } else if key == "keyboard" { + for e in ctx.input(|i| i.events.clone()) { + if let egui::Event::Key { + key, pressed: true, .. + } = e + { + let k = format!("{key:?}"); + if !test.keys.contains(&k) { + test.keys.push(k); + } + } + } + ui.label(test.keys.join(" · ")); + } else { + let (rect, response) = ui.allocate_exact_size( + egui::vec2(ui.available_width(), 280.0), + egui::Sense::click_and_drag(), + ); + ui.painter() + .rect_stroke(rect, 8.0, egui::Stroke::new(2.0_f32, GREEN)); + if response.hovered() { + for e in ctx.input(|i| i.events.clone()) { + match e { + egui::Event::PointerMoved(p) => { + test.moves += 1; + ui.painter().circle_filled(p, 5.0, GREEN); + } + egui::Event::PointerButton { pressed: true, .. } => { + test.clicks += 1 + } + egui::Event::MouseWheel { .. } => test.scrolls += 1, + _ => {} + } + } + } + ui.label(format!( + "Movement / Bewegung: {} · Clicks: {} · Scroll: {}", + test.moves, test.clicks, test.scrolls + )); + } + if ui + .button(tr( + en, + "Test beenden und Beobachtung speichern", + "Finish and save observations", + )) + .clicked() + { + close = true; + } + }); + if close { + let t = self.interactive.take().unwrap(); + let text=match t.key.as_str(){"display"=>format!("Colours viewed / Farben betrachtet: {:?}",t.colors_seen),"keyboard"=>format!("Keys / Tasten: {}",t.keys.join(", ")),_=>format!("Pointer movements: {}; clicks: {}; scroll events: {}. Input source requires technician confirmation.",t.moves,t.clicks,t.scrolls)}; + self.record_diagnostic( + &t.key, + diagnostics::Diagnostic { + outcome: Outcome::NeedsReview, + text, + }, + ); + } + } +} +impl eframe::App for App { + fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { + self.poll(); + if self.interactive.as_ref().map(|t| t.key.as_str()) == Some("display") { + self.display_ui(ctx); + return; + } + let en = self.en(); + ctx.set_visuals(egui::Visuals::dark()); + if self.busy() + || self.dirty + || self.auto_sync + || self.boot_pending + || self.flush_pending + || self.intake_upload_pending + || self.inspection_upload_pending + { + ctx.request_repaint_after(Duration::from_millis(200)); + } + egui::TopBottomPanel::top("navigation").show(ctx, |ui| { + ui.add_space(8.0); + ui.horizontal(|ui| { + ui.heading("CS-R"); + ui.add_space(24.0); + for (page, de, english) in [ + (Page::Intake, "Gerät", "Device"), + (Page::Inspection, "Prüfung", "Inspection"), + (Page::Settings, "Einstellungen", "Settings"), + ] { + if ui + .selectable_label(self.page == page, tr(en, de, english)) + .clicked() + && self.persist() + { + self.page = page; + } + } + ui.menu_button(tr(en, "Mehr", "More"), |ui| { + for (page, de, english) in [ + (Page::Repairs, "Reparaturen", "Repairs"), + (Page::Reports, "Berichte", "Reports"), + (Page::Queue, "Synchronisierung", "Sync queue"), + ] { + if ui.button(tr(en, de, english)).clicked() && self.persist() { + self.page = page; + if page == Page::Queue { + self.refresh_queue(); + } + ui.close_menu(); + } + } + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.small(format!("v{}", env!("CARGO_PKG_VERSION"))); + }); + }); + ui.add_space(8.0); + }); + egui::TopBottomPanel::bottom("status").show(ctx, |ui| { + if self.busy() { + ui.spinner(); + } + ui.colored_label( + if self.storage_error { ORANGE } else { GREEN }, + &self.status, + ); + if self.dirty { + ui.label(tr( + en, + "Änderungen werden gespeichert …", + "Saving changes …", + )); + } + }); + egui::CentralPanel::default().show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| match self.page { + Page::Intake => self.intake_ui(ui), + Page::Inspection => self.inspection_ui(ui), + Page::Repairs => self.repairs_ui(ui), + Page::Reports => self.reports_ui(ui), + Page::Queue => self.queue_ui(ui), + Page::Settings => self.settings_ui(ui), + }); + }); + if self.interactive.is_some() { + self.interactive_ui(ctx); + } + } + fn on_exit(&mut self, _: Option<&eframe::glow::Context>) { + self.persist(); + } +} +fn main() -> eframe::Result<()> { + let args: Vec = std::env::args().collect(); + if args.iter().any(|a| a == "--version") { + println!("CS-R Intake {} · {BUILD}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + let data = args + .iter() + .position(|a| a == "--data") + .and_then(|i| args.get(i + 1)) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/mnt/intake-data")); + eframe::run_native( + "CS-R Intake", + eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([1180.0, 800.0]) + .with_min_inner_size([860.0, 600.0]), + ..Default::default() + }, + Box::new(move |_| Ok(Box::new(App::new(data)))), + ) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn layouts_for_all_pages_and_steps_stay_finite() { + let root = + std::env::temp_dir().join(format!("csr-ui-{}-{}", std::process::id(), workflow::now())); + let mut app = App::new(root.clone()); + let mut s = Inspection::new("PC-1", Identity::default(), Settings::default()); + s.report = Some(serde_json::json!({})); + s.sync_phase = "IN_PROGRESS".into(); + app.inspection = Some(s); + for language in ["de", "en"] { + app.settings.language = language.into(); + for width in [860.0, 1180.0] { + let ctx = egui::Context::default(); + for page in [ + Page::Intake, + Page::Inspection, + Page::Repairs, + Page::Reports, + Page::Queue, + Page::Settings, + ] { + app.page = page; + for step in 0..=workflow::BASE.len() { + app.inspection.as_mut().unwrap().step = step; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(width, 600.0), + )), + events: vec![egui::Event::PointerMoved(egui::pos2(300.0, 300.0))], + ..Default::default() + }, + |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + match page { + Page::Intake => app.intake_ui(ui), + Page::Inspection => app.inspection_ui(ui), + Page::Repairs => app.repairs_ui(ui), + Page::Reports => app.reports_ui(ui), + Page::Queue => app.queue_ui(ui), + Page::Settings => app.settings_ui(ui), + } + assert!(ui.min_rect().is_finite()); + }); + }, + ); + } + } + } + } + fs::remove_dir_all(root).unwrap(); + } +} + +#[cfg(test)] +mod fullscreen_tests { + use super::*; + #[test] + fn colour_test_records_observations_and_returns_to_inspection() { + let root = std::env::temp_dir().join(format!( + "csr-colours-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let mut app = App::new(root.clone()); + app.inspection = Some(Inspection::new( + "PC-1", + Identity::default(), + Settings::default(), + )); + app.start_test("display"); + let ctx = egui::Context::default(); + for colour in 0..7 { + app.interactive.as_mut().unwrap().color = colour; + let _ = ctx.run( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(1180.0, 800.0), + )), + ..Default::default() + }, + |ctx| app.display_ui(ctx), + ); + } + let _ = ctx.run( + egui::RawInput { + events: vec![egui::Event::Key { + key: egui::Key::Escape, + physical_key: None, + pressed: true, + repeat: false, + modifiers: egui::Modifiers::default(), + }], + ..Default::default() + }, + |ctx| app.display_ui(ctx), + ); + assert!(app.interactive.is_none()); + let saved = Inspection::load(&root, "PC-1").unwrap(); + assert_eq!(saved.checks["display"].outcome, Outcome::NeedsReview); + assert!(saved.checks["display"] + .evidence + .contains("0, 1, 2, 3, 4, 5, 6")); + fs::remove_dir_all(root).unwrap(); + } +} diff --git a/gui/src/report.rs b/gui/src/report.rs new file mode 100644 index 0000000..5045fbc --- /dev/null +++ b/gui/src/report.rs @@ -0,0 +1,187 @@ +use crate::workflow::{self, Inspection}; +use serde_json::Value; +use std::path::{Path, PathBuf}; +fn escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} +fn value(v: &Value, k: &str) -> String { + escape(&match &v[k] { + Value::String(s) => s.clone(), + Value::Null => "Unknown / Unbekannt".into(), + v => v.to_string(), + }) +} +pub fn html(s: &Inspection) -> String { + let en = s.settings.language == "en"; + let title = if en { + "Inspection report" + } else { + "Prüfbericht" + }; + let mut h=format!("{} {}

{title}: {}

Build: {} · Revision: {} · UTC Unix: {}

",if en{"en"}else{"de"},title,escape(&s.asset),escape(&s.asset),escape(&s.build),s.revision,s.updated); + h.push_str(&format!( + "

Report generator / Berichterstellung: {}

", + escape(env!("CSR_BUILD_ID")) + )); + let approved = s + .approval + .as_ref() + .filter(|a| a.revision == s.revision && s.blockers().is_empty()); + h.push_str(&format!( + "", + if approved.is_some() { + "APPROVED / FREIGEGEBEN" + } else { + "NOT APPROVED / NICHT FREIGEGEBEN" + } + )); + if let Some(a) = approved { + h.push_str(&format!( + "

Technician / Techniker: {} · UTC Unix: {}

", + escape(&a.technician), + a.timestamp + )); + } + h.push_str(&format!( + "

Device / Gerät

{} {}
Serial: {}
UUID: {}

", + escape(&s.identity.manufacturer), + escape(&s.identity.model), + escape(&s.identity.serial), + escape(&s.identity.uuid) + )); + if let Some(r) = &s.report { + h.push_str(""); + for k in [ + "cpu", + "logical_threads", + "installed_ram_gib", + "ram_gib", + "gpus", + "screen_resolution", + "screen_inches", + "tool_version", + ] { + h.push_str(&format!("", value(r, k))); + } + h.push_str("
Specification / SpezifikationValue / Wert
{k}{}

Storage health / Laufwerkszustand

"); + for d in r["disks"].as_array().into_iter().flatten() { + h.push_str(&format!( + "", + value(d, "name"), + value(d, "model"), + value(d, "size_gib"), + value(d, "smart_health"), + value(d, "smart_details") + )); + } + h.push_str("
DiskModel / GiBSMART
{}{} / {}{}
{}

Battery health / Akkuzustand

"); + for b in r["batteries"].as_array().into_iter().flatten() { + h.push_str(&format!( + "", + value(b, "name"), + value(b, "charge_percent"), + value(b, "health_percent"), + value(b, "status") + )); + } + h.push_str("
BatteryCharge %Health %Status
{}{}{}{}

Measurements / Messwerte

"); + for k in [ + "temperatures_c", + "benchmarks", + "memory_benchmark", + "compilation_benchmark", + "concerns", + ] { + if !r[k].is_null() { + h.push_str(&format!( + "

{k}

{}
", + escape(&serde_json::to_string_pretty(&r[k]).unwrap_or_default()) + )); + } + } + } + h.push_str("

Inspection results / Prüfergebnisse

"); + for k in &s.order { + let c = &s.checks[k]; + h.push_str(&format!( + "", + escape(k), + c.outcome.label(en), + escape(&c.notes) + )); + } + h.push_str("
TestResult / ErgebnisNotes / Notizen
{}{}{}
"); + for k in &s.order { + let c = &s.checks[k]; + h.push_str(&format!( + "

{}

{}
", + escape(k), + escape(&c.evidence) + )); + for e in &c.history { + h.push_str(&format!( + "

{} · {} · {}

{}
", + e.timestamp, + escape(&e.kind), + e.outcome.label(en), + escape(&e.text) + )); + } + } + h.push_str(&format!("

Notes / Notizen

{}

Acceptance settings / Werkstattgrenzen

{}
",escape(&s.notes),escape(&serde_json::to_string_pretty(&s.settings).unwrap_or_default()))); + for b in s.blockers() { + h.push_str(&format!("

{}

", escape(&b))); + } + if !s.approvals.is_empty() { + h.push_str("

Approval history / Freigabehistorie

"); + for a in &s.approvals { + h.push_str(&format!( + "

Revision {} · {} · UTC Unix {}

", + a.revision, + escape(&a.technician), + a.timestamp + )); + } + } + h.push_str(""); + h +} +pub fn export(data: &Path, s: &Inspection) -> Result { + if !workflow::valid_id(&s.asset) { + return Err("Invalid asset ID".into()); + } + let path = data.join("final-reports").join(&s.asset).join(format!( + "inspection-{}-{}.html", + s.revision, + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + )); + workflow::atomic_write(&path, html(s).as_bytes())?; + Ok(path) +} +#[cfg(test)] +mod tests { + use super::*; + use crate::workflow::*; + #[test] + fn escapes_notes_and_does_not_print_stale_approval() { + let mut s = Inspection::new("PC-1", Identity::default(), Settings::default()); + s.notes = "".into(); + s.approval = Some(Approval { + technician: "T".into(), + timestamp: 0, + revision: 0, + }); + let h = html(&s); + assert!(!h.contains("