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
Meldestelle is a Kotlin Multiplatform project targeting three platforms:
1. Web (Kotlin/Wasm)
2. Desktop (JVM)
3. Server (Ktor on JVM)
## Projektübersicht
Dieses Projekt ist eine Kotlin Multiplatform-Anwendung, die aus drei Hauptmodulen besteht:
1. **shared** - Gemeinsam genutzte Klassen und Funktionen für alle Plattformen
2. **server** - Ktor-basierter Backend-Server mit PostgreSQL-Datenbankanbindung
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
- **Purpose**: Contains code shared between all platforms
- **Configuration**:
- Uses Kotlin Multiplatform plugin
- 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"
### Gemeinsame Klassen und Interfaces
- **Platform** (Interface)
- Eigenschaften: `name: String`
- Zweck: Abstraktion für plattformspezifische Implementierungen
## Web (Wasm/JS) Setup
- **Configuration**:
- Uses experimental Wasm/JS target
- 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
- **Greeting** (Klasse)
- Methoden: `greet(): String`
- Zweck: Einfache Beispielklasse, die eine plattformspezifische Begrüßung zurückgibt
## Desktop Setup
- **Configuration**:
- 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
- **Constants** (Klasse)
- Aktuell leer, vermutlich für zukünftige Konstanten vorgesehen
## Server Setup
- **Configuration**:
- Uses Kotlin JVM plugin
- Uses Ktor plugin
- Sets main class to "at.mocode.ApplicationKt"
- **Implementation**:
- Uses Ktor with Netty engine
- Runs on port 8080 (from shared Constants)
- Simple GET endpoint at "/"
- Returns "Ktor: [greeting]" using shared Greeting class
- **Dependencies**:
- Shared module
- Logback for logging
- Ktor server core and Netty
- Testing dependencies
### Datenmodelle
- **Turnier** (Data Class)
- Eigenschaften:
- `id: String` - Eindeutige ID für das Turnier
- `name: String` - Name des Turniers
- `datum: String` - Datum oder Zeitraum als Text
- `logoUrl: String?` - Optionaler Link zum Logo
- `ausschreibungUrl: String?` - Optionaler Link zur Ausschreibungs-PDF
- Annotationen: `@Serializable` für JSON-Serialisierung
## Common UI
- **Implementation**:
- Simple Material Design UI
- Button to toggle content visibility
- Shows Compose Multiplatform logo and greeting when visible
- Uses platform-specific greeting implementation
- **Nennung** (Data Class)
- Eigenschaften:
- `turnierId: String` - Referenz zum zugehörigen Turnier
- `riderName: String` - Name des Reiters
- `horseName: String` - Name des Pferdes
- `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
1. **Code Sharing**: Effectively shares code between platforms
2. **Platform Abstraction**: Good use of expect/actual pattern
3. **Build Configuration**: Clean separation of build configurations
- **WasmPlatform** (Klasse, WASM/JS-spezifisch)
- Implementiert: `Platform`
- Eigenschaften: `name = "Web with Kotlin/Wasm"`
### Potential Improvements
1. **Dependencies**: The shared module has no explicit dependencies in commonMain
2. **Documentation**: Limited inline documentation
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
- **getPlatform()** (Expect/Actual Funktion)
- Rückgabetyp: `Platform`
- Implementierungen für JVM und WASM/JS
### Recommendations
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
## Server Modul
## Conclusion
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.
### Hauptanwendung
- **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
restart: unless-stopped
ports:
- "8080:8080"
- "8081:8080"
environment:
- DB_USER=${POSTGRES_USER}
- DB_PASSWORD=${POSTGRES_PASSWORD}
@ -67,4 +67,4 @@ networks:
driver: bridge
volumes:
postgres_data: # <--- Konsistenter Name
# pgadmin_data: # <--- Konsistenter Name
# pgadmin_data: # <--- Konsistenter Name

View File

@ -6,4 +6,13 @@ kotlin.daemon.jvmargs=-Xmx2048M
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
#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"
postgresql = "42.7.3"
hikari = "5.1.0"
h2 = "2.2.224"
[libraries]
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-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-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" }
jupiter-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" }
@ -37,10 +39,11 @@ exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exp
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
hikari-cp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" }
h2-driver = { module = "com.h2database:h2", version.ref = "h2" }
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

View File

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

View File

@ -1,10 +1,20 @@
package at.mocode
import at.mocode.model.Turnier
import at.mocode.plugins.configureDatabase
import at.mocode.tables.TurniereTable
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
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>) {
EngineMain.main(args)
@ -18,7 +28,79 @@ fun Application.module() {
// Danach deine anderen Konfigurationen (Routing etc.):
routing {
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.slf4j.LoggerFactory
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import at.mocode.tables.TurniereTable
fun configureDatabase() {
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,
// die von Docker Compose (aus .env) gesetzt werden.
val dbHost = System.getenv("DB_HOST") ?: "db" // Fallback auf 'db', falls nicht gesetzt
val dbPort = System.getenv("DB_PORT") ?: "5432"
val dbName = System.getenv("DB_NAME")
?: error("Database name (DB_NAME) not set in environment") // Fehler, wenn nicht gesetzt
val dbUser = System.getenv("DB_USER")
?: error("Database user (DB_USER) not set in environment") // Fehler, wenn nicht gesetzt
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
// Prüfen, ob wir in einer Testumgebung sind (z.B. über System Property)
val isTestEnvironment = System.getProperty("isTestEnvironment")?.toBoolean() ?: false
// Baue die JDBC URL zusammen
val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName"
log.info("Attempting to connect to database at URL: {}", jdbcURL) // Logge die URL (ohne User/Passwort!)
// Konfiguriere HikariCP mit den Werten aus der Umgebung
val hikariConfig = HikariConfig().apply {
this.driverClassName = driverClassName
this.jdbcUrl = jdbcURL
this.username = dbUser
this.password = dbPassword
this.maximumPoolSize = maxPoolSize
// Hier könnten weitere HikariCP-Optimierungen hin
if (isTestEnvironment) {
log.info("Test environment detected, using in-memory H2 database (test)...")
try {
this.validate() // Prüft die Konfiguration frühzeitig
// 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("HikariCP configuration validation failed!", e)
throw e // Wirft den Fehler weiter, damit die App nicht startet
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 dbName = System.getenv("DB_NAME") ?: error("DB_NAME not set in environment")
val dbUser = System.getenv("DB_USER") ?: error("DB_USER not set in environment")
val dbPassword = System.getenv("DB_PASSWORD") ?: error("DB_PASSWORD not set in environment")
val driverClassName = "org.postgresql.Driver"
val maxPoolSize = System.getenv("DB_POOL_SIZE")?.toIntOrNull() ?: 10
val jdbcURL = "jdbc:postgresql://$dbHost:$dbPort/$dbName"
log.info("Attempting to connect to PostgreSQL at URL: {}", jdbcURL)
val hikariConfig = HikariConfig().apply {
this.driverClassName = driverClassName
this.jdbcUrl = jdbcURL
this.username = dbUser
this.password = dbPassword
this.maximumPoolSize = maxPoolSize
this.validate()
}
val dataSource = HikariDataSource(hikariConfig)
Database.connect(dataSource)
log.info("PostgreSQL connection pool initialized successfully!")
connectionSuccessful = true
} catch (e: Exception) {
log.error("Failed to initialize PostgreSQL connection pool!", e)
throw e // Fehler weiterwerfen, App soll nicht starten ohne DB in Prod
}
}
}
// Erstelle DataSource und verbinde Exposed
try {
val dataSource = HikariDataSource(hikariConfig)
Database.connect(dataSource)
log.info("Database connection pool initialized successfully!")
} catch (e: Exception) {
log.error("Failed to initialize database connection pool!", e)
// Optional: Hier entscheiden, ob die App trotzdem starten soll oder nicht.
// 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 ---
// Hier kommt später die Logik zum Erstellen der Tabellen hin,
@ -64,4 +104,4 @@ fun configureDatabase() {
// SchemaUtils.create(TurniereTable) // Erstellt die Tabelle, wenn sie nicht existiert
// }
// ------------------------------------
}
}

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
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.* // Wichtig für testApplication
import kotlin.test.* // Wichtig für assertEquals, assertTrue etc.
import at.mocode.model.Turnier
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
import java.io.StringWriter
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ApplicationTest {
@Test
fun testRootRoute() = testApplication {
application {
module() // Ruft deine Konfigurationsfunktion auf
}
fun testRootRouteShowsTournamentList() {
// Erstelle ein Beispiel-Turnier, das in der Datenbank sein würde
val mockTurnier = Turnier(
id = "dummy-01",
name = "Erstes DB Turnier",
datum = "19.04.2025",
logoUrl = null,
ausschreibungUrl = "/pdfs/ausschreibung_dummy.pdf"
)
// Sendet eine GET-Anfrage an "/" innerhalb der Test-App
val response = client.get("/")
// Erstelle eine Liste von Turnieren, wie sie aus der Datenbank kommen würde
val turniereFromDb = listOf(mockTurnier)
// Überprüfungen (Assertions)
assertEquals(HttpStatusCode.OK, response.status, "Status Code should be OK")
val content = response.bodyAsText() // Holt den HTML-Body als Text
assertTrue(content.contains("Ktor: Hello, Java 21.0.6!"), "Welcome message missing")
// 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):" }
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)" } }
}
}
}.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,12 +3,12 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
plugins {
alias(libs.plugins.kotlinMultiplatform)
kotlin("plugin.serialization") version libs.versions.kotlin.get()
}
kotlin {
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser {
@ -25,11 +25,21 @@ kotlin {
}
}
}
sourceSets {
commonMain.dependencies {
// put your Multiplatform dependencies here
val commonMain by getting {
dependencies {
// 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.
)