Merge pull request #18

* MP-19 Refactoring: Einführung der "Registry" & "Masterdata" Trennung …

* MP-19 Refactoring: Frontend Tabula Rasa

* MP-19 Refactoring: Frontend Tabula Rasa

* refactoring:

* MP-20 fix(docker/clients): include `:domains` module in web/desktop b…

* MP-20 fix(web-app build): resolve JS compile error and add dev/prod b…

* MP-20 fix(web-app): remove vendor.js reference and harden JS bootstra…

* MP-20 fixing: clients

* MP-20 fixing: clients
This commit is contained in:
StefanMo
2025-11-30 14:13:12 +01:00
committed by GitHub
parent 596a05b69c
commit 9ea2b74a81
254 changed files with 5485 additions and 15971 deletions
+8 -4
View File
@@ -44,7 +44,6 @@ kotlin {
webpackTask {
mainOutputFileName = "web-app.js"
output.libraryTarget = "commonjs2"
}
// Development Server konfigurieren
@@ -67,7 +66,10 @@ kotlin {
// WASM, nur wenn explizit aktiviert
if (enableWasm) {
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs { browser() }
wasmJs {
browser()
binaries.executable()
}
}
sourceSets {
@@ -76,8 +78,8 @@ kotlin {
implementation(project(":clients:shared"))
implementation(project(":clients:shared:common-ui"))
implementation(project(":clients:shared:navigation"))
implementation(project(":clients:auth-feature"))
implementation(project(":clients:ping-feature"))
implementation(project(":clients:members-feature"))
// Compose Multiplatform
implementation(compose.runtime)
@@ -129,7 +131,9 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
jvmTarget.set(JvmTarget.JVM_21)
freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-Xskip-metadata-version-check" // Für bleeding-edge Versionen
"-Xskip-metadata-version-check", // Für bleeding-edge Versionen
// Suppress beta warning for expect/actual declarations used in this module
"-Xexpect-actual-classes"
)
}
}
-3833
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -1,12 +0,0 @@
{
"devDependencies": {
"@types/jest": "^29.2.5",
"jest": "^29.3.1"
},
"scripts": {
"test": "jest"
},
"dependencies": {
"webpack-bundle-analyzer": "^4.10.2"
}
}
+238 -87
View File
@@ -2,107 +2,258 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import at.mocode.clients.membersfeature.ProfileScreen
import at.mocode.clients.membersfeature.ProfileViewModel
import androidx.compose.runtime.collectAsState
import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.authfeature.AuthenticatedHttpClient
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.clients.pingfeature.PingScreen
import at.mocode.clients.pingfeature.PingViewModel
import at.mocode.clients.shared.core.AppConstants
import androidx.compose.material3.OutlinedTextField
import androidx.compose.ui.text.input.PasswordVisualTransformation
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
import at.mocode.clients.authfeature.AuthApiClient
import at.mocode.clients.authfeature.oauth.OAuthPkceService
import at.mocode.clients.authfeature.oauth.AuthCallbackParams
import at.mocode.clients.authfeature.oauth.CallbackParams
@Composable
fun MainApp() {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var currentScreen by remember { mutableStateOf<AppScreen>(AppScreen.Home) }
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var currentScreen by remember { mutableStateOf<AppScreen>(AppScreen.Home) }
when (currentScreen) {
is AppScreen.Home -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
is AppScreen.Login -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
is AppScreen.Ping -> DevelopmentScreen(onOpenProfile = { currentScreen = AppScreen.Profile })
is AppScreen.Profile -> ProfileScreen(viewModel = remember { ProfileViewModel() })
val authTokenManager = remember { AuthenticatedHttpClient.getAuthTokenManager() }
val pingViewModel = remember { PingViewModel() }
val scope = rememberCoroutineScope()
// Handle PKCE callback on an app load (web)
LaunchedEffect(Unit) {
val callback: CallbackParams? = AuthCallbackParams.parse()
if (callback != null) {
val code = callback.code
val state = callback.state
val pkce = OAuthPkceService.current()
if (pkce != null && pkce.state == state) {
val api = AuthApiClient()
val res = api.exchangeAuthorizationCode(code, pkce.codeVerifier, AppConstants.webRedirectUri())
val token = res.token
if (res.success && token != null) {
authTokenManager.setToken(token)
OAuthPkceService.clear()
currentScreen = AppScreen.Profile
}
}
}
}
when (currentScreen) {
is AppScreen.Home -> WelcomeScreen(
authTokenManager = authTokenManager,
onOpenPing = { AppScreen.Ping },
onOpenLogin = {
// Fallback to the local LoginScreen (Password Grant) if PKCE cannot be started
currentScreen = AppScreen.Login
},
onOpenProfile = { currentScreen = AppScreen.Profile }
)
is AppScreen.Login -> LoginScreen(
authTokenManager = authTokenManager,
onLoginSuccess = { currentScreen = AppScreen.Profile }
)
is AppScreen.Ping -> PingScreen(viewModel = pingViewModel)
is AppScreen.Profile -> AuthStatusScreen(
authTokenManager = authTokenManager,
onBackToHome = { currentScreen = AppScreen.Home }
)
else -> {}
}
}
}
}
@Composable
fun DevelopmentScreen(onOpenProfile: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"🚀 Meldestelle Development Mode",
style = MaterialTheme.typography.headlineMedium
)
private fun WelcomeScreen(
authTokenManager: AuthTokenManager,
onOpenPing: () -> Unit,
onOpenLogin: () -> Unit,
onOpenProfile: () -> Unit
) {
val authState by authTokenManager.authState.collectAsState()
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"🌐 Backend Connectivity",
style = MaterialTheme.typography.titleMedium
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Willkommen zur Meldestelle",
style = MaterialTheme.typography.headlineMedium
)
var testStatus by remember { mutableStateOf("Not tested") }
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { testStatus = "Testing Gateway..." }) {
Text("Test Gateway")
}
Button(onClick = { testStatus = "Testing Ping Service..." }) {
Text("Test Ping Service")
}
Button(onClick = onOpenProfile) {
Text("Open Profile")
}
}
Text("Status: $testStatus")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"🏓 Ping Service Tests",
style = MaterialTheme.typography.titleMedium
)
var isDarkMode by remember { mutableStateOf(false) }
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { /* TODO: Health Check */ }) {
Text("Health Check")
}
Button(onClick = { /* TODO: Ping Normal */ }) {
Text("Ping Normal")
}
Button(onClick = { isDarkMode = !isDarkMode }) {
Text("Toggle Dark Mode")
}
}
Text("Dark Mode: ${if(isDarkMode) "🌙 Enabled" else "☀️ Disabled"}")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"✅ System Status",
style = MaterialTheme.typography.titleMedium
)
Text("Frontend: 🟢 Running")
Text("Backend: ⚠️ Testing needed")
Text("Build: ✅ Successful")
}
// Auth info
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
if (authState.isAuthenticated) {
Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = onOpenProfile) { Text("Profil anzeigen") }
} else {
Text("Du bist nicht angemeldet.")
}
}
}
// Actions
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onOpenPing, modifier = Modifier.weight(1f)) { Text("Ping-Service") }
if (!authState.isAuthenticated) {
Button(
onClick = {
// Try PKCE login (Authorization Code Flow w/ PKCE)
scope.launch {
try {
val pkce = OAuthPkceService.startAuth()
val url = OAuthPkceService.buildAuthorizeUrl(pkce, AppConstants.webRedirectUri())
uriHandler.openUri(url)
} catch (_: Throwable) {
// Fallback: open the local Login screen (Password Grant)
onOpenLogin()
}
}
},
modifier = Modifier.weight(1f)
) { Text("Login") }
}
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(
onClick = { uriHandler.openUri(AppConstants.registerUrl()) },
modifier = Modifier.weight(1f)
) { Text("Registrieren (Keycloak)") }
OutlinedButton(
onClick = { uriHandler.openUri(AppConstants.loginUrl()) },
modifier = Modifier.weight(1f)
) { Text("Keycloak Login-Seite") }
}
// Desktop Download Link
OutlinedButton(
onClick = { uriHandler.openUri(AppConstants.desktopDownloadUrl()) },
modifier = Modifier.fillMaxWidth()
) { Text("Desktop-App herunterladen") }
}
}
@Composable
private fun AuthStatusScreen(
authTokenManager: AuthTokenManager,
onBackToHome: () -> Unit
) {
val authState by authTokenManager.authState.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Profil / Status", style = MaterialTheme.typography.headlineMedium)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
if (authState.isAuthenticated) {
Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = {
authTokenManager.clearToken()
onBackToHome()
}) { Text("Abmelden") }
} else {
Text("Nicht angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = onBackToHome) { Text("Zurück zur Startseite") }
}
}
}
}
}
@Composable
private fun LoginScreen(
authTokenManager: AuthTokenManager,
onLoginSuccess: () -> Unit
) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var error by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val api = remember { AuthApiClient() }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("Anmeldung", style = MaterialTheme.typography.headlineMedium)
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Benutzername") },
singleLine = true,
enabled = !isLoading,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Passwort") },
singleLine = true,
enabled = !isLoading,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
error?.let {
Text(it, color = MaterialTheme.colorScheme.error)
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = {
error = null
isLoading = true
scope.launch {
val res = api.login(username.trim(), password)
val token = res.token
if (res.success && token != null) {
authTokenManager.setToken(token)
isLoading = false
onLoginSuccess()
} else {
isLoading = false
error = res.message ?: "Login fehlgeschlagen"
}
}
},
enabled = !isLoading && username.isNotBlank() && password.isNotBlank()
) { Text(if (isLoading) "Bitte warten…" else "Login") }
}
}
}
@@ -1,65 +0,0 @@
package at.mocode.clients.app
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import at.mocode.clients.shared.commonui.components.AppHeader
import at.mocode.clients.shared.commonui.components.AppScaffold
import at.mocode.clients.shared.commonui.theme.AppTheme
import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.pingfeature.PingScreen
import at.mocode.clients.pingfeature.PingViewModel
import at.mocode.clients.authfeature.LoginScreen
import at.mocode.clients.authfeature.AuthTokenManager
import androidx.compose.runtime.collectAsState
@Composable
fun App() {
var currentScreen: AppScreen by remember { mutableStateOf(AppScreen.Home) }
// Create a single PingViewModel instance for the lifetime of the App composition.
val pingViewModel: PingViewModel = remember { PingViewModel() }
// Create a single AuthTokenManager instance for the lifetime of the App composition.
val authTokenManager: AuthTokenManager = remember { AuthTokenManager() }
// Observe authentication state
val authState by authTokenManager.authState.collectAsState()
AppTheme {
AppScaffold(
header = {
AppHeader(
title = "Meldestelle",
onNavigateToPing = { currentScreen = AppScreen.Ping },
onNavigateToLogin = { currentScreen = AppScreen.Login },
onLogout = {
authTokenManager.clearToken()
currentScreen = AppScreen.Home
},
isAuthenticated = authState.isAuthenticated,
username = authState.username,
userPermissions = authState.permissions.map { it.name }
)
},
{ paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
when (currentScreen) {
is AppScreen.Home -> {
LandingScreen(authTokenManager = authTokenManager)
}
is AppScreen.Login -> {
LoginScreen(
authTokenManager = authTokenManager,
onLoginSuccess = { currentScreen = AppScreen.Home }
)
}
is AppScreen.Ping -> {
PingScreen(viewModel = pingViewModel)
}
}
}
}
)
}
}
@@ -1,232 +0,0 @@
package at.mocode.clients.app
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.clients.authfeature.Permission
@Composable
fun LandingScreen(
authTokenManager: AuthTokenManager? = null
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Text(
text = "Willkommen bei Meldestelle",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Eine moderne, skalierbare Frontend-Architektur",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Diese Anwendung demonstriert eine \"Shell + Feature-Module\"-Architektur " +
"basierend auf Kotlin Multiplatform. Sie spiegelt die DDD-Struktur des Backends " +
"wider und ist als native Desktop-Anwendung (JVM) und Web-Anwendung (JS/Wasm) lauffähig.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "🚀 Technologien:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
TechItem("Kotlin Multiplatform")
TechItem("Jetpack Compose Multiplatform")
TechItem("Material Design 3")
TechItem("Ktor Client")
TechItem("Domain-Driven Design")
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Verwenden Sie das Ping Service Menü oben, um die API-Funktionalität zu testen.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// Permission-based UI demonstration
authTokenManager?.let { tokenManager ->
val authState by tokenManager.authState.collectAsState()
if (authState.isAuthenticated && authState.permissions.isNotEmpty()) {
Spacer(modifier = Modifier.height(32.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "🔐 Verfügbare Funktionen",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Spacer(modifier = Modifier.height(16.dp))
// Admin features (visible only to users with delete permissions)
if (tokenManager.isAdmin()) {
PermissionCard(
title = "👑 Administrator-Bereich",
description = "Vollzugriff auf alle System-Funktionen",
permissions = listOf("Alle Berechtigungen", "System-Verwaltung", "Benutzer-Management"),
backgroundColor = MaterialTheme.colorScheme.errorContainer,
textColor = MaterialTheme.colorScheme.onErrorContainer
)
}
// Management features (visible to users with create/update permissions)
if (tokenManager.canCreate() || tokenManager.canUpdate()) {
PermissionCard(
title = "✏️ Verwaltung",
description = "Erstellen und bearbeiten von Daten",
permissions = buildList {
if (tokenManager.hasPermission(Permission.PERSON_CREATE)) add("Personen erstellen")
if (tokenManager.hasPermission(Permission.PERSON_UPDATE)) add("Personen bearbeiten")
if (tokenManager.hasPermission(Permission.VEREIN_CREATE)) add("Vereine erstellen")
if (tokenManager.hasPermission(Permission.VEREIN_UPDATE)) add("Vereine bearbeiten")
if (tokenManager.hasPermission(Permission.PFERD_CREATE)) add("Pferde erstellen")
if (tokenManager.hasPermission(Permission.PFERD_UPDATE)) add("Pferde bearbeiten")
if (tokenManager.hasPermission(Permission.VERANSTALTUNG_CREATE)) add("Veranstaltungen erstellen")
if (tokenManager.hasPermission(Permission.VERANSTALTUNG_UPDATE)) add("Veranstaltungen bearbeiten")
},
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
textColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
// Read-only features (visible to all authenticated users)
if (tokenManager.canRead()) {
PermissionCard(
title = "👁️ Ansicht",
description = "Nur-Lese-Zugriff auf Daten",
permissions = buildList {
if (tokenManager.hasPermission(Permission.PERSON_READ)) add("Personen anzeigen")
if (tokenManager.hasPermission(Permission.VEREIN_READ)) add("Vereine anzeigen")
if (tokenManager.hasPermission(Permission.PFERD_READ)) add("Pferde anzeigen")
if (tokenManager.hasPermission(Permission.VERANSTALTUNG_READ)) add("Veranstaltungen anzeigen")
},
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
textColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
@Composable
private fun TechItem(text: String) {
Text(
text = "• $text",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
)
}
@Composable
private fun PermissionCard(
title: String,
description: String,
permissions: List<String>,
backgroundColor: androidx.compose.ui.graphics.Color,
textColor: androidx.compose.ui.graphics.Color
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = backgroundColor
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = textColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = textColor
)
if (permissions.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
permissions.forEach { permission ->
Text(
text = "✓ $permission",
style = MaterialTheme.typography.bodySmall,
color = textColor,
modifier = Modifier.padding(vertical = 2.dp)
)
}
}
}
}
}
@@ -1,202 +0,0 @@
package screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.clients.shared.presentation.store.AppStore
import at.mocode.clients.shared.presentation.state.AppState
import at.mocode.clients.pingfeature.PingViewModel
import at.mocode.ping.api.HealthResponse
import at.mocode.ping.api.PingResponse
import at.mocode.ping.api.EnhancedPingResponse
@Composable
fun DevelopmentScreen(appStore: AppStore) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"🚀 Meldestelle Development Mode",
style = MaterialTheme.typography.headlineMedium
)
// Backend Connectivity Tests
BackendTestSection()
// Ping Service Test
PingTestSection()
// State Debugging
StateDebugSection(appStore)
}
}
@Composable
fun BackendTestSection() {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("🌐 Backend Connectivity", style = MaterialTheme.typography.titleMedium)
var testStatus by remember { mutableStateOf("Not tested") }
var isLoading by remember { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = {
// TODO: Test Gateway Connection
isLoading = true
testStatus = "Testing..."
},
enabled = !isLoading
) {
Text("Test Gateway")
}
Button(
onClick = {
// TODO: Test Ping Service Direct
isLoading = true
testStatus = "Testing direct connection..."
},
enabled = !isLoading
) {
Text("Test Ping Service")
}
}
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.padding(8.dp))
}
Text("Status: $testStatus")
}
}
}
@Composable
fun PingTestSection() {
val pingViewModel = remember { PingViewModel() }
val uiState = pingViewModel.uiState
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("🏓 Ping Service Integration", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { pingViewModel.performHealthCheck() },
enabled = !uiState.isLoading
) {
Text("Health Check")
}
Button(
onClick = { pingViewModel.performSimplePing() },
enabled = !uiState.isLoading
) {
Text("Simple Ping")
}
Button(
onClick = { pingViewModel.performEnhancedPing(true) },
enabled = !uiState.isLoading
) {
Text("Test Circuit Breaker")
}
}
if (uiState.isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp))
}
// Results Display
uiState.healthResponse?.let { health ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(modifier = Modifier.padding(8.dp)) {
Text("✅ Health Check Result:")
Text("Status: ${health.status}")
Text("Service: ${health.service}")
Text("Healthy: ${health.healthy}")
Text("Timestamp: ${health.timestamp}")
}
}
}
uiState.simplePingResponse?.let { ping ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(modifier = Modifier.padding(8.dp)) {
Text("🏓 Simple Ping Result:")
Text("Status: ${ping.status}")
Text("Service: ${ping.service}")
Text("Timestamp: ${ping.timestamp}")
}
}
}
uiState.enhancedPingResponse?.let { ping ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
) {
Column(modifier = Modifier.padding(8.dp)) {
Text("⚡ Enhanced Ping Result:")
Text("Status: ${ping.status}")
Text("Circuit Breaker: ${ping.circuitBreakerState}")
Text("Response Time: ${ping.responseTime}ms")
Text("Service: ${ping.service}")
}
}
}
uiState.errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
"❌ Error: $error",
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
}
@Composable
fun StateDebugSection(appStore: AppStore) {
val appState by appStore.state.collectAsState()
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("🔍 App State Debug", style = MaterialTheme.typography.titleMedium)
Text("Auth State: ${if(appState.auth.isAuthenticated) "✅ Authenticated" else "❌ Not Authenticated"}")
Text("Current Route: ${appState.navigation.currentRoute}")
Text("Dark Mode: ${if(appState.ui.isDarkMode) "🌙 Enabled" else "☀️ Disabled"}")
Text("Online: ${if(appState.network.isOnline) "🟢 Online" else "🔴 Offline"}")
Button(
onClick = {
appStore.dispatch(at.mocode.clients.shared.presentation.actions.AppAction.UI.ToggleDarkMode)
}
) {
Text("Toggle Dark Mode")
}
}
}
}
@@ -0,0 +1,10 @@
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppCommonTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
@@ -1,2 +1,2 @@
actual fun isDevelopmentMode(): Boolean =
kotlinx.browser.window.location.hostname == "localhost"
kotlinx.browser.window.location.hostname == "localhost"
+28 -12
View File
@@ -1,21 +1,37 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.HTMLElement
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
window.onload = {
try {
val root = document.getElementById("ComposeTarget") as HTMLElement
ComposeViewport(root) {
MainApp()
}
} catch (e: Exception) {
console.error("Failed to start Compose Web app", e)
document.getElementById("root")?.innerHTML =
"<div style='padding: 50px; text-align: center;'>❌ Failed to load app: ${e.message}</div>"
}
console.log("[WebApp] main() entered")
fun startApp() {
try {
console.log("[WebApp] startApp(): readyState=", document.asDynamic().readyState)
val root = document.getElementById("ComposeTarget") as HTMLElement
console.log("[WebApp] ComposeTarget exists? ", (root != null))
ComposeViewport(root) {
MainApp()
}
// Remove the static loading placeholder if present
(document.querySelector(".loading") as? HTMLElement)?.let { it.parentElement?.removeChild(it) }
console.log("[WebApp] ComposeViewport mounted, loading placeholder removed")
} catch (e: Exception) {
console.error("Failed to start Compose Web app", e)
val fallbackTarget = (document.getElementById("ComposeTarget") ?: document.body) as HTMLElement
fallbackTarget.innerHTML =
"<div style='padding: 50px; text-align: center;'>❌ Failed to load app: ${e.message}</div>"
}
}
// Start immediately if DOM is already parsed, otherwise wait for DOMContentLoaded.
val state = document.asDynamic().readyState as String?
if (state == "interactive" || state == "complete") {
console.log("[WebApp] DOM already ready (", state, ") → starting immediately")
startApp()
} else {
console.log("[WebApp] Waiting for DOMContentLoaded, current state:", state)
document.addEventListener("DOMContentLoaded", { startApp() })
}
}
+20 -32
View File
@@ -1,39 +1,27 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Meldestelle - Web Development</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, sans-serif;
background: #fafafa;
}
#ComposeTarget {
width: 100vw;
height: 100vh;
}
#root {
width: 100vw;
height: 100vh;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 18px;
color: #666;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meldestelle - Web</title>
<link type="text/css" rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#0f172a">
</head>
<body>
<div id="root">
<canvas id="ComposeTarget"></canvas>
<div class="loading">🚀 Loading Meldestelle...</div>
</div>
<script src="web-app.js"></script>
<div id="ComposeTarget">
<div class="loading">Loading...</div>
</div>
<script src="web-app.js"></script>
<script>
// Register Service Worker only in non-localhost environments
if ('serviceWorker' in navigator && !['localhost', '127.0.0.1', '::1'].includes(location.hostname)) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').catch(function(err){
console.warn('ServiceWorker registration failed:', err);
});
});
}
</script>
</body>
</html>
+17 -7
View File
@@ -1,12 +1,22 @@
html, body {
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden; /* Verhindert Scrollbalken durch die Canvas */
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, sans-serif;
background: #fafafa;
overflow: hidden; /* Verhindert Scrollbalken durch die Canvas */
}
#ComposeTarget {
height: 100vh;
display: flex;
flex-direction: column;
height: 100vh;
display: flex;
flex-direction: column;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 18px;
color: #666;
}
+4 -2
View File
@@ -58,7 +58,8 @@ self.addEventListener('fetch', (event) => {
.then((resp) => {
if (resp && resp.status === 200 && resp.type === 'basic') {
const copy = resp.clone();
caches.open(CACHE_NAME).then((cache) => cache.put('/index.html', copy)).catch(() => {});
caches.open(CACHE_NAME).then((cache) => cache.put('/index.html', copy)).catch(() => {
});
}
return resp;
})
@@ -83,7 +84,8 @@ self.addEventListener('fetch', (event) => {
.then((resp) => {
if (resp && resp.status === 200 && resp.type === 'basic') {
const copy = resp.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {});
caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)).catch(() => {
});
}
return resp;
})
@@ -1,2 +1,2 @@
actual fun isDevelopmentMode(): Boolean =
System.getProperty("development.mode", "false").toBoolean()
System.getProperty("development.mode", "false").toBoolean()
+7 -7
View File
@@ -4,11 +4,11 @@ import androidx.compose.ui.window.WindowState
import androidx.compose.ui.unit.dp
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle - Desktop Development",
state = WindowState(width = 1200.dp, height = 800.dp)
) {
MainApp()
}
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle - Desktop Development",
state = WindowState(width = 1200.dp, height = 800.dp)
) {
MainApp()
}
}
+4 -5
View File
@@ -1,13 +1,12 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import at.mocode.clients.app.App
import kotlinx.browser.document
import org.w3c.dom.HTMLElement
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val root = document.getElementById("ComposeTarget") as HTMLElement
ComposeViewport(root) {
App()
}
val root = document.getElementById("ComposeTarget") as HTMLElement
ComposeViewport(root) {
MainApp()
}
}
+37 -40
View File
@@ -3,49 +3,46 @@
// Bundle-Analyse für Development (optional, only if package is available)
if (process.env.ANALYZE_BUNDLE === 'true') {
try {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
}));
console.log('Bundle analyzer enabled');
} catch (e) {
console.log('Bundle analyzer not available (webpack-bundle-analyzer not installed)');
}
try {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
}));
console.log('Bundle analyzer enabled');
} catch (e) {
console.log('Bundle analyzer not available (webpack-bundle-analyzer not installed)');
}
}
// Weitere Optimierungen hinzufügen (erweitert bestehende config)
config.optimization = {
...config.optimization, // Behalte Kotlin/JS Optimierungen
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
};
// Hinweis: Wir liefern eine statische index.html aus src/jsMain/resources aus.
// Diese Datei enthält nur einen Script-Tag zu "web-app.js" und wird NICHT
// vom HtmlWebpackPlugin generiert. Zusätzliche Chunks (z. B. vendor/runtime)
// würden dann nicht automatisch injiziert und führen dazu, dass die App nicht startet
// (Bildschirm bleibt auf "Loading...").
//
// Daher überschreiben wir config.optimization NICHT mehr mit splitChunks.
// Wenn später Chunking gewünscht ist, muss die index.html durch die generierte
// HTML ersetzt oder die zusätzlichen Chunks manuell eingebunden werden.
//
// (Frühere splitChunks-Konfiguration wurde bewusst entfernt.)
// Development Server Konfiguration erweitern
if (config.devServer) {
config.devServer = {
...config.devServer,
historyApiFallback: true,
hot: true,
// API Proxy für Backend-Anfragen (Array-Format für moderne Webpack)
proxy: [
{
context: ['/api'],
target: 'http://localhost:8081',
changeOrigin: true,
secure: false,
pathRewrite: { '^/api': '' }
}
]
}
config.devServer = {
...config.devServer,
historyApiFallback: true,
hot: true,
// API Proxy für Backend-Anfragen (Array-Format für moderne Webpack)
proxy: [
{
context: ['/api'],
target: 'http://localhost:8081',
changeOrigin: true,
secure: false,
pathRewrite: {'^/api': ''}
}
]
}
}