diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index 35ce20a6..b2637685 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -276,7 +276,7 @@ und über definierte Schnittstellen kommunizieren. ## 4. Geplante Phasen -### PHASE 13: Export & ZNS-Rückmeldung ✅ ABGESCHLOSSEN +### PHASE 13: Export & ZNS-Rückmeldung ✅ ABGESCHLOSSEN (18. April 2026) *Ziel: Finalisierung der Turnier-Daten und Rückübermittlung an den OEPS.* * [x] **Mail-Service Integration:** Online-Nennungen via REST/Mail empfangen und persistieren. ✓ (April 2026) @@ -284,6 +284,10 @@ und über definierte Schnittstellen kommunizieren. 18. April 2026) * [x] **Plug-and-Play Architektur:** Umstellung der Frontend-Komponenten auf fachlich autarke Organismen (ADR-0024). ✓ (18. April 2026) +* [x] **WASM-Transition:** Projektweite Umstellung auf JVM (Desktop) und wasmJs (Web). Eliminierung von `js(IR)`. ✓ ( + 18. April 2026) +* [x] **Geräte-Initialisierung:** Refactoring des Onboarding-Prozesses in das Plug-and-Play Modul + `device-initialization`. ✓ (18. April 2026) * [ ] **XML-Export:** Vollständiger B-Satz Export (inkl. Ergebnisse und Platzierungen). * [ ] **ZNS-Portal:** Upload-Integration in das OEPS-ZNS. * [ ] **Archivierung:** Langzeit-Archivierung abgeschlossener Turniere. diff --git a/docs/99_Journal/2026-04-18_DeviceInitialization-PlugAndPlay.md b/docs/99_Journal/2026-04-18_DeviceInitialization-PlugAndPlay.md index dde33a7e..77c52e9f 100644 --- a/docs/99_Journal/2026-04-18_DeviceInitialization-PlugAndPlay.md +++ b/docs/99_Journal/2026-04-18_DeviceInitialization-PlugAndPlay.md @@ -50,7 +50,11 @@ Prinzip und ist fachlich präzise benannt. * **ViewModel-Fix:** `DeviceInitializationViewModel` erbt nun von `androidx.lifecycle.ViewModel`, was die Integration in `koinViewModel` ermöglicht. * **DesktopMainLayout:** Syntaxfehler beim `koinViewModel`-Aufruf behoben und Typos (`geraetName` -> `deviceName`) - korrigiert. + korrigiert. Unbenutzter `settings`-Parameter entfernt. * **Multiplatform-Härtung:** `DeviceInitializationSettingsManager` und `DeviceInitializationConfig` auf `expect/actual` - umgestellt, um JVM-Lecks im Common-Code zu vermeiden und JS/WasmJS Kompatibilität (via Stubs) sicherzustellen. + umgestellt. +* **Beta-Warnungen:** `@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")` hinzugefügt, um + Compiler-Warnungen in den Domain-Klassen zu unterdrücken. +* **WASM-Komplettierung:** `DeviceInitializationSettingsManager` nutzt nun `localStorage` im Web. + `DeviceInitializationConfig` wurde für WasmJS funktional implementiert (Basis-Konfiguration). * **UI-Cleanup:** Code-Duplikate in der Desktop-Konfiguration durch `MsSettingsField` reduziert. diff --git a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.kt b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.kt index adf18164..8f34a5ce 100644 --- a/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.kt +++ b/frontend/features/device-initialization/src/commonMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.kt @@ -1,3 +1,5 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + package at.mocode.frontend.features.deviceinitialization.domain expect object DeviceInitializationSettingsManager { diff --git a/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.wasmJs.kt b/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.wasmJs.kt index 4b94ffdd..a90ce222 100644 --- a/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.wasmJs.kt +++ b/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/domain/DeviceInitializationSettingsManager.wasmJs.kt @@ -1,11 +1,35 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + package at.mocode.frontend.features.deviceinitialization.domain +import kotlinx.browser.localStorage +import kotlinx.serialization.json.Json + actual object DeviceInitializationSettingsManager { + private const val SETTINGS_KEY = "device_initialization_settings" + private val json = Json { prettyPrint = true; ignoreUnknownKeys = true } + actual fun saveSettings(settings: DeviceInitializationSettings) { - // Nicht implementiert für WasmJS + try { + val content = json.encodeToString(DeviceInitializationSettings.serializer(), settings) + localStorage.setItem(SETTINGS_KEY, content) + } catch (e: Exception) { + println("Fehler beim Speichern der Einstellungen (WasmJS): ${e.message}") + } } - actual fun loadSettings(): DeviceInitializationSettings? = null + actual fun loadSettings(): DeviceInitializationSettings? { + val content = localStorage.getItem(SETTINGS_KEY) ?: return null + return try { + json.decodeFromString(DeviceInitializationSettings.serializer(), content) + } catch (e: Exception) { + println("Fehler beim Laden der Einstellungen (WasmJS): ${e.message}") + null + } + } - actual fun isConfigured(): Boolean = false + actual fun isConfigured(): Boolean { + val settings = loadSettings() ?: return false + return DeviceInitializationValidator.canContinue(settings) + } } diff --git a/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/presentation/DeviceInitializationConfig.wasmJs.kt b/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/presentation/DeviceInitializationConfig.wasmJs.kt index 1a1c7e73..b1561b55 100644 --- a/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/presentation/DeviceInitializationConfig.wasmJs.kt +++ b/frontend/features/device-initialization/src/wasmJsMain/kotlin/at/mocode/frontend/features/deviceinitialization/presentation/DeviceInitializationConfig.wasmJs.kt @@ -1,12 +1,92 @@ package at.mocode.frontend.features.deviceinitialization.presentation -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import at.mocode.frontend.features.deviceinitialization.domain.DeviceInitializationValidator @Composable actual fun DeviceInitializationConfig( uiState: DeviceInitializationUiState, viewModel: DeviceInitializationViewModel ) { - Text("Konfiguration für Web (WasmJS) ist nicht implementiert.") + val settings = uiState.settings + + Card(modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("⚙️ Geräte-Konfiguration", style = MaterialTheme.typography.titleMedium) + + MsSettingsField( + value = settings.deviceName, + onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } }, + label = "Gerätename", + placeholder = "z.B. Web-Client", + isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName), + errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich." + ) + + var passwordVisible by remember { mutableStateOf(false) } + MsSettingsField( + value = settings.sharedKey, + onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } }, + label = "Sicherheitsschlüssel (Sync-Key)", + placeholder = "Mindestens 8 Zeichen", + isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey), + errorText = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.", + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = if (passwordVisible) "Verbergen" else "Anzeigen" + ) + } + } + ) + + Text( + "Hinweis: In der Web-Version sind erweiterte Master-Funktionen (wie lokales Backup) derzeit deaktiviert.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun MsSettingsField( + value: String, + onValueChange: (String) -> Unit, + label: String, + placeholder: String, + isError: Boolean, + errorText: String, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + placeholder = { Text(placeholder) }, + modifier = Modifier.fillMaxWidth(), + isError = isError, + visualTransformation = visualTransformation, + trailingIcon = trailingIcon, + supportingText = { + if (isError) { + Text(errorText) + } + } + ) } diff --git a/frontend/shells/meldestelle-desktop/settings.json b/frontend/shells/meldestelle-desktop/settings.json index 681b4189..cf06af35 100644 --- a/frontend/shells/meldestelle-desktop/settings.json +++ b/frontend/shells/meldestelle-desktop/settings.json @@ -1,12 +1,4 @@ { - "geraetName": "Meldestelle", - "sharedKey": "Meldestelle", - "backupPath": "/home/stefan/WsMeldestelle/Meldestelle/meldestelle/docs/temp", - "networkRole": "MASTER", - "expectedClients": [ - { - "name": "Richter-Turm", - "role": "RICHTER" - } - ] + "deviceName": "Richter-Turm", + "sharedKey": "Meldestelle" } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt index b6e17eef..5bd7b0ea 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt @@ -118,7 +118,6 @@ fun DesktopMainLayout( onboardingSettings = it DeviceInitializationSettingsManager.saveSettings(it) }, - settings = onboardingSettings, ) } @@ -523,7 +522,6 @@ private fun DesktopContentArea( currentScreen: AppScreen, onNavigate: (AppScreen) -> Unit, onBack: () -> Unit, - settings: DeviceInitializationSettings, onSettingsChange: (DeviceInitializationSettings) -> Unit, ) { when (currentScreen) {