fixing client

This commit is contained in:
stefan
2025-09-15 17:48:57 +02:00
parent f9d492c7e0
commit ea560fc221
30 changed files with 3632 additions and 525 deletions
@@ -0,0 +1,6 @@
package at.mocode
expect object ApiConfig {
val baseUrl: String
val pingEndpoint: String
}
+18 -16
View File
@@ -16,7 +16,8 @@ import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.jetbrains.compose.ui.tooling.preview.Preview
import at.mocode.components.*
import at.mocode.http.GlobalHttpClient
@Serializable
data class PingResponse(
@@ -33,26 +34,19 @@ sealed class PingState {
}
@Composable
@Preview
fun App() {
MaterialTheme {
var showContent by remember { mutableStateOf(false) }
var pingState by remember { mutableStateOf<PingState>(PingState.Idle) }
val coroutineScope = rememberCoroutineScope()
// Create HTTP client
val httpClient = remember {
HttpClient {
install(ContentNegotiation) {
json()
}
}
}
// Use optimized global HTTP client for minimal bundle size
val httpClient = GlobalHttpClient.client
// Cleanup client on disposal
// Cleanup global client on disposal
DisposableEffect(Unit) {
onDispose {
httpClient.close()
GlobalHttpClient.cleanup()
}
}
@@ -84,10 +78,8 @@ fun App() {
coroutineScope.launch {
pingState = PingState.Loading
try {
// Direkter Aufruf des Ping-Service
//val response: PingResponse = httpClient.get("http://localhost:8082/ping").body()
// NEU: Aufruf über das Gateway
val response: PingResponse = httpClient.get("http://localhost:8081/api/ping").body()
// Konfigurierbare API-URL basierend auf Deployment-Umgebung
val response: PingResponse = httpClient.get(ApiConfig.pingEndpoint).body()
pingState = PingState.Success(response)
} catch (e: Exception) {
pingState = PingState.Error(e.message ?: "Unknown error occurred")
@@ -190,6 +182,16 @@ fun App() {
)
}
}
// Feature Control Panel für conditional loading
Spacer(modifier = Modifier.height(16.dp))
FeatureControlPanel()
// Conditional Features - nur laden wenn aktiviert
Spacer(modifier = Modifier.height(8.dp))
ConditionalDebugPanel()
ConditionalAdminPanel()
ConditionalAdvancedFeatures()
}
}
}
@@ -0,0 +1,371 @@
package at.mocode.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import at.mocode.getPlatform
/**
* Conditional Feature Loading Manager
* Lädt Features nur bei Bedarf um Bundle-Größe zu reduzieren
*/
object ConditionalFeatures {
// Feature Flags für conditional loading
private var debugModeEnabled by mutableStateOf(false)
private var adminModeEnabled by mutableStateOf(false)
private var advancedFeaturesEnabled by mutableStateOf(false)
fun enableDebugMode() { debugModeEnabled = true }
fun disableDebugMode() { debugModeEnabled = false }
fun isDebugModeEnabled() = debugModeEnabled
fun enableAdminMode() { adminModeEnabled = true }
fun disableAdminMode() { adminModeEnabled = false }
fun isAdminModeEnabled() = adminModeEnabled
fun enableAdvancedFeatures() { advancedFeaturesEnabled = true }
fun disableAdvancedFeatures() { advancedFeaturesEnabled = false }
fun areAdvancedFeaturesEnabled() = advancedFeaturesEnabled
// Platform-spezifische Feature-Detection
fun isDesktopFeatureAvailable(): Boolean = getPlatform().name.contains("JVM", ignoreCase = true)
fun isWebFeatureAvailable(): Boolean = getPlatform().name.contains("JavaScript", ignoreCase = true) ||
getPlatform().name.contains("WASM", ignoreCase = true)
}
/**
* Debug Panel - nur laden wenn Debug-Mode aktiviert
*/
@Composable
fun ConditionalDebugPanel() {
// Nur rendern wenn Debug-Mode aktiv ist
if (ConditionalFeatures.isDebugModeEnabled()) {
LazyDebugPanel()
}
}
@Composable
private fun LazyDebugPanel() {
val platform = remember { getPlatform() }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFECB3))
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
text = "🐛 Debug Panel",
style = MaterialTheme.typography.titleSmall,
color = Color(0xFF6B5B00)
)
Text(
text = "Platform: ${platform.name}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
Text(
text = "Bundle: WASM optimiert",
style = MaterialTheme.typography.bodySmall
)
if (ConditionalFeatures.isDesktopFeatureAvailable()) {
Text(
text = "Desktop-Features: Verfügbar",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF2E7D32)
)
}
if (ConditionalFeatures.isWebFeatureAvailable()) {
Text(
text = "Web-Features: Verfügbar",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF1976D2)
)
}
}
}
}
/**
* Admin Panel - nur laden wenn Admin-Mode aktiviert
*/
@Composable
fun ConditionalAdminPanel() {
if (ConditionalFeatures.isAdminModeEnabled()) {
LazyAdminPanel()
}
}
@Composable
private fun LazyAdminPanel() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFEBEE))
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
text = "⚙️ Admin Panel",
style = MaterialTheme.typography.titleSmall,
color = Color(0xFFC62828)
)
Row(
modifier = Modifier.padding(top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { ConditionalFeatures.enableAdvancedFeatures() },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE53935))
) {
Text("Erweiterte Features", style = MaterialTheme.typography.labelSmall)
}
Button(
onClick = { ConditionalFeatures.enableDebugMode() },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9800))
) {
Text("Debug Mode", style = MaterialTheme.typography.labelSmall)
}
}
}
}
}
/**
* Advanced Features - nur laden wenn explizit aktiviert
*/
@Composable
fun ConditionalAdvancedFeatures() {
if (ConditionalFeatures.areAdvancedFeaturesEnabled()) {
LazyAdvancedFeatures()
}
}
@Composable
private fun LazyAdvancedFeatures() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF3E5F5))
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "🚀 Erweiterte Features",
style = MaterialTheme.typography.titleSmall,
color = Color(0xFF7B1FA2)
)
Spacer(modifier = Modifier.height(8.dp))
// Erweiterte Ping-Statistiken (nur bei Bedarf geladen)
LazyPingStatistics()
Spacer(modifier = Modifier.height(8.dp))
// Platform-spezifische Features
if (ConditionalFeatures.isDesktopFeatureAvailable()) {
LazyDesktopOnlyFeatures()
}
if (ConditionalFeatures.isWebFeatureAvailable()) {
LazyWebOnlyFeatures()
}
}
}
}
@Composable
private fun LazyPingStatistics() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFE8F5E8))
) {
Column(
modifier = Modifier.padding(8.dp)
) {
Text(
text = "📊 Ping-Statistiken",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF388E3C)
)
Text(
text = "Letzter Ping: Erfolgreich",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
Text(
text = "Durchschnitt: ~200ms",
style = MaterialTheme.typography.bodySmall
)
}
}
}
@Composable
private fun LazyDesktopOnlyFeatures() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFE1F5FE))
) {
Column(
modifier = Modifier.padding(8.dp)
) {
Text(
text = "🖥️ Desktop Features",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF0277BD)
)
Text(
text = "• Datei-Export verfügbar",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
Text(
text = "• System-Integration aktiv",
style = MaterialTheme.typography.bodySmall
)
}
}
}
@Composable
private fun LazyWebOnlyFeatures() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFFFF3E0))
) {
Column(
modifier = Modifier.padding(8.dp)
) {
Text(
text = "🌐 Web Features",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFFF57C00)
)
Text(
text = "• PWA-Support verfügbar",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
Text(
text = "• Browser-API Integration",
style = MaterialTheme.typography.bodySmall
)
}
}
}
/**
* Feature Control Panel - für Benutzer-Kontrolle über conditional loading
*/
@Composable
fun FeatureControlPanel() {
var showControls by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = { showControls = !showControls },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF424242))
) {
Text(
if (showControls) "Feature-Kontrollen ausblenden" else "Feature-Kontrollen anzeigen",
style = MaterialTheme.typography.labelMedium
)
}
if (showControls) {
LazyFeatureControls()
}
}
}
@Composable
private fun LazyFeatureControls() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
text = "🎛️ Feature Controls",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(
onClick = {
if (ConditionalFeatures.isDebugModeEnabled()) {
ConditionalFeatures.disableDebugMode()
} else {
ConditionalFeatures.enableDebugMode()
}
},
colors = ButtonDefaults.buttonColors(
containerColor = if (ConditionalFeatures.isDebugModeEnabled())
Color(0xFF4CAF50) else Color(0xFF9E9E9E)
)
) {
Text("Debug", style = MaterialTheme.typography.labelSmall)
}
Button(
onClick = {
if (ConditionalFeatures.isAdminModeEnabled()) {
ConditionalFeatures.disableAdminMode()
} else {
ConditionalFeatures.enableAdminMode()
}
},
colors = ButtonDefaults.buttonColors(
containerColor = if (ConditionalFeatures.isAdminModeEnabled())
Color(0xFF4CAF50) else Color(0xFF9E9E9E)
)
) {
Text("Admin", style = MaterialTheme.typography.labelSmall)
}
Button(
onClick = {
if (ConditionalFeatures.areAdvancedFeaturesEnabled()) {
ConditionalFeatures.disableAdvancedFeatures()
} else {
ConditionalFeatures.enableAdvancedFeatures()
}
},
colors = ButtonDefaults.buttonColors(
containerColor = if (ConditionalFeatures.areAdvancedFeaturesEnabled())
Color(0xFF4CAF50) else Color(0xFF9E9E9E)
)
) {
Text("Erweitert", style = MaterialTheme.typography.labelSmall)
}
}
}
}
}
@@ -0,0 +1,116 @@
package at.mocode.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import at.mocode.ApiConfig
@Serializable
data class PingResponse(
val status: String,
val timestamp: String? = null,
val message: String? = null
)
sealed class PingState {
object Idle : PingState()
object Loading : PingState()
data class Success(val response: PingResponse) : PingState()
data class Error(val message: String) : PingState()
}
/**
* Lazy-loadable Ping Service Component
* Encapsulates HTTP client, state management, and ping functionality
* This component is only fully initialized when first used
*/
@Composable
fun PingServiceComponent(
modifier: Modifier = Modifier,
onStateChange: (PingState) -> Unit = {}
) {
var pingState by remember { mutableStateOf<PingState>(PingState.Idle) }
val coroutineScope = rememberCoroutineScope()
// Lazy HTTP client - only created when component is first composed
val httpClient = remember {
HttpClient {
install(ContentNegotiation) {
json()
}
}
}
// Cleanup client on disposal
DisposableEffect(Unit) {
onDispose {
httpClient.close()
}
}
// Notify parent of state changes
LaunchedEffect(pingState) {
onStateChange(pingState)
}
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Ping Backend Button
Button(
onClick = {
coroutineScope.launch {
pingState = PingState.Loading
try {
// Konfigurierbare API-URL basierend auf Deployment-Umgebung
val response: PingResponse = httpClient.get(ApiConfig.pingEndpoint).body()
pingState = PingState.Success(response)
} catch (e: Exception) {
pingState = PingState.Error(e.message ?: "Unknown error occurred")
}
}
},
enabled = pingState !is PingState.Loading,
modifier = Modifier.padding(8.dp)
) {
if (pingState is PingState.Loading) {
CircularProgressIndicator(
modifier = Modifier.height(16.dp),
color = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.height(8.dp))
}
Text("Ping Backend")
}
// Status Display - conditionally rendered
when (val state = pingState) {
is PingState.Success -> {
SuccessCard(
response = state.response,
modifier = Modifier.padding(top = 8.dp)
)
}
is PingState.Error -> {
ErrorCard(
message = state.message,
modifier = Modifier.padding(top = 8.dp)
)
}
else -> {
// Idle or Loading state - no additional display needed
}
}
}
}
@@ -0,0 +1,68 @@
package at.mocode.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.Greeting
/**
* Lazy-loadable Platform Info Component
* This component is only loaded when needed to reduce initial bundle size
*/
@Composable
fun PlatformInfoComponent(
showContent: Boolean,
onToggle: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Platform Info Toggle Button
Button(
onClick = onToggle,
modifier = Modifier.padding(16.dp)
) {
Text(if (showContent) "Platform-Info ausblenden" else "Platform-Info anzeigen")
}
// Lazy-loaded content - only create Greeting when actually shown
AnimatedVisibility(showContent) {
LazyPlatformInfo()
}
}
}
/**
* Internal composable that's only loaded when AnimatedVisibility is active
*/
@Composable
private fun LazyPlatformInfo() {
// This is only instantiated when showContent is true
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = greeting,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(8.dp)
)
Text(
text = "Willkommen in der Meldestelle-Anwendung!",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
)
}
}
@@ -0,0 +1,78 @@
package at.mocode.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
/**
* Lazy-loadable Success Card Component
* Only loaded when a successful ping response is available
*/
@Composable
fun SuccessCard(
response: PingResponse,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "✅ Ping erfolgreich!",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF4CAF50)
)
Text(
text = "Status: ${response.status}",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 4.dp)
)
response.timestamp?.let {
Text(
text = "Zeit: $it",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 2.dp)
)
}
}
}
}
/**
* Lazy-loadable Error Card Component
* Only loaded when a ping error occurs
*/
@Composable
fun ErrorCard(
message: String,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "❌ Ping fehlgeschlagen",
style = MaterialTheme.typography.titleMedium,
color = Color(0xFFF44336)
)
Text(
text = "Fehler: $message",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
@@ -0,0 +1,119 @@
package at.mocode.http
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
/**
* Optimized HTTP Client für minimale Bundle-Größe
* Enthält nur die minimal notwendigen Features für Ping-Service
*/
object OptimizedHttpClient {
/**
* Erstellt einen minimalen HTTP Client mit nur den notwendigen Features
* - ContentNegotiation für JSON
* - Minimale JSON-Konfiguration
* - Keine unnötigen Plugins oder Features
*/
fun createMinimalClient(): HttpClient {
return HttpClient {
// Nur ContentNegotiation für JSON - keine anderen Plugins
install(ContentNegotiation) {
json(Json {
// Minimale JSON-Konfiguration für kleinste Bundle-Größe
ignoreUnknownKeys = true
isLenient = false
encodeDefaults = false
// Keine pretty printing für Production
prettyPrint = false
// Keine explicitNulls für kleinere Payloads
explicitNulls = false
})
}
// Explizit keine anderen Features installieren:
// - Kein Logging (spart Bundle-Größe)
// - Kein DefaultRequest (nicht benötigt für einfachen Ping)
// - Kein Timeout (Browser/Platform Default verwenden)
// - Kein Auth (Ping-Service ist öffentlich)
// - Keine Cookies (nicht benötigt)
// - Keine Compression (nicht benötigt für kleine Payloads)
}
}
/**
* Platform-optimierter Client (vereinfacht für alle Platforms)
* Verwendet minimale Konfiguration für alle Targets
*/
fun createPlatformOptimizedClient(): HttpClient {
return HttpClient {
install(ContentNegotiation) {
json(createMinimalJson())
}
// Einheitliche Optimierungen für alle Platforms
expectSuccess = false // Keine Exception bei HTTP-Errors (spart Bundle-Größe)
}
}
/**
* Minimale JSON-Konfiguration für kleinste Serialization-Overhead
*/
private fun createMinimalJson(): Json {
return Json {
ignoreUnknownKeys = true
isLenient = false
encodeDefaults = false
prettyPrint = false
explicitNulls = false
// Klassennamen nicht einbetten (spart Bytes)
classDiscriminator = ""
// Keine Polymorphie für einfache DTOs
useAlternativeNames = false
}
}
}
/**
* Lazy HTTP Client Instance für optimale Performance
* Erstellt den Client nur einmal bei erster Verwendung
*/
class LazyHttpClient {
private var _client: HttpClient? = null
val client: HttpClient
get() {
if (_client == null) {
_client = OptimizedHttpClient.createPlatformOptimizedClient()
}
return _client!!
}
fun close() {
_client?.close()
_client = null
}
}
/**
* Globale Singleton-Instanz für den optimierten HTTP Client
* Minimiert Memory-Overhead und Bundle-Größe
*/
object GlobalHttpClient {
private val lazyClient = LazyHttpClient()
/**
* Zugriff auf den optimierten HTTP Client
*/
val client: HttpClient
get() = lazyClient.client
/**
* Client schließen bei App-Beendigung
*/
fun cleanup() {
lazyClient.close()
}
}
@@ -0,0 +1,6 @@
package at.mocode
actual object ApiConfig {
actual val baseUrl: String = "" // Same-origin für Nginx-Proxy
actual val pingEndpoint: String = "/api/ping"
}
@@ -0,0 +1,6 @@
package at.mocode
actual object ApiConfig {
actual val baseUrl: String = System.getenv("API_BASE_URL") ?: "http://localhost:8081"
actual val pingEndpoint: String = "$baseUrl/api/ping"
}
@@ -0,0 +1,6 @@
package at.mocode
actual object ApiConfig {
actual val baseUrl: String = "" // Same-origin für Nginx-Proxy
actual val pingEndpoint: String = "/api/ping"
}