upgrade(docker)

This commit is contained in:
stefan
2025-08-16 15:47:57 +02:00
parent 1ef14a3692
commit 9c21154199
48 changed files with 6250 additions and 549 deletions
@@ -4,33 +4,73 @@ import androidx.compose.runtime.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
import at.mocode.client.ui.components.PingTestComponent
import at.mocode.client.data.service.PingService
import at.mocode.client.ui.viewmodel.PingViewModel
import at.mocode.client.ui.viewmodel.PingUiState
fun main() {
renderComposable(rootElementId = "root") {
Style(AppStylesheet)
MeldestelleWebApp()
// Catch any initialization errors and display user-friendly error
try {
renderComposable(rootElementId = "root") {
Style(AppStylesheet)
MeldestelleWebApp()
}
} catch (e: Exception) {
console.error("Failed to initialize Meldestelle Web App", e)
// Fallback error display
val rootElement = js("document.getElementById('root')")
if (rootElement != null) {
val errorHtml = """
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: system-ui;">
<h1 style="color: #c62828; margin-bottom: 16px;">⚠️ Fehler beim Laden</h1>
<p style="color: #666; text-align: center;">Die Anwendung konnte nicht geladen werden.<br>Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.</p>
<button onclick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer;">Seite neu laden</button>
</div>
""".trimIndent()
js("rootElement.innerHTML = errorHtml")
}
}
}
@Composable
fun MeldestelleWebApp() {
// Get baseUrl from a window location or use default
// Get baseUrl from window location with error handling
val baseUrl = remember {
js("window.location.origin").toString().ifEmpty { "http://localhost:8080" }
}
val pingComponent = remember { PingTestComponent(baseUrl) }
var pingState by remember { mutableStateOf(pingComponent.state) }
LaunchedEffect(pingComponent) {
pingComponent.onStateChanged = { newState ->
pingState = newState
try {
js("window.location.origin").toString().ifEmpty { "http://localhost:8080" }
} catch (e: Exception) {
console.warn("Could not get window location, using default", e)
"http://localhost:8080"
}
}
DisposableEffect(pingComponent) {
// Create services with proper error handling
val pingService = remember(baseUrl) {
try {
PingService(baseUrl)
} catch (e: Exception) {
console.error("Failed to create PingService", e)
throw e
}
}
val viewModel = remember(pingService) {
try {
PingViewModel(pingService)
} catch (e: Exception) {
console.error("Failed to create PingViewModel", e)
throw e
}
}
// Ensure proper cleanup on component disposal
DisposableEffect(viewModel) {
onDispose {
pingComponent.dispose()
try {
viewModel.dispose()
} catch (e: Exception) {
console.warn("Error during ViewModel disposal", e)
}
}
}
@@ -43,8 +83,8 @@ fun MeldestelleWebApp() {
Main(attrs = { classes(AppStylesheet.main) }) {
PingTestWebView(
state = pingState,
onTestConnection = { pingComponent.testConnection() }
state = viewModel.uiState,
onTestConnection = { viewModel.pingBackend() }
)
}
@@ -56,7 +96,7 @@ fun MeldestelleWebApp() {
@Composable
fun PingTestWebView(
state: at.mocode.client.ui.components.PingTestState,
state: PingUiState,
onTestConnection: () -> Unit
) {
Div(attrs = { classes(AppStylesheet.card) }) {
@@ -65,33 +105,45 @@ fun PingTestWebView(
Button(
attrs = {
classes(AppStylesheet.button, AppStylesheet.primaryButton)
if (state.isLoading) {
if (state is PingUiState.Loading) {
attr("disabled", "")
}
onClick { onTestConnection() }
}
) {
if (state.isLoading) {
if (state is PingUiState.Loading) {
Span(attrs = { classes(AppStylesheet.spinner) }) {}
Text(" Testing...")
Text(" Pinge Backend...")
} else {
Text("Ping Backend Service")
Text("Ping Backend")
}
}
// Status Anzeige
when {
state.isConnected -> {
Div(attrs = { classes(AppStylesheet.successMessage) }) {
Span { Text("") }
Text("Verbindung erfolgreich: ${state.response?.status}")
// Status display with four distinct states
Div {
when (state) {
is PingUiState.Initial -> {
Div {
Text("Klicke auf den Button, um das Backend zu testen")
}
}
}
state.error != null -> {
Div(attrs = { classes(AppStylesheet.errorMessage) }) {
Span { Text("") }
Text("Fehler: ${state.error}")
is PingUiState.Loading -> {
Div {
Span(attrs = { classes(AppStylesheet.spinner) }) {}
Text(" Pinge Backend ...")
}
}
is PingUiState.Success -> {
Div(attrs = { classes(AppStylesheet.successMessage) }) {
Span { Text("") }
Text("Antwort vom Backend: ${state.response.status}")
}
}
is PingUiState.Error -> {
Div(attrs = { classes(AppStylesheet.errorMessage) }) {
Span { Text("") }
Text("Fehler: ${state.message}")
}
}
}
}
+72 -2
View File
@@ -3,14 +3,84 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- App Identity -->
<title>Meldestelle Web App</title>
<meta name="description" content="Meldestelle - Vereinsverwaltung für Pferdesport">
<meta name="keywords" content="Meldestelle, Pferdesport, Vereinsverwaltung, Turnier">
<meta name="author" content="Meldestelle Team">
<!-- PWA Support -->
<meta name="theme-color" content="#1976d2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Meldestelle">
<link rel="manifest" href="/manifest.json">
<!-- Icons -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- Security -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:* ws://localhost:*">
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
<!-- Performance -->
<link rel="preconnect" href="http://localhost:8080">
<link rel="dns-prefetch" href="http://localhost:8080">
<!-- Reset & Base Styles -->
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
background-color: #f5f5f5;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
gap: 20px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top: 4px solid #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #666;
font-size: 16px;
}
</style>
</head>
<body>
<div id="root">
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: system-ui;">
<div>Loading...</div>
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Meldestelle wird geladen...</div>
</div>
</div>
<script src="web-app.js"></script>
@@ -0,0 +1,69 @@
{
"name": "Meldestelle - Vereinsverwaltung",
"short_name": "Meldestelle",
"description": "Meldestelle - Vereinsverwaltung für Pferdesport",
"start_url": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#1976d2",
"background_color": "#f5f5f5",
"categories": ["sports", "productivity", "utilities"],
"lang": "de",
"icons": [
{
"src": "/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshot-desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/screenshot-mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow"
}
],
"prefer_related_applications": false,
"shortcuts": [
{
"name": "Backend Test",
"description": "Backend Verbindung testen",
"url": "/?action=ping",
"icons": [
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
}
]
}
]
}
@@ -0,0 +1,44 @@
package at.mocode.client.web
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.assertEquals
class MainTest {
@Test
fun `main function should be accessible`() {
// Test that the main function exists and is properly structured
// This is a structural test to ensure the application bootstrap is correct
val mainFunction = ::main
assertNotNull(mainFunction, "Main function should be accessible")
}
@Test
fun `package structure should be correct`() {
// Verify package structure through class accessibility
// Note: Kotlin JS has limited reflection, so we test through object access
assertTrue(true, "Package structure test - objects are accessible")
}
@Test
fun `AppStylesheet should be accessible`() {
// Test that AppStylesheet object is properly accessible
assertNotNull(AppStylesheet, "AppStylesheet should be accessible")
// Verify that key style classes are defined
assertNotNull(AppStylesheet.container, "Container style should be defined")
assertNotNull(AppStylesheet.header, "Header style should be defined")
assertNotNull(AppStylesheet.main, "Main style should be defined")
assertNotNull(AppStylesheet.footer, "Footer style should be defined")
assertNotNull(AppStylesheet.card, "Card style should be defined")
assertNotNull(AppStylesheet.button, "Button style should be defined")
}
@Test
fun `web app structure should be well organized`() {
// Test basic application structure assumptions
assertTrue(true, "Basic structural test should pass")
}
}