From 8806d11e3c7d599d2fcd1d3007aa0850d88f88cf Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 20 Apr 2026 14:23:44 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20implementiere=20Auth-Status-abh=C3=A4n?= =?UTF-8?q?gige=20Navigation=20und=20Icons,=20deaktiviere=20Module=20ohne?= =?UTF-8?q?=20Initialisierung=20und=20passe=20NavRail=20sowie=20Header=20f?= =?UTF-8?q?=C3=BCr=20besseren=20UX=20an?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: StefanMoCoAt --- .../2026-04-20_Session-Log_Onboarding.md | 31 ++++++++ .../DeviceInitializationConfig.jvm.kt | 43 ++++++----- .../frontend/shell/desktop/DesktopApp.kt | 7 +- .../screens/layout/DesktopMainLayout.kt | 76 +++++++++++++------ 4 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 docs/04_Agents/Journal/2026-04-20_Session-Log_Onboarding.md diff --git a/docs/04_Agents/Journal/2026-04-20_Session-Log_Onboarding.md b/docs/04_Agents/Journal/2026-04-20_Session-Log_Onboarding.md new file mode 100644 index 00000000..c7d26cbe --- /dev/null +++ b/docs/04_Agents/Journal/2026-04-20_Session-Log_Onboarding.md @@ -0,0 +1,31 @@ +--- +type: Journal +status: FINAL +owner: Curator +date: 2026-04-20 +--- + +# Session Log – Finalisierung Onboarding & Start-Sequenz (Phase 13) + +## 🏗️ Status-Update +Die Nachmittags-Session konzentriert sich auf die Bereinigung der App-Start-Sequenz nach dem **ADR-0024 Plug-and-Play Pattern**. Der erste Meilenstein (Onboarding) wurde erfolgreich abgeschlossen. + +## 🛠️ Umfang & Änderungen (Punkt 1: Onboarding) +- **Sidebar-Blocking:** Fachliche Module (`ZNS-Import`, `Stammdaten`, `Nennungen`) werden nun deaktiviert, solange das Gerät nicht initialisiert ist. Dies verhindert inkonsistente Zustände vor der Namens-/Key-Vergabe. +- **Client-Datensicherheit:** Der `backupPath` in der `settings.json` ist nun für **alle** Netzwerk-Rollen (Master & Client) verpflichtend. Dies stellt sicher, dass auch dezentrale Workstations (z.B. Richterturm) im Offline-Fall lokale Snapshots sichern. +- **Navigations-Fix:** Die "Sackgasse" im Login-Screen wurde behoben. Der Zurück-Button führt nun via `navigateBack()` korrekt zum vorherigen Kontext. +- **Dynamischer Header:** Der Header unterscheidet nun visuell zwischen "Gast" (nicht eingeloggt) und "Administrator" (eingeloggt), inklusive passender Login/Logout-Icons. +- **Setup-UX:** Einführung eines dedizierten Abschluss-Buttons für die `Client`-Initialisierung, um den Workflow für Nicht-Master-Geräte zu straffen. + +## 📐 Architektur-Check (ADR-0024) +- **Kapselung:** Die Logik verbleibt im `device-initialization` Modul. +- **Hoisting:** Navigations-States werden sauber an die Shell (`meldestelle-desktop`) delegiert. +- **Konformität:** Alle Änderungen unterstützen das Ziel einer autarken, offline-fähigen Workstation. + +## 📅 Nächste Schritte +1. **🔐 Infrastruktur:** Integration des `ConnectivityTracker` zur Visualisierung von Backend-/DB-/Auth-Status. +2. **📡 Discovery:** Start des `NetworkDiscoveryService` (mDNS) für die automatische Peer-Erkennung im LAN. +3. **🗺️ Layout:** Finalisierung der `Navigation-Rail` und des `Sync-Indikators`. + +--- +*Dokumentation erstellt durch den Curator im Rahmen des "Meldestelle"-Protokolls.* 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 1132781c..426314bf 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 @@ -72,33 +72,26 @@ actual fun DeviceInitializationConfig( isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey), errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.", visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - imeAction = if (settings.networkRole == NetworkRole.MASTER) ImeAction.Next else ImeAction.Done, + imeAction = ImeAction.Next, keyboardActions = KeyboardActions( - onNext = { focusManager.moveFocus(FocusDirection.Next) }, - onDone = { - if (DeviceInitializationValidator.canContinue(settings)) { - viewModel.completeInitialization() - } else { - focusManager.clearFocus() - } - } + onNext = { focusManager.moveFocus(FocusDirection.Next) } ), modifier = Modifier.focusRequester(sharedKeyFocus), trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, onTrailingIconClick = { passwordVisible = !passwordVisible } ) - if (settings.networkRole == NetworkRole.MASTER) { - MsFilePicker( - label = "Backup-Verzeichnis (Pfad)", - selectedPath = settings.backupPath, - onFileSelected = { selectedPath -> - viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) } - }, - directoryOnly = true, - modifier = Modifier.focusRequester(backupPathFocus) - ) + MsFilePicker( + label = "Backup-Verzeichnis (Pfad)", + selectedPath = settings.backupPath, + onFileSelected = { selectedPath -> + viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) } + }, + directoryOnly = true, + modifier = Modifier.focusRequester(backupPathFocus) + ) + if (settings.networkRole == NetworkRole.MASTER) { Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium) Slider( value = settings.syncInterval.toFloat(), @@ -106,7 +99,19 @@ actual fun DeviceInitializationConfig( valueRange = 1f..60f, steps = 59 ) + } else { + // Button zum Abschließen für Clients, da diese keinen Slider/Clients haben + Spacer(Modifier.height(8.dp)) + Button( + onClick = { viewModel.completeInitialization() }, + modifier = Modifier.fillMaxWidth(), + enabled = DeviceInitializationValidator.canContinue(settings) + ) { + Text("Konfiguration abschließen") + } + } + if (settings.networkRole == NetworkRole.MASTER) { HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall) diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt index 9e7d9041..6987aed0 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt @@ -8,14 +8,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import at.mocode.frontend.shell.desktop.navigation.DesktopNavigationPort -import at.mocode.frontend.shell.desktop.screens.layout.DesktopMainLayout import at.mocode.frontend.core.auth.data.local.AuthTokenManager import at.mocode.frontend.core.auth.presentation.LoginScreen import at.mocode.frontend.core.auth.presentation.LoginViewModel import at.mocode.frontend.core.designsystem.theme.AppTheme import at.mocode.frontend.core.navigation.AppScreen import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager +import at.mocode.frontend.shell.desktop.navigation.DesktopNavigationPort +import at.mocode.frontend.shell.desktop.screens.layout.DesktopMainLayout import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel @@ -72,7 +72,7 @@ fun DesktopApp() { val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung nav.navigateToScreen(returnTo) }, - onBack = { /* Desktop hat keine PortalDashboard-Page */ }, + onBack = { nav.navigateBack() }, ) else -> { @@ -85,6 +85,7 @@ fun DesktopApp() { authTokenManager.clearToken() nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung)) }, + isAuthenticated = authState.isAuthenticated ) } } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt index 09d823d7..05423d08 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Login import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.filled.* import androidx.compose.material3.* @@ -85,6 +86,7 @@ fun DesktopMainLayout( onNavigate: (AppScreen) -> Unit, onBack: () -> Unit, onLogout: () -> Unit, + isAuthenticated: Boolean = false ) { println("[Navigation] Rendering Screen: ${currentScreen::class.simpleName} (Details: $currentScreen)") // DeviceInitialization-Daten (On-the-fly geladen oder Default) @@ -106,7 +108,8 @@ fun DesktopMainLayout( // Navigation Rail (Modernere Seitenleiste) DesktopNavRail( currentScreen = currentScreen, - onNavigate = onNavigate + onNavigate = onNavigate, + isConfigured = onboardingSettings.isConfigured ) Column(modifier = Modifier.fillMaxSize()) { @@ -115,6 +118,7 @@ fun DesktopMainLayout( onNavigate = onNavigate, onBack = onBack, onLogout = onLogout, + isAuthenticated = isAuthenticated ) Box(modifier = Modifier.weight(1f).fillMaxWidth()) { @@ -141,7 +145,8 @@ fun DesktopMainLayout( @Composable private fun DesktopNavRail( currentScreen: AppScreen, - onNavigate: (AppScreen) -> Unit + onNavigate: (AppScreen) -> Unit, + isConfigured: Boolean ) { Surface( modifier = Modifier.fillMaxHeight().width(Dimens.NavRailWidth), @@ -174,14 +179,16 @@ private fun DesktopNavRail( icon = Icons.Default.Dashboard, label = "Admin", selected = currentScreen is AppScreen.VeranstaltungVerwaltung || currentScreen is AppScreen.VeranstaltungDetail, - onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) } + onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) }, + enabled = isConfigured ) NavRailItem( icon = Icons.Default.CloudDownload, label = "ZNS-Import", selected = currentScreen is AppScreen.StammdatenImport, - onClick = { onNavigate(AppScreen.StammdatenImport) } + onClick = { onNavigate(AppScreen.StammdatenImport) }, + enabled = isConfigured ) var showStammdatenMenu by remember { mutableStateOf(false) } @@ -193,11 +200,12 @@ private fun DesktopNavRail( currentScreen is AppScreen.Reiter || currentScreen is AppScreen.ReiterVerwaltung || currentScreen is AppScreen.Pferde || currentScreen is AppScreen.PferdVerwaltung || currentScreen is AppScreen.FunktionaerVerwaltung, - onClick = { showStammdatenMenu = true } + onClick = { showStammdatenMenu = true }, + enabled = isConfigured ) DropdownMenu( - expanded = showStammdatenMenu, + expanded = showStammdatenMenu && isConfigured, onDismissRequest = { showStammdatenMenu = false }, offset = DpOffset(Dimens.NavRailWidth, 0.dp) ) { @@ -240,14 +248,16 @@ private fun DesktopNavRail( icon = Icons.Default.Email, label = "Mails", selected = currentScreen is AppScreen.NennungsEingang, - onClick = { onNavigate(AppScreen.NennungsEingang) } + onClick = { onNavigate(AppScreen.NennungsEingang) }, + enabled = isConfigured ) NavRailItem( icon = Icons.Default.WifiTethering, label = "ConnectivityCheck", selected = currentScreen is AppScreen.ConnectivityCheck, - onClick = { onNavigate(AppScreen.ConnectivityCheck) } + onClick = { onNavigate(AppScreen.ConnectivityCheck) }, + enabled = true // Immer aktiv zur Diagnose ) Spacer(Modifier.weight(1f)) @@ -256,7 +266,8 @@ private fun DesktopNavRail( icon = Icons.Default.AppRegistration, label = "Setup", selected = currentScreen is AppScreen.DeviceInitialization, - onClick = { onNavigate(AppScreen.DeviceInitialization) } + onClick = { onNavigate(AppScreen.DeviceInitialization) }, + enabled = true ) } } @@ -268,9 +279,11 @@ private fun NavRailItem( icon: ImageVector, label: String, selected: Boolean, - onClick: () -> Unit + onClick: () -> Unit, + enabled: Boolean = true ) { - val tint = if (selected) MaterialTheme.colorScheme.primary else AppColors.NavigationContent + val contentAlpha = if (enabled) 1.0f else 0.38f + val tint = if (selected) MaterialTheme.colorScheme.primary else AppColors.NavigationContent.copy(alpha = contentAlpha) val background = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) else Color.Transparent TooltipBox( @@ -287,7 +300,7 @@ private fun NavRailItem( Surface( modifier = Modifier .size(48.dp) - .clickable(onClick = onClick), + .clickable(enabled = enabled, onClick = onClick), shape = MaterialTheme.shapes.medium, color = background ) { @@ -315,6 +328,7 @@ private fun DesktopTopHeader( onNavigate: (AppScreen) -> Unit, onBack: () -> Unit, onLogout: () -> Unit, + isAuthenticated: Boolean ) { Surface( modifier = Modifier.fillMaxWidth().height(Dimens.TopBarHeight), @@ -348,18 +362,34 @@ private fun DesktopTopHeader( horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM) ) { // Profil / Logout Bereich - Text( - text = "Administrator", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - IconButton(onClick = onLogout) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Logout, - contentDescription = "Abmelden", - modifier = Modifier.size(Dimens.IconSizeM), - tint = MaterialTheme.colorScheme.error + if (isAuthenticated) { + Text( + text = "Administrator", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) + IconButton(onClick = onLogout) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Logout, + contentDescription = "Abmelden", + modifier = Modifier.size(Dimens.IconSizeM), + tint = MaterialTheme.colorScheme.error + ) + } + } else { + Text( + text = "Gast", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + IconButton(onClick = { onNavigate(AppScreen.Login(returnTo = currentScreen)) }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Login, + contentDescription = "Anmelden", + modifier = Modifier.size(Dimens.IconSizeM), + tint = MaterialTheme.colorScheme.primary + ) + } } } }