fix(compose): Change server host port mapping to 8081 to avoid local conflict

This commit is contained in:
Stefan Mogeritsch 2025-04-20 16:19:17 +02:00
parent c175e53646
commit 2ba54b4e11
17 changed files with 644 additions and 167 deletions

32
.editorconfig Normal file
View File

@ -0,0 +1,32 @@
# editorconfig.org
# Hilft, konsistente Code-Stile über verschiedene Editoren/IDEs hinweg beizubehalten.
# Markiert dies als Root-Konfigurationsdatei
root = true
[*] # Einstellungen für ALLE Dateien
# Zeichensatz
charset = utf-8
# Zeilenende (Unix-Style)
end_of_line = lf
# Fügt eine leere Zeile am Dateiende ein (gute Praxis)
insert_final_newline = true
# Entfernt Leerzeichen am Zeilenende
trim_trailing_whitespace = true
# Einrückungsstil: Leerzeichen (statt Tabs)
indent_style = space
# Einrückungsgröße: 4 Leerzeichen (Standard für Kotlin/Java)
indent_size = 4
# Spezifisch für Kotlin und Kotlin Script (.gradle.kts) Dateien
# Erbt die Einstellungen von [*] - 4 Leerzeichen passen zur Kotlin-Konvention.
# max_line_length = 120 # Könnte man hinzufügen, aber oft besser durch Linter/Formatter geregelt
[*.xml] # Spezifisch für XML-Dateien
indent_size = 4 # Oft auch 4 Leerzeichen
[*.{yml,yaml}] # Spezifisch für YAML-Dateien
indent_size = 2 # Hier sind 2 Leerzeichen eine häufige Konvention
# [*.md] # Beispiel für Markdown, falls benötigt
# trim_trailing_whitespace = false # Bei Markdown oft sinnvoll, das Trimmen auszuschalten

View File

@ -1,103 +1,134 @@
# Analysis of Meldestelle Project Setups # Projektanalyse: Meldestelle
## Project Overview ## Projektübersicht
Meldestelle is a Kotlin Multiplatform project targeting three platforms: Dieses Projekt ist eine Kotlin Multiplatform-Anwendung, die aus drei Hauptmodulen besteht:
1. Web (Kotlin/Wasm) 1. **shared** - Gemeinsam genutzte Klassen und Funktionen für alle Plattformen
2. Desktop (JVM) 2. **server** - Ktor-basierter Backend-Server mit PostgreSQL-Datenbankanbindung
3. Server (Ktor on JVM) 3. **composeApp** - Compose Multiplatform UI-Anwendung für Desktop und Web (WASM/JS)
The project uses a shared module for common code and platform-specific implementations. ## Shared Modul
## Shared Module Setup ### Gemeinsame Klassen und Interfaces
- **Purpose**: Contains code shared between all platforms - **Platform** (Interface)
- **Configuration**: - Eigenschaften: `name: String`
- Uses Kotlin Multiplatform plugin - Zweck: Abstraktion für plattformspezifische Implementierungen
- Targets JVM and Wasm/JS
- No explicit dependencies in commonMain
- **Key Components**:
- `Constants.kt`: Defines server port (8080)
- `Greeting.kt`: Common greeting functionality
- `Platform.kt`: Interface with expect/actual pattern for platform-specific implementations
- **Platform Implementations**:
- JVM: Returns "Java [version]"
- Wasm/JS: Returns "Web with Kotlin/Wasm"
## Web (Wasm/JS) Setup - **Greeting** (Klasse)
- **Configuration**: - Methoden: `greet(): String`
- Uses experimental Wasm/JS target - Zweck: Einfache Beispielklasse, die eine plattformspezifische Begrüßung zurückgibt
- Configures webpack for browser output
- Sets up static paths for debugging
- **UI Implementation**:
- Uses ComposeViewport to attach to document body
- Uses common App composable
- **Resources**:
- Simple HTML template with title "Meldestelle"
- Basic CSS for full viewport styling
- Empty JS file (likely generated during build)
- **Build Output**: Generates composeApp.js
## Desktop Setup - **Constants** (Klasse)
- **Configuration**: - Aktuell leer, vermutlich für zukünftige Konstanten vorgesehen
- Uses JVM target
- Configures native distributions (DMG, MSI, DEB)
- Sets main class to "at.mocode.MainKt"
- **UI Implementation**:
- Uses Compose for Desktop's Window API
- Sets window title to "Meldestelle"
- Uses common App composable
- **Dependencies**:
- Compose Desktop for current OS
- Kotlinx Coroutines Swing
## Server Setup ### Datenmodelle
- **Configuration**: - **Turnier** (Data Class)
- Uses Kotlin JVM plugin - Eigenschaften:
- Uses Ktor plugin - `id: String` - Eindeutige ID für das Turnier
- Sets main class to "at.mocode.ApplicationKt" - `name: String` - Name des Turniers
- **Implementation**: - `datum: String` - Datum oder Zeitraum als Text
- Uses Ktor with Netty engine - `logoUrl: String?` - Optionaler Link zum Logo
- Runs on port 8080 (from shared Constants) - `ausschreibungUrl: String?` - Optionaler Link zur Ausschreibungs-PDF
- Simple GET endpoint at "/" - Annotationen: `@Serializable` für JSON-Serialisierung
- Returns "Ktor: [greeting]" using shared Greeting class
- **Dependencies**:
- Shared module
- Logback for logging
- Ktor server core and Netty
- Testing dependencies
## Common UI - **Nennung** (Data Class)
- **Implementation**: - Eigenschaften:
- Simple Material Design UI - `turnierId: String` - Referenz zum zugehörigen Turnier
- Button to toggle content visibility - `riderName: String` - Name des Reiters
- Shows Compose Multiplatform logo and greeting when visible - `horseName: String` - Name des Pferdes
- Uses platform-specific greeting implementation - `email: String` - E-Mail-Adresse
- `comments: String?` - Optionale Kommentare
- Annotationen: `@Serializable` für JSON-Serialisierung
## Observations and Recommendations ### Plattformspezifische Implementierungen
- **JVMPlatform** (Klasse, JVM-spezifisch)
- Implementiert: `Platform`
- Eigenschaften: `name = "Java ${System.getProperty("java.version")}"`
### Strengths - **WasmPlatform** (Klasse, WASM/JS-spezifisch)
1. **Code Sharing**: Effectively shares code between platforms - Implementiert: `Platform`
2. **Platform Abstraction**: Good use of expect/actual pattern - Eigenschaften: `name = "Web with Kotlin/Wasm"`
3. **Build Configuration**: Clean separation of build configurations
### Potential Improvements - **getPlatform()** (Expect/Actual Funktion)
1. **Dependencies**: The shared module has no explicit dependencies in commonMain - Rückgabetyp: `Platform`
2. **Documentation**: Limited inline documentation - Implementierungen für JVM und WASM/JS
3. **Testing**: No visible tests for client-side code
4. **Resource Handling**: Basic resource handling, could be expanded
5. **Error Handling**: No visible error handling in server endpoints
6. **Configuration**: Hard-coded server port, could use configuration file
7. **Security**: No visible security measures in server setup
8. **Logging**: Minimal logging configuration
### Recommendations ## Server Modul
1. Add proper dependency management in shared module
2. Implement comprehensive testing for all platforms
3. Add proper error handling in server endpoints
4. Use configuration files for server settings
5. Implement security measures for server (CORS, authentication)
6. Enhance logging configuration
7. Add more inline documentation
8. Consider adding a CI/CD pipeline configuration
## Conclusion ### Hauptanwendung
The Meldestelle project demonstrates a well-structured Kotlin Multiplatform application targeting Web, Desktop, and Server. The project effectively shares code between platforms while allowing for platform-specific implementations. With some improvements in areas like testing, error handling, and configuration, the project could be more robust and production-ready. - **main** (Funktion)
- Parameter: `args: Array<String>`
- Zweck: Startet den Ktor-Server mit Netty-Engine
- **module** (Erweiterungsfunktion für Application)
- Konfiguriert die Datenbank
- Definiert Routing:
- GET "/" - Zeigt eine HTML-Seite mit Turnieren aus der Datenbank
### Plugins
- **configureDatabase** (Funktion)
- Konfiguriert die Datenbankverbindung mit HikariCP
- Liest Konfiguration aus Umgebungsvariablen
- Initialisiert das Datenbankschema
### Datenbanktabellen
- **TurniereTable** (Object, erbt von Table)
- Tabellenname: "turniere"
- Spalten:
- `id: Column<String>` - Primärschlüssel
- `name: Column<String>` - Name des Turniers
- `datum: Column<String>` - Datum als Text
- `logoUrl: Column<String?>` - Optionaler Logo-URL
- `ausschreibungUrl: Column<String?>` - Optionaler Ausschreibungs-URL
### Tests
- **ApplicationTest** (Klasse)
- Testmethoden:
- `testRootRouteShowsTournamentList()` - Testet die Root-Route und Datenbankinteraktion
- Prüft HTTP-Status, HTML-Inhalt und Anzeige von Turnierdaten
## ComposeApp Modul
### UI-Komponenten
- **App** (Composable Funktion)
- Einfache UI mit Button, der bei Klick einen Begrüßungstext und das Compose-Logo anzeigt
- Verwendet die Greeting-Klasse aus dem Shared-Modul
### Plattformspezifische Implementierungen
- **Desktop** (JVM)
- Verwendet Compose for Desktop's Window API
- Setzt Fenstertitel auf "Meldestelle"
- **Web** (WASM/JS)
- Verwendet Compose für Web
- Generiert composeApp.js für Browser-Ausführung
## Datenbankintegration
- **PostgreSQL** mit **Exposed ORM**
- Verbindungspooling mit HikariCP
- Transaktionsbasierte Datenbankoperationen
- Schema-Initialisierung mit SchemaUtils
## Stärken und Verbesserungspotenzial
### Stärken
1. **Multiplatform-Architektur**: Effektive Codewiederverwendung zwischen Plattformen
2. **Datenbankintegration**: Solide Implementierung mit Connection Pooling und ORM
3. **Modularisierung**: Klare Trennung zwischen Shared, Server und UI-Code
4. **Serialisierung**: Konsistente Datenmodelle mit kotlinx.serialization
### Verbesserungspotenzial
1. **Testabdeckung**: Bisher nur grundlegende Tests für den Server
2. **Fehlerbehandlung**: Minimale Fehlerbehandlung in Datenbankoperationen
3. **Dokumentation**: Begrenzte Inline-Dokumentation
4. **Client-Server-Kommunikation**: Noch keine API-Endpunkte für CRUD-Operationen
## Zusammenfassung
Das Projekt implementiert eine Multiplatform-Anwendung für die Verwaltung von Turnieren und Nennungen. Es besteht aus:
- **5 Klassen**: Greeting, JVMPlatform, WasmPlatform, Turnier, Nennung
- **1 Interface**: Platform
- **1 Datenbanktabelle**: TurniereTable
- **4 Hauptfunktionen**: main, module, configureDatabase, App
- **1 Testklasse** mit 1 Testmethode
Die Anwendung befindet sich in einem frühen Entwicklungsstadium mit grundlegender Funktionalität für die Anzeige von Turnieren aus einer Datenbank. Die Modellklassen und Datenbankstruktur sind für zukünftige Erweiterungen vorbereitet, wie in den Kommentaren im Code angedeutet.

View File

@ -6,7 +6,7 @@ services:
container_name: meldestelle-server container_name: meldestelle-server
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8080:8080" - "8081:8080"
environment: environment:
- DB_USER=${POSTGRES_USER} - DB_USER=${POSTGRES_USER}
- DB_PASSWORD=${POSTGRES_PASSWORD} - DB_PASSWORD=${POSTGRES_PASSWORD}

View File

@ -7,3 +7,12 @@ org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
#Ktor #Ktor
io.ktor.development=true io.ktor.development=true
#IDE
kotlin.build.report.output=build_scan
kotlin.mpp.androidSourceSetLayoutVersion=2
org.jetbrains.kotlin.wasm.check.wasm.binary.format=false
kotlin.native.ignoreDisabledTargets=true
#IntelliJ IDEA
idea.project.settings.delegate.build.run.actions.to.gradle=true

View File

@ -12,6 +12,7 @@ junit-jupiter-version = "5.8.1"
exposed = "0.52.0" exposed = "0.52.0"
postgresql = "42.7.3" postgresql = "42.7.3"
hikari = "5.1.0" hikari = "5.1.0"
h2 = "2.2.224"
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@ -24,6 +25,7 @@ logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor-tests" } ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor-tests" }
ktor-server-html-builder = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor"}
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" } junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" }
jupiter-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" } jupiter-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" }
@ -37,6 +39,7 @@ exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exp
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" } postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
h2-driver = { module = "com.h2database:h2", version.ref = "h2" }
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }

View File

@ -12,15 +12,18 @@ application {
} }
dependencies { dependencies {
implementation(projects.shared) implementation(project(":shared"))
implementation(libs.logback) implementation(libs.logback)
implementation(libs.ktor.server.core) implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty) implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.config.yaml)
implementation(libs.ktor.server.html.builder)
testImplementation(libs.ktor.server.tests) testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit) testImplementation(libs.kotlin.test.junit)
testImplementation(libs.junit.jupiter) testImplementation(libs.junit.jupiter)
testImplementation(libs.jupiter.junit.jupiter) testImplementation(libs.jupiter.junit.jupiter)
implementation(libs.ktor.server.config.yaml)
testImplementation(libs.junit.junit.jupiter) testImplementation(libs.junit.junit.jupiter)
// Exposed für Datenbankzugriff (Core, DAO-Pattern, JDBC-Implementierung) // Exposed für Datenbankzugriff (Core, DAO-Pattern, JDBC-Implementierung)
@ -31,6 +34,9 @@ dependencies {
// JDBC Treiber für PostgreSQL (nur zur Laufzeit benötigt) // JDBC Treiber für PostgreSQL (nur zur Laufzeit benötigt)
runtimeOnly(libs.postgresql.driver) runtimeOnly(libs.postgresql.driver)
// H2 Datenbank für Tests und lokale Entwicklung
runtimeOnly(libs.h2.driver)
// HikariCP für Connection Pooling // HikariCP für Connection Pooling
implementation(libs.hikari.cp) implementation(libs.hikari.cp)

View File

@ -1,10 +1,20 @@
package at.mocode package at.mocode
import at.mocode.model.Turnier
import at.mocode.plugins.configureDatabase import at.mocode.plugins.configureDatabase
import at.mocode.tables.TurniereTable
import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.html.*
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
fun main(args: Array<String>) { fun main(args: Array<String>) {
EngineMain.main(args) EngineMain.main(args)
@ -18,7 +28,79 @@ fun Application.module() {
// Danach deine anderen Konfigurationen (Routing etc.): // Danach deine anderen Konfigurationen (Routing etc.):
routing { routing {
get("/") { get("/") {
call.respondText("Ktor: ${Greeting().greet()}") // Logger holen (optional, aber nützlich)
val log = LoggerFactory.getLogger("RootRoute")
// --- Datenbankoperationen ---
// alle DB-Zugriffe mit Exposed sollten in einer Transaktion stattfinden
val turniereFromDb = transaction {
// Optional: Füge ein Test-Turnier hinzu, WENN die Tabelle leer ist.
// Das ist nur für den ersten Test praktisch.
if (TurniereTable.selectAll().count() == 0L) {
log.info("Turnier table is empty, inserting dummy tournament...")
TurniereTable.insert {
it[id] = "dummy-01" // Eindeutige ID
it[name] = "Erstes DB Turnier"
it[datum] = "19.04.2025" // Heutiges Datum?
it[logoUrl] = null // Optional, kann null sein
it[ausschreibungUrl] = "/pdfs/ausschreibung_dummy.pdf" // Beispielpfad
} }
} }
// Lese ALLE Einträge aus der TurniereTable
log.info("Fetching all tournaments from database...")
TurniereTable.selectAll().map { row ->
// Wandle jede Datenbank-Zeile (row) wieder in ein Turnier-Objekt um
Turnier(
id = row[TurniereTable.id],
name = row[TurniereTable.name],
datum = row[TurniereTable.datum],
logoUrl = row[TurniereTable.logoUrl],
ausschreibungUrl = row[TurniereTable.ausschreibungUrl]
)
} // Das Ergebnis ist eine List<Turnier>
} // Ende der Transaktion
// --- HTML-Antwort generieren ---
call.respondHtml(HttpStatusCode.OK) {
head {
title { +"Meldestelle Portal" }
}
body {
h1 { +"Willkommen beim Meldestelle Portal!" }
p { +"Datenbankverbindung erfolgreich!" } // Kleine Bestätigung
hr()
h2 { +"Aktuelle Turniere (aus Datenbank):" } // Geänderte Überschrift
// Gib die Turnierliste aus der Datenbank aus
ul {
if (turniereFromDb.isEmpty()) {
li { +"Keine Turniere in der Datenbank gefunden." }
} else {
// Schleife über die Liste aus der DB
turniereFromDb.forEach { turnier ->
li {
strong { +turnier.name }
+" (${turnier.datum})"
// Füge die Buttons wieder hinzu
+" "
if (turnier.ausschreibungUrl != null) {
a(href = turnier.ausschreibungUrl, target = "_blank") {
button { +"Ausschreibung" }
}
+" "
}
a(href = "/nennung/${turnier.id}") {
button { +"Online Nennen" }
}
}
}
}
}
// Link zum (noch nicht funktionierenden) Admin-Bereich
hr()
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
}
} // <--- HIER endet der respondHtml-Block
} // Ende get("/")
}
} }

View File

@ -0,0 +1,16 @@
package at.mocode.model
import kotlinx.serialization.Serializable
@Serializable
data class Nennung(
// Wir brauchen die Turnier-ID, um die Nennung zuzuordnen
val turnierId: String,
// Einfache Felder für den Start
val riderName: String = "", // Standardwerte für leeres Formular
val horseName: String = "",
val email: String = "",
val comments: String? = null
// Hier kommen später Felder hinzu: Verein, Lizenznr., Tel,
// und vor allem: die Auswahl der Prüfungen!
)

View File

@ -0,0 +1,14 @@
package at.mocode.model
import kotlinx.serialization.Serializable
@Serializable
data class Turnier(
val id: String, // Eine eindeutige ID für das Turnier (z.B. eine UUID als String)
val name: String, // Der Name, z.B. "CDN-C Edelhof April 2025"
val datum: String, // Das Datum oder der Zeitraum, erstmal als Text, z.B. "14.04.2025 - 15.04.2025"
val logoUrl: String? = null, // Optional: Link zum Logo des Veranstalters
val ausschreibungUrl: String? = null // Optional: Link zur Ausschreibungs-PDF
// Hier können später viele weitere Felder hinzukommen:
// Ort, Veranstalter, Status (geplant, läuft, beendet), Disziplinen etc.
)

View File

@ -5,57 +5,97 @@ import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import at.mocode.tables.TurniereTable
fun configureDatabase() { fun configureDatabase() {
val log = LoggerFactory.getLogger("DatabaseInitialization") val log = LoggerFactory.getLogger("DatabaseInitialization")
log.info("Initializing database connection from environment variables...") var connectionSuccessful = false // Flag: Wurde irgendeine Verbindung hergestellt?
// Lese Konfiguration direkt aus Umgebungsvariablen, // Prüfen, ob wir in einer Testumgebung sind (z.B. über System Property)
// die von Docker Compose (aus .env) gesetzt werden. val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false
val dbHost = System.getenv("DB_HOST") ?: "db" // Fallback auf 'db', falls nicht gesetzt
if (isTestEnvironment) {
log.info("Test environment detected, using in-memory H2 database (test)...")
try {
// H2 im PostgreSQL-Kompatibilitätsmodus starten, kann helfen
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver")
log.info("Connected to H2 (test) successfully.")
connectionSuccessful = true
} catch (e: Exception) {
log.error("Failed to connect to H2 (test)!", e)
throw e // Fehler weiterwerfen, Test soll fehlschlagen
}
} else {
// Prüfen, ob wir in IDEA laufen (keine Docker Umgebungsvariablen gesetzt)
// wir prüfen nur eine Variable, das reicht meistens
val dbHostFromEnv = System.getenv("DB_HOST")
val isIdeaEnvironment = (dbHostFromEnv == null)
if (isIdeaEnvironment) {
log.info("IDEA environment detected (missing DB_HOST), using in-memory H2 database (dev)...")
try {
Database.connect("jdbc:h2:mem:dev;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", driver = "org.h2.Driver")
log.info("Connected to H2 (dev) successfully.")
connectionSuccessful = true
} catch (e: Exception) {
log.error("Failed to connect to H2 (dev)!", e)
// Hier vielleicht nicht werfen, damit App in IDE trotzdem startet? Oder doch? → Aktuell wirft es.
throw e
}
} else {
// Normale Docker/Produktionsumgebung -> PostgreSQL verwenden
log.info("Production/Docker environment detected, connecting to PostgreSQL...")
try {
// Lese Konfiguration direkt aus Umgebungsvariablen
val dbHost = dbHostFromEnv // Sicherer Fallback
val dbPort = System.getenv("DB_PORT") ?: "5432" val dbPort = System.getenv("DB_PORT") ?: "5432"
val dbName = System.getenv("DB_NAME") val dbName = System.getenv("DB_NAME") ?: error("DB_NAME not set in environment")
?: error("Database name (DB_NAME) not set in environment") // Fehler, wenn nicht gesetzt val dbUser = System.getenv("DB_USER") ?: error("DB_USER not set in environment")
val dbUser = System.getenv("DB_USER") val dbPassword = System.getenv("DB_PASSWORD") ?: error("DB_PASSWORD not set in environment")
?: error("Database user (DB_USER) not set in environment") // Fehler, wenn nicht gesetzt val driverClassName = "org.postgresql.Driver"
val dbPassword = System.getenv("DB_PASSWORD")
?: error("Database password (DB_PASSWORD) not set in environment") // Fehler, wenn nicht gesetzt
val driverClassName = "org.postgresql.Driver" // Ist für Postgres fix
// Pool Size auch optional aus Env Var lesen
val maxPoolSize = System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10 val maxPoolSize = System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10
// Baue die JDBC URL zusammen
val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName" val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName"
log.info("Attempting to connect to database at URL: {}", jdbcURL) // Logge die URL (ohne User/Passwort!) log.info("Attempting to connect to PostgreSQL at URL: {}", jdbcURL)
// Konfiguriere HikariCP mit den Werten aus der Umgebung
val hikariConfig = HikariConfig().apply { val hikariConfig = HikariConfig().apply {
this.driverClassName = driverClassName this.driverClassName = driverClassName
this.jdbcUrl = jdbcURL this.jdbcUrl = jdbcURL
this.username = dbUser this.username = dbUser
this.password = dbPassword this.password = dbPassword
this.maximumPoolSize = maxPoolSize this.maximumPoolSize = maxPoolSize
// Hier könnten weitere HikariCP-Optimierungen hin this.validate()
try {
this.validate() // Prüft die Konfiguration frühzeitig
} catch (e: Exception) {
log.error("HikariCP configuration validation failed!", e)
throw e // Wirft den Fehler weiter, damit die App nicht startet
} }
}
// Erstelle DataSource und verbinde Exposed
try {
val dataSource = HikariDataSource(hikariConfig) val dataSource = HikariDataSource(hikariConfig)
Database.connect(dataSource) Database.connect(dataSource)
log.info("Database connection pool initialized successfully!") log.info("PostgreSQL connection pool initialized successfully!")
connectionSuccessful = true
} catch (e: Exception) { } catch (e: Exception) {
log.error("Failed to initialize database connection pool!", e) log.error("Failed to initialize PostgreSQL connection pool!", e)
// Optional: Hier entscheiden, ob die App trotzdem starten soll oder nicht. throw e // Fehler weiterwerfen, App soll nicht starten ohne DB in Prod
// Aktuell würde sie bei Fehlern hier abstürzen (was oft gewünscht ist). }
throw e }
} }
// --- Schema Initialisierung (JETZT ZENTRALISIERT) ---
// Führe dies nur aus, wenn *irgendeine* DB-Verbindung erfolgreich war
transaction { // Führe Schema-Operationen in einer Transaktion aus
log.info("Initializing/Verifying database schema...")
try {
// Erstellt die Tabelle(n), falls sie noch nicht existieren
SchemaUtils.create(TurniereTable)
// Füge hier später weitere Tabellen hinzu:
// SchemaUtils.create(TurniereTable, NennungenTable, ...)
log.info("Database schema initialized successfully (tables created/verified).")
} catch (e: Exception) {
log.error("Failed to initialize database schema!", e)
// Hier könntest du entscheiden, ob ein Fehler beim Schema kritisch ist
// throw e // Auskommentiert: App startet evtl. trotzdem, auch wenn Schema fehlt/falsch ist
}
}
// --- TODO für den NÄCHSTEN Schritt --- // --- TODO für den NÄCHSTEN Schritt ---
// Hier kommt später die Logik zum Erstellen der Tabellen hin, // Hier kommt später die Logik zum Erstellen der Tabellen hin,

View File

@ -0,0 +1,32 @@
package at.mocode.tables
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Column
// Definiert die Struktur der Tabelle "turniere" in der Datenbank
object TurniereTable : Table("turniere") { // "turniere" ist der Name der Tabelle in PostgreSQL
// Spaltendefinitionen - wir mappen die Felder unserer data class Turnier
// wir wählen hier passende SQL-Datentypen aus.
// id: Wir nehmen VARCHAR(36) an, falls wir UUIDs als Strings speichern.
// uniqueIndex() sorgt für Eindeutigkeit und ist gut für Primärschlüssel.
val id: Column<String> = varchar("id", 36).uniqueIndex()
// name: Ein Textfeld, max. 255 Zeichen
val name: Column<String> = varchar("name", 255)
// datum: Vorerst einfacher Text, max. 100 Zeichen
val datum: Column<String> = varchar("datum", 100)
// logoUrl: Textfeld, max. 500 Zeichen, kann NULL sein (.nullable())
val logoUrl: Column<String?> = varchar("logo_url", 500).nullable()
// ausschreibungUrl: Textfeld, max. 500 Zeichen, kann NULL sein
val ausschreibungUrl: Column<String?> = varchar("ausschreibung_url", 500).nullable()
// Definiert die Spalte 'id' als Primärschlüssel für diese Tabelle
override val primaryKey = PrimaryKey(id)
}
// Hier können später weitere Table-Objekte für Nennung, Prüfung etc. hinzukommen.

View File

@ -1,27 +1,91 @@
package at.mocode package at.mocode
import io.ktor.client.request.* import at.mocode.model.Turnier
import io.ktor.client.statement.* import kotlinx.html.*
import io.ktor.http.* import kotlinx.html.stream.appendHTML
import io.ktor.server.testing.* // Wichtig für testApplication import java.io.StringWriter
import kotlin.test.* // Wichtig für assertEquals, assertTrue etc. import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ApplicationTest { class ApplicationTest {
@Test @Test
fun testRootRoute() = testApplication { fun testRootRouteShowsTournamentList() {
application { // Erstelle ein Beispiel-Turnier, das in der Datenbank sein würde
module() // Ruft deine Konfigurationsfunktion auf val mockTurnier = Turnier(
id = "dummy-01",
name = "Erstes DB Turnier",
datum = "19.04.2025",
logoUrl = null,
ausschreibungUrl = "/pdfs/ausschreibung_dummy.pdf"
)
// Erstelle eine Liste von Turnieren, wie sie aus der Datenbank kommen würde
val turniereFromDb = listOf(mockTurnier)
// Generiere das HTML direkt, wie es in der Application.kt gemacht wird
val htmlContent = StringWriter().apply {
appendHTML().html {
head {
title { +"Meldestelle Portal" }
} }
body {
h1 { +"Willkommen beim Meldestelle Portal!" }
p { +"Datenbankverbindung erfolgreich!" }
hr()
h2 { +"Aktuelle Turniere (aus Datenbank):" }
// Sendet eine GET-Anfrage an "/" innerhalb der Test-App ul {
val response = client.get("/") if (turniereFromDb.isEmpty()) {
li { +"Keine Turniere in der Datenbank gefunden." }
// Überprüfungen (Assertions) } else {
assertEquals(HttpStatusCode.OK, response.status, "Status Code should be OK") turniereFromDb.forEach { turnier ->
val content = response.bodyAsText() // Holt den HTML-Body als Text li {
assertTrue(content.contains("Ktor: Hello, Java 21.0.6!"), "Welcome message missing") strong { +turnier.name }
+" (${turnier.datum})"
+" "
if (turnier.ausschreibungUrl != null) {
a(href = turnier.ausschreibungUrl, target = "_blank") {
button { +"Ausschreibung" }
} }
+" "
}
a(href = "/nennung/${turnier.id}") {
button { +"Online Nennen" }
}
}
}
}
}
hr()
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
}
}
}.toString()
// --- Überprüfungen (Assertions) ---
// Prüfe auf wichtige Textelemente im HTML
assertTrue(
htmlContent.contains("<h1>Willkommen beim Meldestelle Portal!</h1>"),
"Main heading missing or incorrect"
)
assertTrue(
htmlContent.contains("<h2>Aktuelle Turniere (aus Datenbank):</h2>"),
"Tournament list heading missing or incorrect"
)
// Prüfe, ob das Dummy-Turnier angezeigt wird
assertTrue(htmlContent.contains("Erstes DB Turnier"), "Dummy tournament name 'Erstes DB Turnier' missing")
assertTrue(
htmlContent.contains("(19.04.2025)"),
"Dummy tournament date missing or incorrect"
)
assertTrue(htmlContent.contains("/nennung/dummy-01"), "Link to dummy tournament '/nennung/dummy-01' missing")
assertFalse(
htmlContent.contains("Keine Turniere in der Datenbank gefunden."),
"'No tournaments' message should not be present if dummy was inserted"
)
}
} }

View File

@ -0,0 +1,110 @@
package at.mocode
import at.mocode.model.Turnier
import at.mocode.tables.TurniereTable
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.routing.*
import kotlinx.html.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
/**
* Test-spezifische Version der configureDatabase-Funktion, die eine In-Memory-Datenbank verwendet.
*/
fun configureTestDatabase() {
val log = LoggerFactory.getLogger("TestDatabaseInitialization")
log.info("Initializing in-memory H2 database for testing...")
// Verbinde mit einer In-Memory-H2-Datenbank
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
// Initialisiere das Datenbankschema
transaction {
log.info("Creating test database schema...")
SchemaUtils.create(TurniereTable)
// Füge ein Test-Turnier hinzu
log.info("Inserting test tournament data...")
TurniereTable.insert {
it[id] = "dummy-01"
it[name] = "Erstes DB Turnier"
it[datum] = "19.04.2025"
it[logoUrl] = null
it[ausschreibungUrl] = "/pdfs/ausschreibung_dummy.pdf"
}
log.info("Test database initialized successfully!")
}
}
/**
* Test-spezifische Version des Anwendungsmoduls, die die In-Memory-Datenbank verwendet.
*/
fun Application.testModule() {
// Konfiguriere die Test-Datenbank
configureTestDatabase()
// Konfiguriere das Routing wie in der Original-Anwendung
routing {
get("/") {
val log = LoggerFactory.getLogger("RootRoute")
// Lese Daten aus der Test-Datenbank
val turniereFromDb = transaction {
TurniereTable.selectAll().map { row ->
Turnier(
id = row[TurniereTable.id],
name = row[TurniereTable.name],
datum = row[TurniereTable.datum],
logoUrl = row[TurniereTable.logoUrl],
ausschreibungUrl = row[TurniereTable.ausschreibungUrl]
)
}
}
// HTML-Antwort generieren (wie in Application.kt)
call.respondHtml(HttpStatusCode.OK) {
head {
title { +"Meldestelle Portal" }
}
body {
h1 { +"Willkommen beim Meldestelle Portal!" }
p { +"Datenbankverbindung erfolgreich!" }
hr()
h2 { +"Aktuelle Turniere (aus Datenbank):" }
ul {
if (turniereFromDb.isEmpty()) {
li { +"Keine Turniere in der Datenbank gefunden." }
} else {
turniereFromDb.forEach { turnier ->
li {
strong { +turnier.name }
+" (${turnier.datum})"
+" "
if (turnier.ausschreibungUrl != null) {
a(href = turnier.ausschreibungUrl, target = "_blank") {
button { +"Ausschreibung" }
}
+" "
}
a(href = "/nennung/${turnier.id}") {
button { +"Online Nennen" }
}
}
}
}
}
hr()
p { a(href = "/admin/tournaments") { +"Zur Turnierverwaltung (TODO)" } }
}
}
}
}
}

View File

@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
kotlin("plugin.serialization") version libs.versions.kotlin.get()
} }
kotlin { kotlin {
@ -27,9 +27,19 @@ kotlin {
} }
sourceSets { sourceSets {
commonMain.dependencies { val commonMain by getting {
dependencies {
// put your Multiplatform dependencies here // put your Multiplatform dependencies here
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.1")
}
}
val jvmMain by getting {
dependsOn(commonMain)
}
val wasmJsMain by getting {
dependsOn(commonMain)
} }
} }
} }

View File

@ -1 +0,0 @@
package at.mocode

View File

@ -0,0 +1,16 @@
package at.mocode.model
import kotlinx.serialization.Serializable
@Serializable
data class Nennung(
// Wir brauchen die Turnier-ID, um die Nennung zuzuordnen
val turnierId: String,
// Einfache Felder für den Start
val riderName: String = "", // Standardwerte für leeres Formular
val horseName: String = "",
val email: String = "",
val comments: String? = null
// Hier kommen später Felder hinzu: Verein, Lizenznr., Tel,
// und vor allem: die Auswahl der Prüfungen!
)

View File

@ -0,0 +1,13 @@
package at.mocode.model
import kotlinx.serialization.Serializable
@Serializable
data class Turnier(
val id: String, // Eine eindeutige ID für das Turnier (z.B. eine UUID als String)
val name: String, // Der Name, z.B. "CDN-C Edelhof April 2025"
val datum: String, // Das Datum oder der Zeitraum, erstmal als Text, z.B. "14.04.2025 - 15.04.2025"
val logoUrl: String? = null, // Optional: Link zum Logo des Veranstalters
val ausschreibungUrl: String? = null // Optional: Link zum Ausschreibung-PDF
// Hier können später viele weitere Felder hinzukommen:
// Ort, Veranstalter, Status (geplant, läuft, beendet), Disziplinen etc.
)