chore: implementiere Auth-Status-abhängige Navigation und Icons, deaktiviere Module ohne Initialisierung und passe NavRail sowie Header für besseren UX an

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-20 14:23:44 +02:00
parent a1bf93342e
commit 8806d11e3c
4 changed files with 112 additions and 45 deletions
@@ -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.*
@@ -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)
@@ -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
)
}
}
@@ -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
)
}
}
}
}