Offene Werkstatt-Issues umsetzen
Some checks failed
Tests / test (push) Has been cancelled

This commit is contained in:
2026-10-15 10:12:29 +02:00
parent 9dc37d1ece
commit 31ac17dd41
8 changed files with 146 additions and 5 deletions

View File

@@ -25,6 +25,7 @@ Reparaturen, Berichte und die Synchronisationswarteschlange sind über das Menü
- automatische Hardware-ID und serverseitig vergebene Gerätenummer
- Schutz vor doppelten Datensätzen
- gespeichertes Werkstatt-WLAN mit geschütztem Passwort
- umschaltbare deutsche oder US-Tastaturbelegung
- 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

View File

@@ -101,6 +101,20 @@ fn ethernet_connected() -> bool {
.iter()
.any(|(_, kind, carrier, _)| kind == "LAN/other" && carrier == "1")
}
fn mobile_device() -> bool {
let chassis = read_trim("/sys/class/dmi/id/chassis_type")
.or_else(|| read_trim("/run/csr-intake/chassis_type"))
.unwrap_or_default();
matches!(
chassis.as_str(),
"8" | "9" | "10" | "11" | "14" | "30" | "31" | "32"
) || fs::read_dir("/sys/class/power_supply").is_ok_and(|entries| {
entries.flatten().any(|entry| {
read_trim(&entry.path().join("type").to_string_lossy())
.is_some_and(|kind| kind == "Battery")
})
})
}
fn memory_dmi() -> (String, String) {
let text = match output("dmidecode", &["-t", "memory"]) {
Some(s) => s,
@@ -257,8 +271,8 @@ pub fn guided_tests(data: &Path) {
println!("Invalid asset ID");
return;
}
println!("\nNETWORK PRECHECK: Before Wi-Fi test, unplug Ethernet if one is connected.");
if ethernet_connected() {
println!("\nNETWORK PRECHECK");
if mobile_device() && 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() {
@@ -267,7 +281,7 @@ pub fn guided_tests(data: &Path) {
} else {
println!("No active wired link detected. Wireless testing may proceed.");
}
let wifi = if ethernet_connected() {
let wifi = if mobile_device() && ethernet_connected() {
"NOT_TESTED"
} else {
println!(

View File

@@ -31,6 +31,38 @@ fn exec(tool: &str, args: &[&str]) -> Result<String, String> {
Err(format!("{tool} ({})\n{log}", out.status))
}
}
fn mobile_chassis(value: &str) -> bool {
matches!(
value.trim(),
"8" | "9" | "10" | "11" | "14" | "30" | "31" | "32"
)
}
fn mobile_device() -> bool {
let chassis = fs::read_to_string("/sys/class/dmi/id/chassis_type")
.or_else(|_| fs::read_to_string("/run/csr-intake/chassis_type"))
.unwrap_or_default();
if mobile_chassis(&chassis) {
return true;
}
fs::read_dir("/sys/class/power_supply").is_ok_and(|entries| {
entries.flatten().any(|entry| {
fs::read_to_string(entry.path().join("type")).is_ok_and(|kind| kind.trim() == "Battery")
})
})
}
fn wired_link_active() -> bool {
fs::read_dir("/sys/class/net").is_ok_and(|entries| {
entries.flatten().any(|entry| {
let path = entry.path();
entry.file_name() != "lo"
&& !path.join("wireless").exists()
&& fs::read_to_string(path.join("carrier")).is_ok_and(|v| v.trim() == "1")
})
})
}
pub fn needs_lan_disconnect() -> bool {
mobile_device() && wired_link_active()
}
fn temps() -> Vec<(String, f64)> {
let mut values = Vec::new();
let Ok(chips) = fs::read_dir("/sys/class/hwmon") else {
@@ -316,7 +348,12 @@ fn run_legacy(tool: &str, settings: &crate::workflow::Settings) -> (bool, String
&["-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."))
let lan = if needs_lan_disconnect() {
"\nWARNUNG: Mobiles Gerät mit aktiver LAN-Verbindung. LAN-Kabel vor dem WLAN-Test entfernen."
} else {
""
};
(true,format!("Status:\n{status}\nGeräte:\n{dev}\nNetze:\n{scan}{lan}\nWLAN-Verbindung aktiv testen; Sichtbarkeit ist noch kein PASS."))
}
}
}
@@ -479,4 +516,14 @@ mod tests {
);
assert_eq!(peak_temperature("PEAK_TEMPERATURE_C=NaN"), None);
}
#[test]
fn portable_chassis_types_are_recognized() {
for value in ["8", "9", "10", "11", "14", "30", "31", "32"] {
assert!(mobile_chassis(value));
}
for value in ["3", "7", "13", "15", "unknown"] {
assert!(!mobile_chassis(value));
}
}
}

View File

@@ -650,6 +650,15 @@ impl App {
));
if let Some(d) = &self.station_device {
ui.heading(format!("#{}", d["number"]));
match d["_registration"].as_str() {
Some("NEW") => {
ui.colored_label(GREEN, tr(en, "Neu im Inventory registriert", "Newly registered in inventory"));
}
Some("EXISTING") => {
ui.label(tr(en, "Bereits im Inventory vorhanden", "Existing inventory device"));
}
_ => {}
}
let stage = match d["stage"].as_str().unwrap_or("") {
"INTAKE" => "Eingang",
"INSPECTION" => "Inspektion",
@@ -912,6 +921,16 @@ impl App {
}
ui.add_enabled_ui(!conflict, |ui| {
if key == "wifi" {
if diagnostics::needs_lan_disconnect() {
ui.colored_label(
ORANGE,
tr(
en,
"LAN-Kabel entfernen: Bei diesem mobilen Gerät muss der WLAN-Test ohne aktive Kabelverbindung erfolgen.",
"Disconnect Ethernet: test Wi-Fi on this mobile device without an active wired connection.",
),
);
}
self.wifi.set_language(en);
let was_busy = self.wifi.busy();
self.wifi.ui(ui, self.receiver.is_some());
@@ -1437,6 +1456,29 @@ impl App {
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, "Tastaturbelegung", "Keyboard layout"));
egui::ComboBox::from_id_salt("keyboard-layout")
.selected_text(match self.settings_draft.keyboard_layout.as_str() {
"us" => "English (US)",
_ => "Deutsch (DE)",
})
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.settings_draft.keyboard_layout,
"de".into(),
"Deutsch (DE)",
);
ui.selectable_value(
&mut self.settings_draft.keyboard_layout,
"us".into(),
"English (US)",
);
});
ui.small(tr(
en,
"Die Tastaturbelegung wird beim nächsten Start des USB-Systems angewendet.",
"The keyboard layout is applied on the next USB-system boot.",
));
ui.label(tr(en, "WLAN-Testserver", "Wi-Fi test server"));
ui.text_edit_singleline(&mut self.settings_draft.test_url);
ui.horizontal(|ui| {

View File

@@ -86,6 +86,7 @@ pub fn init_index(data: &Path) -> Result<(), String> {
#[serde(default)]
pub struct Settings {
pub language: String,
pub keyboard_layout: String,
pub test_url: String,
pub stress_seconds: u64,
pub recording_seconds: u64,
@@ -98,6 +99,7 @@ impl Default for Settings {
fn default() -> Self {
Self {
language: "de".into(),
keyboard_layout: "de".into(),
test_url: "https://example.com".into(),
stress_seconds: 120,
recording_seconds: 5,
@@ -113,6 +115,9 @@ impl Settings {
if !["de", "en"].contains(&self.language.as_str()) {
return Err("Language must be de or en".into());
}
if !["de", "us"].contains(&self.keyboard_layout.as_str()) {
return Err("Keyboard layout must be de or us".into());
}
if !(self.test_url.starts_with("https://") || self.test_url.starts_with("http://"))
|| self.test_url.contains(['\n', '\r'])
{

View File

@@ -4,6 +4,19 @@
[[ -z "${WAYLAND_DISPLAY:-}" ]] || return 0
export WLR_RENDERER_ALLOW_SOFTWARE=1
export XDG_SESSION_TYPE=wayland
keyboard_layout=$(python3 - /mnt/intake-data/config/workshop.json <<'PY'
import json
import pathlib
import sys
try:
value = json.loads(pathlib.Path(sys.argv[1]).read_text()).get('keyboard_layout', 'de')
except (OSError, ValueError):
value = 'de'
print(value if value in ('de', 'us') else 'de')
PY
)
export XKB_DEFAULT_LAYOUT="$keyboard_layout"
if [[ ! -w /mnt/intake-data ]]; then
printf '\nCS-R Intake: data partition not writable; no scan can be saved.\n' >&2
printf 'Press Enter for diagnostic console...'

View File

@@ -111,7 +111,12 @@ def api_request(base, key, method, suffix, payload=None, timeout=12, ca_file=Non
if ca_file:
context.load_verify_locations(cafile=str(ca_file))
with urllib.request.urlopen(request, timeout=timeout, context=context) as res:
return json.loads(res.read(1024 * 1024).decode('utf-8'))
result = json.loads(res.read(1024 * 1024).decode('utf-8'))
if method == 'POST' and suffix.strip('/') == 'devices/resolve':
status = getattr(res, 'status', None)
if status in (200, 201):
result['_registration'] = 'NEW' if status == 201 else 'EXISTING'
return result
def credentials(config):
base = config.get('url', '')
@@ -346,9 +351,12 @@ def run_station(root,action,report_path):
else:
report=json.loads(report_path.read_text()) if report_path else station_identity()
device=station_resolve(config,report)
registration=device.get('_registration')
if action=='intake' and device['stage']=='INTAKE':
device=station_api(config,'POST',f"devices/{device['number']}/intake/",{
'hwid':hardware_id(report),'expected_revision':device['revision'],'specifications':map_fields(report)})
if registration:
device['_registration']=registration
if action == 'intake' and report_path:
raw=report_path.read_bytes(); sha=hashlib.sha256(raw).hexdigest()
with contextlib.closing(sqlite3.connect(root/'database/intake.sqlite3')) as db, db:

View File

@@ -157,6 +157,17 @@ class ConnectionTests(unittest.TestCase):
self.assertTrue(context.check_hostname)
self.assertIs(request.call_args.kwargs['context'], context)
def test_resolve_exposes_new_or_existing_registration(self):
for status, expected in ((201, 'NEW'), (200, 'EXISTING')):
with self.subTest(status=status), patch.object(bridge.urllib.request, 'urlopen') as request:
response = request.return_value.__enter__.return_value
response.status = status
response.read.return_value = b'{"number": 42}'
result = bridge.api_request(
'https://inventory.test', 'test-key', 'POST', 'devices/resolve/', {}
)
self.assertEqual(result['_registration'], expected)
def test_network_errors_are_actionable_without_raw_secrets(self):
cases = [
(bridge.ssl.SSLCertVerificationError(1, 'secret'), 'certificate'),