From 3aaf5cc59cef7cfdb8da854ce2a05125a9f776ae Mon Sep 17 00:00:00 2001 From: Stefan Mogeritsch Date: Thu, 7 May 2026 17:18:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(desktop,=20network):=20Fehlerhandling=20ve?= =?UTF-8?q?rbessert,=20Tools-Men=C3=BC=20erweitert=20und=20mDNS-Discovery?= =?UTF-8?q?=20optimiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Mogeritsch --- conveyor.conf | 3 +- ...ssion_Log_Frontend_Networking_Discovery.md | 37 ++++++++++ docs/ToDo/ToDo-Firewall_2026-7-5.md | 72 +++++++++++++++++++ .../discovery/JmDnsDiscoveryService.kt | 38 ++++++++-- .../DeviceInitializationViewModel.kt | 5 +- .../DeviceInitializationConfig.jvm.kt | 17 ++++- .../at/mocode/frontend/shell/desktop/main.kt | 6 +- .../navigation/DesktopNavigationPort.kt | 12 ++-- .../screens/layout/components/TopHeader.kt | 35 +++++++-- 9 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 docs/99_Journal/2026-05-07_Session_Log_Frontend_Networking_Discovery.md create mode 100644 docs/ToDo/ToDo-Firewall_2026-7-5.md diff --git a/conveyor.conf b/conveyor.conf index 61da28df..97b9232b 100644 --- a/conveyor.conf +++ b/conveyor.conf @@ -40,7 +40,8 @@ app { jvm-options = [ "-Xms128m", "-Xmx512m", - "-Dfile.encoding=UTF-8" + "-Dfile.encoding=UTF-8", + "--enable-native-access=ALL-UNNAMED" ] } diff --git a/docs/99_Journal/2026-05-07_Session_Log_Frontend_Networking_Discovery.md b/docs/99_Journal/2026-05-07_Session_Log_Frontend_Networking_Discovery.md new file mode 100644 index 00000000..10aa3275 --- /dev/null +++ b/docs/99_Journal/2026-05-07_Session_Log_Frontend_Networking_Discovery.md @@ -0,0 +1,37 @@ +--- +type: Journal +status: ACTIVE +owner: Curator +last_update: 2026-05-07 +--- + +# 2026-05-07 — Session Log (Frontend Networking, Discovery, Connectivity) + +## Kontext +- Fokus: Stabilisierung der lokalen Host/Client‑Kommunikation (mDNS, WS‑Chat), robuste Connectivity‑Checks, UX für Backup‑Pfad, Session‑Abschluss mit Dokumentation. + +## Summary +- ConnectivityCheck robuster gemacht (Fallbacks, schneller Erstcheck) und Logs (Base‑URL, WS‑Port) korrigiert. +- Discovery/Registration zentralisiert und entdoppelt; Interface‑Bindung und Logging verbessert. +- Datei‑Picker auf `JFileChooser` umgestellt; editierbares Pfadfeld mit Validierung integriert. +- Firewalld/mDNS‑Ursache für fehlende Sichtbarkeit zwischen Host/Client identifiziert und als ToDo/Guide dokumentiert. + +## Changes +- ConnectivityTracker: Fallback‑Kaskade readiness → health → /api/ping/simple; Intervalle angepasst; Debug‑Logs ergänzt. +- main.kt: korrekte String‑Interpolation; Start‑Log der `NetworkConfig.baseUrl`; WS‑Port 8090 konsistent. +- JmDnsDiscoveryService: Interface‑Filter (ohne docker/br/veth, private IPv4 priorisiert), Debounce/De‑Dup der Registrierung, Log‑Noise reduziert. +- Navigation: Guard gegen Navigation auf gleichen Screen; Top‑Bar Tools erweitert (Reset/Backup/Settings‑Ordner öffnen). +- MsFilePicker (JVM): `JFileChooser` mit freier Pfadeingabe; Validierung inkl. Schreib‑Probe; automatische Ordnererstellung bei Auswahl. +- conveyor.conf: JVM‑Flag `--enable-native-access=ALL-UNNAMED` ergänzt (Netty‑Warnung mitigiert). + +## Verification +- Build (Gradle): erfolgreich ✓ +- Laufzeit/Netzwerk: Verifikation ausstehend (mDNS nach Firewall‑Freigaben; KDE‑Picker unter Fedora 44; Host/Client‑Sichtbarkeit LAN/WLAN) — Anti‑Halluzinations‑Protokoll beachtet. + +## Hinweise / Betriebsleitfaden +- Firewalld/mDNS Freigaben dokumentiert in: `docs/ToDo/ToDo-Firewall_2026-7-5.md` (mdns + Ports 8090/8080; Reload/Kontrolle; Avahi/Tcpdump Checks). + +## Nächste Schritte +1. KDE‑Directory‑Picker: auf `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus wechseln; präzisere Fehlermeldungen; HOME‑Fallback. +2. Guard gegen mehrfachen P2P‑Start ergänzen. +3. Conveyor/Windows‑Installer in CI (Runtime‑Flags; optional SLF4J‑Binding), danach erneute Laufzeit‑Verifikation. diff --git a/docs/ToDo/ToDo-Firewall_2026-7-5.md b/docs/ToDo/ToDo-Firewall_2026-7-5.md new file mode 100644 index 00000000..e89c1f8e --- /dev/null +++ b/docs/ToDo/ToDo-Firewall_2026-7-5.md @@ -0,0 +1,72 @@ +🎨 [Frontend Expert] + +### Kurzfazit +- Die beiden Rechner sehen sich über mDNS höchstwahrscheinlich wegen Firewall‑Regeln/Zonen nicht. JmDNS registriert korrekt auf 192.168.0.124:8090, aber der Client (192.168.0.167, WLAN) empfängt keine mDNS‑Antworten → Fedora/Firewalld blockiert UDP 5353 Multicast in der aktiven Zone (WLAN/Public) sehr häufig standardmäßig. +- Der Datei‑Picker unter Fedora KDE hakt im Verzeichnis‑Modus vermutlich wegen der Kombination DIRECTORIES_ONLY + SAVE_DIALOG und/oder wegen unserer „schreibbar“-Validierung. + +### Sofortmaßnahmen (auf beiden Geräten, Host + Client) +1) Firewalld freischalten (mdns + Ports) und neu laden +``` +sudo firewall-cmd --get-active-zones +# notieren, in welcher Zone das aktive Interface liegt (z. B. public, home) +ZONE=public # ggf. anpassen + +# mDNS erlauben (UDP 5353 Multicast) +sudo firewall-cmd --zone=$ZONE --add-service=mdns --permanent + +# unsere Dienste öffnen +sudo firewall-cmd --zone=$ZONE --add-port=8090/tcp --permanent # WS-Chat +sudo firewall-cmd --zone=$ZONE --add-port=8080/tcp --permanent # P2P-Server (PoC) + +# Regeln anwenden +sudo firewall-cmd --reload + +# Kontrolle +sudo firewall-cmd --zone=$ZONE --list-all +``` +Hinweis: WLAN und Ethernet können in unterschiedlichen Zonen liegen (z. B. public vs. home). Bitte in beiden relevanten Zonen freischalten. + +2) Optional: Avahi als Indikator prüfen (nicht zwingend für JmDNS, aber gut für mDNS‑Baseline) +``` +systemctl status avahi-daemon +``` + +3) mDNS‑Pakete verifizieren (nur wenn es nach 1) noch nicht klappt) +``` +# Auf dem Client (WLAN‑Interface anpassen): +sudo tcpdump -n -i wlp0s20f3 port 5353 +# Auf dem Host (Ethernet‑Interface anpassen): +sudo tcpdump -n -i port 5353 +``` +Erwartung: Bei App‑Start siehst du Multicast an 224.0.0.251. Wenn auf einer Seite „Stille“ herrscht, blockiert noch etwas. + +4) WS‑Konnektivität direkt testen (um Networking unabhängig von mDNS zu prüfen) +``` +# vom Client auf den Host +websocat ws://192.168.0.124:8090/chat +# oder wscat: wscat -c ws://192.168.0.124:8090/chat +``` +Wenn das klappt, ist der WS‑Pfad ok; dann bleibt nur mDNS/Firewall als Ursache. + +### Erklärung zu deinen Logs +- „[Discovery] Dienst 'fedora-stefan' … (Port 8090)“ → Host hat korrekt via mDNS veröffentlicht. +- „Discovery Update: 0 Dienste gefunden“ am Client → keine mDNS‑Antworten angekommen (typisches Firewall‑Symptom bei Fedora/KDE, vor allem in Zone „public“). +- Mehrfach „[P2P Server] Gestartet auf Port 8080“ → Wir haben noch einen Trigger, der den P2P‑Start mehrfach feuert. Funktional unkritisch für den aktuellen Befund; ich bereinige das im nächsten Patch mit einem zusätzlichen Guard. + +### Fedora KDE Datei‑Picker (JFileChooser) – Workarounds jetzt, Fix als Patch im Anschluss +- Mögliche Ursache 1: `SAVE_DIALOG` im Ordner‑Modus unter KDE verhält sich zickig. Besser: `OPEN_DIALOG` im `DIRECTORIES_ONLY`‑Modus + eigener Bestätigungs‑Button‑Text. +- Mögliche Ursache 2: Unsere Validierung „Ordner existiert und ist beschreibbar“ verhindert die Übernahme (z. B. externer Stick nur lesbar, NTFS ohne Schreibrechte, Netzwerkpfad ohne Rechte). Teste bitte einmal mit einem lokalen Pfad wie `~/MeldestelleBackup`. +- Quick‑Test jetzt: Tippe im editierbaren Feld manuell `~/MeldestelleBackup`, bestätige → sollte grün werden. Falls rot: bitte den Text der Fehlermeldung nennen. + +Geplanter Fix (nach deiner Bestätigung): +- Directory‑Picker: Wechsel auf `showOpenDialog(null)` mit `JFileChooser.DIRECTORIES_ONLY`, `approveButtonText = "Auswählen"`, `acceptAllFileFilterUsed=false`. Fallback auf HOME, wenn `currentDirectory` ungültig. Beibehalt der freien Texteingabe. +- Validierung: Fehlermeldung präziser („existiert nicht“ vs. „keine Schreibrechte“) und angebotene Auto‑Erstellung, wenn der User bestätigt. + +### Bitte liefere kurz zurück +- Nach Schritt 1 (firewalld): Finden sich Host (192.168.0.124) und Client (192.168.0.167) gegenseitig? Tauchen Services in der UI/Logs auf? +- Falls noch nicht: kurzer Ausschnitt aus `tcpdump` beider Seiten (je 3–5 Zeilen genügen). +- Datei‑Picker auf dem Client: Was passiert bei manuellem Pfad `~/MeldestelleBackup`? Erscheint eine Fehlermeldung? Wenn ja, welcher Text? + +### Nächste Schritte (nach Feedback) +- Ich liefere: Patch für den KDE‑Picker (OPEN_DIALOG) und einen zusätzlichen Guard gegen mehrfachen P2P‑Start; außerdem noch etwas Discovery‑Logging (Interface/Zonen‑Hinweis). +- Danach kümmern wir uns um Conveyor (Windows‑Installer aus CI, inkl. JVM‑Flag gegen die Netty‑Warnung). diff --git a/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt index 7380f512..c98c3f7c 100644 --- a/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt +++ b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt @@ -19,12 +19,23 @@ class JmDnsDiscoveryService : NetworkDiscoveryService { private val jmdnsInstances = mutableListOf() private val SERVICE_TYPE = "_meldestelle._tcp.local." private val discoveredServicesMap = ConcurrentHashMap() + private val registeredSet = ConcurrentHashMap.newKeySet() // key: "${name}@${addr.hostAddress}:$port" + + // Debounce/Guards + @Volatile private var lastStartRequestedAt: Long = 0L + @Volatile private var lastStartIp: String? = null private val _discoveredServices = MutableStateFlow>(emptyList()) override val discoveredServices: StateFlow> = _discoveredServices.asStateFlow() override fun startDiscovery(preferredIp: String?) { - if (jmdnsInstances.isNotEmpty()) return + // Debounce schnelle Folgeaufrufe mit identischer IP + val now = System.currentTimeMillis() + if (jmdnsInstances.isNotEmpty() && lastStartIp == preferredIp && (now - lastStartRequestedAt) < 500) { + return + } + lastStartRequestedAt = now + lastStartIp = preferredIp val addresses = getRelevantAddresses(preferredIp) if (addresses.isEmpty()) { @@ -112,8 +123,13 @@ class JmDnsDiscoveryService : NetworkDiscoveryService { ) ) try { - jmdns.registerService(serviceInfo) - println("[Discovery] Dienst '$name' auf ${jmdns.inetAddress} registriert (Port $port)") + val key = "${name}@${jmdns.inetAddress.hostAddress}:$port" + if (registeredSet.add(key)) { + jmdns.registerService(serviceInfo) + println("[Discovery] Dienst '$name' auf ${jmdns.inetAddress} registriert (Port $port)") + } else { + // bereits registriert – kein Spam + } } catch (e: Exception) { println("[Discovery] Fehler bei Registrierung auf ${jmdns.inetAddress}: ${e.message}") } @@ -130,13 +146,19 @@ class JmDnsDiscoveryService : NetworkDiscoveryService { val interfaces = NetworkInterface.getNetworkInterfaces() while (interfaces.hasMoreElements()) { val iface = interfaces.nextElement() + val name = iface.name.lowercase() + // Filtere Docker/Bridged/VETH/VM-Schnittstellen heraus if (iface.isLoopback || !iface.isUp || iface.isVirtual) continue + if (name.startsWith("br-") || name.startsWith("docker") || name.startsWith("veth") || name.contains("vmnet") || name.contains("virbr")) continue val inetAddresses = iface.inetAddresses while (inetAddresses.hasMoreElements()) { val addr = inetAddresses.nextElement() // Nur IPv4 für maximale Kompatibilität in lokalen Netzen (ÖTO/FEI Standardumgebungen) if (addr is java.net.Inet4Address) { + // Exkludiere Link-Local + val host = addr.hostAddress + if (host.startsWith("169.254.")) continue addresses.add(addr) } } @@ -145,7 +167,15 @@ class JmDnsDiscoveryService : NetworkDiscoveryService { println("[Discovery] Fehler beim Auflisten der Interfaces: ${e.message}") } - return if (addresses.isEmpty()) listOf(InetAddress.getLocalHost()) else addresses + if (addresses.isEmpty()) return listOf(InetAddress.getLocalHost()) + + // Bevorzuge private LAN IPv4 (192.168.x.x, 10.x.x.x, 172.16-31.x.x) + fun isPrivateIPv4(a: InetAddress): Boolean { + val h = a.hostAddress + return h.startsWith("192.168.") || h.startsWith("10.") || (h.startsWith("172.") && h.split('.').getOrNull(1)?.toIntOrNull() in 16..31) + } + return addresses.sortedWith(compareByDescending { isPrivateIPv4(it) } + .thenBy { it.hostAddress }) } override fun getDiscoveredServices(): List { diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt index 13006143..c83520c7 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationViewModel.kt @@ -208,10 +208,11 @@ class DeviceInitializationViewModel( discoveryService.stopDiscovery() discoveryService.startDiscovery(ip) - // Falls wir ein Master sind, registrieren wir uns auch direkt, damit andere uns finden + // Falls wir ein Master sind, starten wir den lokalen P2P‑Server. + // Die mDNS‑Registrierung erfolgt zentral beim App‑Start (entkoppelt, um Duplikate zu vermeiden). if (uiState.value.settings.networkRole == NetworkRole.MASTER) { - discoveryService.registerService(8080, ip, uiState.value.settings.deviceName) syncService.startServer(8080) + println("[P2P Server] Gestartet auf Port 8080") } } diff --git a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt index 9ced6d6c..e48cfd1a 100644 --- a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt +++ b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt @@ -214,7 +214,22 @@ actual fun DeviceInitializationConfig( MsFilePicker( label = "Backup-Verzeichnis (Plan-USB)", selectedPath = settings.backupPath, - onFileSelected = { viewModel.updateSettings { s -> s.copy(backupPath = it) } }, + onFileSelected = { path -> + if (path.isNotBlank()) { + try { + val dir = java.io.File(path) + if (!dir.exists()) dir.mkdirs() + val probe = java.io.File(dir, ".ms_write_test.tmp") + probe.writeText("ok") + probe.delete() + viewModel.updateSettings { s -> s.copy(backupPath = path) } + } catch (e: Exception) { + println("[DeviceInit] Backup-Verzeichnis nicht beschreibbar: ${e.message}") + } + } else { + viewModel.updateSettings { s -> s.copy(backupPath = path) } + } + }, directoryOnly = true, modifier = Modifier.focusRequester(backupPathFocus), enabled = !uiState.isLocked diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt index 506723ee..b0954514 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/main.kt @@ -73,13 +73,13 @@ fun main() = application { wsServer.start() val discovery = koin.get() discovery.startDiscovery() - // Im Host-Modus würden wir hier registerService aufrufen + // Im Host-Modus würden wir hier registerService aufrufen. // Für den POC registrieren wir den lokalen Host-Dienst immer mit dem WS-Port try { discovery.registerService(wsServer.getPort()) println("[DesktopApp] Discovery-Registrierung durchgeführt (Port ${wsServer.getPort()})") - } catch (_: Exception) { - println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${'$'}{e.message}") + } catch (e: Exception) { + println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${e.message}") } } catch(e: Exception) { println("[DesktopApp] POC-Dienste konnten nicht gestartet werden: ${e.message}") diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/navigation/DesktopNavigationPort.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/navigation/DesktopNavigationPort.kt index 23661929..ce3c742a 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/navigation/DesktopNavigationPort.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/navigation/DesktopNavigationPort.kt @@ -24,14 +24,14 @@ class DesktopNavigationPort : NavigationPort { } override fun navigateToScreen(screen: AppScreen) { - println("[DesktopNav] navigateToScreen -> $screen") - // Aktuellen Screen auf den Stack legen, falls er nicht derselbe ist val current = _currentScreen.value - if (current != screen) { - backStack.add(current) - // Begrenzung des Backstacks auf z. B. 50 Einträge - if (backStack.size > 50) backStack.removeAt(0) + if (current == screen) { + // Keine Aktion/kein Log bei identischem Ziel – beruhigt die Navigation + return } + println("[DesktopNav] navigateToScreen -> $screen") + backStack.add(current) + if (backStack.size > 50) backStack.removeAt(0) _currentScreen.value = screen } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt index 57321d20..2e6aa51a 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt @@ -151,24 +151,45 @@ fun DesktopTopHeader( try { val backupService: BackupService = GlobalContext.get().get { parametersOf(deviceName) } val result = backupService.exportDelta("poc-backup", backupPath, sharedKey) - result.onSuccess { _ -> println($$"[Backup] Erfolgreich exportiert: $path") } - .onFailure { _ -> println($$"[Backup] Fehler: ${e.message}") } - } catch (_: Exception) { - println($$"[Backup] Fehler bei der Initialisierung: ${e.message}") + result.onSuccess { fileName -> println("[Backup] Erfolgreich exportiert: $fileName") } + .onFailure { ex -> println("[Backup] Fehler: ${ex.message}") } + } catch (e: Exception) { + println("[Backup] Fehler bei der Initialisierung: ${e.message}") } } } ) HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) + DropdownMenuItem( + text = { Text("Einstellungen-Ordner öffnen") }, + onClick = { + menuOpen = false + val settingsDir = DeviceInitializationSettingsManager.getSettingsFilePath() + val parent = java.io.File(settingsDir).parentFile?.absolutePath ?: settingsDir + try { + // Versuche plattformspezifisch den Ordner zu öffnen + val os = System.getProperty("os.name").lowercase() + if (os.contains("win")) { + Runtime.getRuntime().exec(arrayOf("explorer", parent)) + } else if (os.contains("mac")) { + Runtime.getRuntime().exec(arrayOf("open", parent)) + } else { + Runtime.getRuntime().exec(arrayOf("xdg-open", parent)) + } + } catch (e: Exception) { + println("[Tools] Konnte Ordner nicht öffnen: ${e.message}. Pfad: $parent") + } + } + ) DropdownMenuItem( text = { Text("Einstellungen zurücksetzen") }, onClick = { menuOpen = false val res = DeviceInitializationSettingsManager.resetToFactoryDefaults(deleteDatabase = false) if (res.isSuccess) { - println($$"[Reset] settings.json gelöscht: ${DeviceInitializationSettingsManager.getSettingsFilePath()}") + println("[Reset] settings.json gelöscht: ${DeviceInitializationSettingsManager.getSettingsFilePath()}") } else { - println($$"[Reset] Fehler: ${res.exceptionOrNull()?.message}") + println("[Reset] Fehler: ${res.exceptionOrNull()?.message}") } onNavigate(AppScreen.DeviceInitialization) } @@ -181,7 +202,7 @@ fun DesktopTopHeader( if (res.isSuccess) { println("[Reset] settings + ~/.meldestelle gelöscht") } else { - println("[Reset] Fehler: ${'$'}{res.exceptionOrNull()?.message}") + println("[Reset] Fehler: ${res.exceptionOrNull()?.message}") } onNavigate(AppScreen.DeviceInitialization) }