From b11432df160e8629a9bf5f4ed62e36bcaa72dc5b Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Tue, 21 Apr 2026 17:02:12 +0200 Subject: [PATCH] chore: implementiere Ping-Screen mit UI-Logik, ViewModel und Preview-Komponenten Signed-off-by: StefanMoCoAt --- .../ping/presentation/PingActionGroup.kt | 102 ++++++++ .../features/ping/presentation/PingScreen.kt | 232 ++++++++++++++++++ .../ping/presentation/PingViewModel.kt | 156 ++++++++++++ .../ping/presentation/TerminalConsole.kt | 66 +++++ .../ping/presentation/PingScreenPreview.kt | 105 ++++++++ 5 files changed, 661 insertions(+) create mode 100644 frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingActionGroup.kt create mode 100644 frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreen.kt create mode 100644 frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingViewModel.kt create mode 100644 frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/TerminalConsole.kt create mode 100644 frontend/features/ping-feature/src/jvmMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreenPreview.kt diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingActionGroup.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingActionGroup.kt new file mode 100644 index 00000000..3b34c654 --- /dev/null +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingActionGroup.kt @@ -0,0 +1,102 @@ +package at.mocode.frontend.features.ping.presentation + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HealthAndSafety +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.NetworkCheck +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import at.mocode.frontend.core.designsystem.theme.Dimens + +/** + * Eine modulare Gruppe von Test-Buttons für die Konnektivitäts-Diagnose. + * Plug-and-Play fähig für Ping-Screen oder Sidebar. + */ +@Composable +fun PingActionGroup( + viewModel: PingViewModel, + modifier: Modifier = Modifier +) { + val uiState = viewModel.uiState + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS) + ) { + Text( + text = "DIAGNOSE-TESTS", + style = MaterialTheme.typography.labelSmall, + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, + modifier = Modifier.padding(bottom = Dimens.SpacingXS) + ) + + // Grid-ähnliches Layout für die Buttons + Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) { + PingTestButton( + text = "Simple Ping", + icon = Icons.Default.NetworkCheck, + onClick = { viewModel.performSimplePing() }, + isLoading = uiState.isLoading, + modifier = Modifier.weight(1f) + ) + PingTestButton( + text = "Secure Ping", + icon = Icons.Default.Lock, + onClick = { viewModel.performSecurePing() }, + isLoading = uiState.isLoading, + modifier = Modifier.weight(1f) + ) + } + + Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) { + PingTestButton( + text = "Health Check", + icon = Icons.Default.HealthAndSafety, + onClick = { viewModel.performHealthCheck() }, + isLoading = uiState.isLoading, + modifier = Modifier.weight(1f) + ) + PingTestButton( + text = "Delta Sync", + icon = Icons.Default.Sync, + onClick = { viewModel.triggerSync() }, + isLoading = uiState.isSyncing, + modifier = Modifier.weight(1f) + ) + } + + // Zusätzlicher Button für Enhanced Ping (Circuit Breaker Test) + OutlinedButton( + onClick = { viewModel.performEnhancedPing() }, + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading + ) { + Text("Enhanced Ping (Simulation)", fontSize = 12.sp) + } + } +} + +@Composable +private fun PingTestButton( + text: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + onClick: () -> Unit, + isLoading: Boolean, + modifier: Modifier = Modifier +) { + Button( + onClick = onClick, + modifier = modifier.height(48.dp), + enabled = !isLoading, + contentPadding = PaddingValues(horizontal = Dimens.SpacingS) + ) { + Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(Dimens.SpacingXS)) + Text(text, fontSize = 12.sp, maxLines = 1) + } +} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreen.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreen.kt new file mode 100644 index 00000000..20a219f3 --- /dev/null +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreen.kt @@ -0,0 +1,232 @@ +package at.mocode.frontend.features.ping.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import at.mocode.frontend.core.auth.presentation.AuthStatusCard +import at.mocode.frontend.core.auth.presentation.LoginViewModel +import at.mocode.frontend.core.designsystem.components.MsCard +import at.mocode.frontend.core.designsystem.theme.Dimens +import org.koin.compose.koinInject + +@Composable +fun PingScreen( + viewModel: PingViewModel, + onBack: () -> Unit = {}, + onNavigateToLogin: () -> Unit = {} +) { + val uiState = viewModel.uiState + val authViewModel: LoginViewModel = koinInject() + + // Wir nutzen jetzt das globale Theme (Hintergrund kommt vom Theme) + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(Dimens.SpacingS) // Globales Spacing + ) { + // 1. Header + PingHeader( + onBack = onBack, + isSyncing = uiState.isSyncing, + isLoading = uiState.isLoading + ) + + Spacer(Modifier.height(Dimens.SpacingS)) + + // 2. Auth Status Area (Plug-and-Play) + AuthStatusCard( + viewModel = authViewModel, + onLoginClick = onNavigateToLogin + ) + + Spacer(Modifier.height(Dimens.SpacingS)) + + // 3. Main Dashboard Area (Split View) + Row(modifier = Modifier.weight(1f)) { + // Left Panel: Controls & Status Grid (60%) + Column( + modifier = Modifier + .weight(0.6f) + .fillMaxHeight() + .padding(end = Dimens.SpacingS) + ) { + PingActionGroup(viewModel) + Spacer(Modifier.height(Dimens.SpacingS)) + StatusGrid(uiState) + } + + // Right Panel: Terminal Log (40%) + TerminalConsole( + logs = uiState.logs, + onClear = { viewModel.clearLogs() }, + modifier = Modifier + .weight(0.4f) + .fillMaxHeight() + ) + } + + Spacer(Modifier.height(Dimens.SpacingXS)) + + // 4. Footer + PingStatusBar(uiState.lastSyncResult) + } +} + +@Composable +private fun PingHeader( + onBack: () -> Unit, + isSyncing: Boolean, + isLoading: Boolean +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().height(40.dp) + ) { + Text( + "KONNEKTIVITÄTS-DIAGNOSE // DASHBOARD", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.weight(1f).padding(start = Dimens.SpacingS) + ) + + if (isLoading) { + StatusBadge("BUSY", Color(0xFFFFA000)) // Amber + Spacer(Modifier.width(Dimens.SpacingS)) + } + + if (isSyncing) { + StatusBadge("SYNCING", MaterialTheme.colorScheme.primary) + Spacer(Modifier.width(Dimens.SpacingS)) + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } else { + StatusBadge("IDLE", Color(0xFF388E3C)) // Green + } + } +} + +@Composable +private fun StatusBadge(text: String, color: Color) { + Surface( + color = color.copy(alpha = 0.1f), + contentColor = color, + shape = MaterialTheme.shapes.small, + border = androidx.compose.foundation.BorderStroke(1.dp, color) + ) { + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) + ) + } +} + +@Composable +private fun StatusGrid(uiState: PingUiState) { + Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) { + // Row 1 + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) { + MsCard(modifier = Modifier.weight(1f)) { + StatusHeader("SIMPLE / SECURE PING") + if (uiState.simplePingResponse != null) { + KeyValueRow("Status", uiState.simplePingResponse.status) + KeyValueRow("Service", uiState.simplePingResponse.service) + KeyValueRow("Time", uiState.simplePingResponse.timestamp) + } else { + EmptyStateText() + } + } + + MsCard(modifier = Modifier.weight(1f)) { + StatusHeader("HEALTH CHECK") + if (uiState.healthResponse != null) { + KeyValueRow("Status", uiState.healthResponse.status) + KeyValueRow("Healthy", uiState.healthResponse.healthy.toString()) + KeyValueRow("Service", uiState.healthResponse.service) + } else { + EmptyStateText() + } + } + } + + // Row 2 + MsCard(modifier = Modifier.fillMaxWidth()) { + StatusHeader("ENHANCED PING (RESILIENCE)") + if (uiState.enhancedPingResponse != null) { + Row(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.weight(1f)) { + KeyValueRow("Status", uiState.enhancedPingResponse.status) + KeyValueRow("Timestamp", uiState.enhancedPingResponse.timestamp) + } + Column(modifier = Modifier.weight(1f)) { + KeyValueRow("Circuit Breaker", uiState.enhancedPingResponse.circuitBreakerState) + KeyValueRow("Latency", "${uiState.enhancedPingResponse.responseTime}ms") + } + } + } else { + EmptyStateText() + } + } + } +} + +@Composable +private fun StatusHeader(title: String) { + Text( + text = title, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = Dimens.SpacingXS) + ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp) + Spacer(Modifier.height(Dimens.SpacingXS)) +} + +@Composable +private fun EmptyStateText() { + Text("No Data", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) +} + +@Composable +private fun KeyValueRow(key: String, value: String) { + Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { + Text( + text = "$key:", + modifier = Modifier.width(100.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Composable +private fun PingStatusBar(lastSync: String?) { + Surface( + color = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = lastSync ?: "Ready", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(horizontal = Dimens.SpacingS, vertical = 2.dp) + ) + } +} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingViewModel.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingViewModel.kt new file mode 100644 index 00000000..0573c013 --- /dev/null +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/PingViewModel.kt @@ -0,0 +1,156 @@ +package at.mocode.frontend.features.ping.presentation + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.mocode.frontend.features.ping.domain.PingSyncService +import at.mocode.ping.api.EnhancedPingResponse +import at.mocode.ping.api.HealthResponse +import at.mocode.ping.api.PingApi +import at.mocode.ping.api.PingResponse +import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock + +data class LogEntry( + val timestamp: String, + val source: String, + val message: String, + val isError: Boolean = false +) + +data class PingUiState( + val isLoading: Boolean = false, + val simplePingResponse: PingResponse? = null, + val enhancedPingResponse: EnhancedPingResponse? = null, + val healthResponse: HealthResponse? = null, + val errorMessage: String? = null, + val isSyncing: Boolean = false, + val lastSyncResult: String? = null, + val logs: List = emptyList() +) + +open class PingViewModel( + private val apiClient: PingApi, + private val syncService: PingSyncService +) : ViewModel() { + + var uiState by mutableStateOf(PingUiState()) + internal set + + private fun addLog(source: String, message: String, isError: Boolean = false) { + val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + val timeString = "${now.hour.toString().padStart(2, '0')}:${now.minute.toString().padStart(2, '0')}:${ + now.second.toString().padStart(2, '0') + }" + val entry = LogEntry(timeString, source, message, isError) + uiState = uiState.copy(logs = listOf(entry) + uiState.logs) // Prepend for newest first + } + + fun performSimplePing() { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + addLog("SimplePing", "Sending request...") + try { + val response = apiClient.simplePing() + uiState = uiState.copy( + isLoading = false, + simplePingResponse = response + ) + addLog("SimplePing", "Success: ${response.status} from ${response.service}") + } catch (e: Exception) { + val msg = "Simple ping failed: ${e.message}" + uiState = uiState.copy(isLoading = false, errorMessage = msg) + addLog("SimplePing", "Failed: ${e.message}", isError = true) + } + } + } + + fun performEnhancedPing(simulate: Boolean = false) { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + addLog("EnhancedPing", "Sending request (simulate=$simulate)...") + try { + val response = apiClient.enhancedPing(simulate) + uiState = uiState.copy( + isLoading = false, + enhancedPingResponse = response + ) + addLog("EnhancedPing", "Success: CB=${response.circuitBreakerState}, Time=${response.responseTime}ms") + } catch (e: Exception) { + val msg = "Enhanced ping failed: ${e.message}" + uiState = uiState.copy(isLoading = false, errorMessage = msg) + addLog("EnhancedPing", "Failed: ${e.message}", isError = true) + } + } + } + + fun performHealthCheck() { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + addLog("HealthCheck", "Checking system health...") + try { + val response = apiClient.healthCheck() + uiState = uiState.copy( + isLoading = false, + healthResponse = response + ) + addLog("HealthCheck", "Status: ${response.status}, Healthy: ${response.healthy}") + } catch (e: Exception) { + val msg = "Health check failed: ${e.message}" + uiState = uiState.copy(isLoading = false, errorMessage = msg) + addLog("HealthCheck", "Failed: ${e.message}", isError = true) + } + } + } + + fun performSecurePing() { + viewModelScope.launch { + uiState = uiState.copy(isLoading = true, errorMessage = null) + addLog("SecurePing", "Sending authenticated request...") + try { + val response = apiClient.securePing() + uiState = uiState.copy( + isLoading = false, + simplePingResponse = response + ) + addLog("SecurePing", "Success: Authorized access granted.") + } catch (e: Exception) { + val msg = "Secure ping failed: ${e.message}" + uiState = uiState.copy(isLoading = false, errorMessage = msg) + addLog("SecurePing", "Access Denied/Error: ${e.message}", isError = true) + } + } + } + + fun triggerSync() { + viewModelScope.launch { + uiState = uiState.copy(isSyncing = true, errorMessage = null) + addLog("Sync", "Starting delta sync...") + try { + syncService.syncPings() + val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + uiState = uiState.copy( + isSyncing = false, + lastSyncResult = "Sync successful at $now" + ) + addLog("Sync", "Sync completed successfully.") + } catch (e: Exception) { + val msg = "Sync failed: ${e.message}" + uiState = uiState.copy(isSyncing = false, errorMessage = msg) + addLog("Sync", "Sync failed: ${e.message}", isError = true) + } + } + } + + fun clearLogs() { + uiState = uiState.copy(logs = emptyList()) + } + + fun clearError() { + uiState = uiState.copy(errorMessage = null) + } +} diff --git a/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/TerminalConsole.kt b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/TerminalConsole.kt new file mode 100644 index 00000000..2c6d10f3 --- /dev/null +++ b/frontend/features/ping-feature/src/commonMain/kotlin/at/mocode/frontend/features/ping/presentation/TerminalConsole.kt @@ -0,0 +1,66 @@ +package at.mocode.frontend.features.ping.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import at.mocode.frontend.core.designsystem.theme.Dimens + +/** + * Eine universelle Terminal-Konsole zur Anzeige von Log-Einträgen. + * Plug-and-Play ist fähig für verschiedene Features (Ping, Sync, Auth-Logs). + */ +@Composable +fun TerminalConsole( + logs: List, + modifier: Modifier = Modifier, + onClear: () -> Unit = {} +) { + Column(modifier = modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = Dimens.SpacingXS), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("EVENT LOG", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold) + TextButton( + onClick = onClear, + contentPadding = PaddingValues(0.dp), + modifier = Modifier.height(24.dp) + ) { + Text("CLEAR", style = MaterialTheme.typography.labelSmall) + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF1E1E1E)) // Terminallook (Dunkel) + .padding(Dimens.SpacingXS) + ) { + items(logs) { log -> + val color = if (log.isError) Color(0xFFFF5555) else Color(0xFF55FF55) + Text( + text = "[${log.timestamp}] [${log.source}] ${log.message}", + color = color, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + lineHeight = 14.sp + ) + } + } + } +} diff --git a/frontend/features/ping-feature/src/jvmMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreenPreview.kt b/frontend/features/ping-feature/src/jvmMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreenPreview.kt new file mode 100644 index 00000000..d243f3b7 --- /dev/null +++ b/frontend/features/ping-feature/src/jvmMain/kotlin/at/mocode/frontend/features/ping/presentation/PingScreenPreview.kt @@ -0,0 +1,105 @@ +package at.mocode.frontend.features.ping.presentation + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import at.mocode.frontend.core.designsystem.preview.ComponentPreview +import at.mocode.frontend.features.ping.domain.PingSyncService +import at.mocode.ping.api.* + +// ───────────────────────────────────────────────────────────────────────────── +// Fake-Implementierungen für Preview (kein Koin, kein Netzwerk nötig) +// ───────────────────────────────────────────────────────────────────────────── + +private val fakePingResponse = PingResponse( + status = "OK", timestamp = "2026-03-26T12:00:00Z", service = "ping-service" +) + +private val fakeEnhancedResponse = EnhancedPingResponse( + status = "OK", timestamp = "2026-03-26T12:00:00Z", service = "ping-service", + circuitBreakerState = "CLOSED", responseTime = 42L +) + +private val fakeHealthResponse = HealthResponse( + status = "UP", timestamp = "2026-03-26T12:00:00Z", service = "ping-service", healthy = true +) + +private object FakePingApi : PingApi { + override suspend fun simplePing() = fakePingResponse + override suspend fun enhancedPing(simulate: Boolean) = fakeEnhancedResponse + override suspend fun healthCheck() = fakeHealthResponse + override suspend fun publicPing() = fakePingResponse + override suspend fun securePing() = fakePingResponse + override suspend fun syncPings(since: Long): List = emptyList() +} + +private object FakePingSyncService : PingSyncService { + override suspend fun syncPings() { /* no-op */ + } +} + +// Subclass um uiState für Preview direkt setzen zu können +private class PreviewPingViewModel(state: PingUiState) : + PingViewModel(FakePingApi, FakePingSyncService) { + init { + uiState = state + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Previews +// ───────────────────────────────────────────────────────────────────────────── + +@ComponentPreview +@Composable +fun PreviewPingScreen_Empty() { + MaterialTheme { + PingScreen( + viewModel = PreviewPingViewModel(PingUiState()), + onBack = {} + ) + } +} + +@ComponentPreview +@Composable +fun PreviewPingScreen_WithData() { + MaterialTheme { + PingScreen( + viewModel = PreviewPingViewModel( + PingUiState( + simplePingResponse = fakePingResponse, + healthResponse = fakeHealthResponse, + logs = listOf( + LogEntry("12:00:01", "SimplePing", "Success: OK from ping-service"), + LogEntry("12:00:00", "HealthCheck", "Status: UP, Healthy: true"), + ) + ) + ), + onBack = {} + ) + } +} + +@ComponentPreview +@Composable +fun PreviewPingScreen_Loading() { + MaterialTheme { + PingScreen( + viewModel = PreviewPingViewModel(PingUiState(isLoading = true, isSyncing = true)), + onBack = {} + ) + } +} + +@ComponentPreview +@Composable +fun PreviewPingScreen_Error() { + MaterialTheme { + PingScreen( + viewModel = PreviewPingViewModel( + PingUiState(errorMessage = "Connection refused: Backend nicht erreichbar") + ), + onBack = {} + ) + } +}