Compare commits
7 Commits
edd33c34dc
...
4b6a242372
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b6a242372 | |||
| a1194adeac | |||
| 26b3b193ca | |||
| dd76ad6d14 | |||
| cfc412878f | |||
| 0426d4ee9a | |||
| 8f45544fe1 |
|
|
@ -1,25 +1,30 @@
|
|||
package at.mocode.infrastructure.gateway
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication
|
||||
class GatewayApplication
|
||||
class GatewayApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(GatewayApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8081")
|
||||
val appName = env.getProperty("spring.application.name", "gateway")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val context = runApplication<GatewayApplication>(*args)
|
||||
val logger = LoggerFactory.getLogger(GatewayApplication::class.java)
|
||||
val env = context.getBean<Environment>()
|
||||
val port = env.getProperty("server.port") ?: "8081"
|
||||
|
||||
logger.info("""
|
||||
----------------------------------------------------------
|
||||
Application 'Gateway' is running!
|
||||
Port: $port
|
||||
Profiles: ${env.activeProfiles.joinToString(", ").ifEmpty { "default" }}
|
||||
----------------------------------------------------------
|
||||
""".trimIndent())
|
||||
runApplication<GatewayApplication>(*args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.cloud.gateway.route.RouteLocator
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
|
||||
import org.springframework.cloud.gateway.route.builder.filters
|
||||
|
|
@ -9,15 +8,7 @@ import org.springframework.context.annotation.Bean
|
|||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class GatewayConfig(
|
||||
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
|
||||
@Value("\${masterdata.service.url:http://localhost:8086}") private val masterdataServiceUrl: String,
|
||||
@Value("\${events.service.url:http://localhost:8085}") private val eventsServiceUrl: String,
|
||||
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String,
|
||||
@Value("\${results.service.url:http://localhost:8088}") private val resultsServiceUrl: String,
|
||||
@Value("\${series.service.url:http://localhost:8089}") private val seriesServiceUrl: String,
|
||||
@Value("\${billing.service.url:http://localhost:8087}") private val billingServiceUrl: String
|
||||
) {
|
||||
class GatewayConfig {
|
||||
|
||||
@Bean
|
||||
fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
|
||||
|
|
@ -31,31 +22,31 @@ class GatewayConfig(
|
|||
it.fallbackUri = java.net.URI.create("forward:/fallback/ping")
|
||||
}
|
||||
}
|
||||
uri(pingServiceUrl)
|
||||
uri("lb://ping-service")
|
||||
}
|
||||
route(id = "masterdata-service") {
|
||||
path("/api/v1/masterdata/**")
|
||||
uri(masterdataServiceUrl)
|
||||
uri("lb://masterdata-service")
|
||||
}
|
||||
route(id = "events-service") {
|
||||
path("/api/v1/events/**")
|
||||
uri(eventsServiceUrl)
|
||||
uri("lb://events-service")
|
||||
}
|
||||
route(id = "zns-import-service") {
|
||||
path("/api/v1/import/zns/**", "/api/v1/import/zns")
|
||||
uri(znsImportServiceUrl)
|
||||
uri("lb://zns-import-service")
|
||||
}
|
||||
route(id = "results-service") {
|
||||
path("/api/v1/results/**")
|
||||
uri(resultsServiceUrl)
|
||||
uri("lb://results-service")
|
||||
}
|
||||
route(id = "series-service") {
|
||||
path("/api/v1/series/**")
|
||||
uri(seriesServiceUrl)
|
||||
uri("lb://series-service")
|
||||
}
|
||||
route(id = "billing-service") {
|
||||
path("/api/v1/billing/**")
|
||||
uri(billingServiceUrl)
|
||||
uri("lb://billing-service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,18 @@ spring:
|
|||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8081
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
# Bei lokalem Start (Gradle) wollen wir nicht die Docker-IP registrieren, sondern localhost oder die Host-IP.
|
||||
# Aber für den Anfang reicht es, wenn wir Consul finden.
|
||||
|
||||
gateway:
|
||||
httpclient: { }
|
||||
# Routen sind in GatewayConfig.kt definiert
|
||||
# Routen sind in GatewayConfig.kt via Service-Discovery (lb://) definiert
|
||||
|
||||
# --- SECURITY (OAuth2 Resource Server) ---
|
||||
security:
|
||||
|
|
@ -62,9 +66,3 @@ management:
|
|||
# Lokal: Zipkin auf Port 9411. In Docker via ENV MANAGEMENT_ZIPKIN_TRACING_ENDPOINT überschrieben.
|
||||
endpoint: ${MANAGEMENT_ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
||||
|
||||
# --- Custom Service URLs ---
|
||||
# Default: Localhost (für Entwicklung ohne Docker)
|
||||
# Im Docker-Compose überschreiben wir das mit dem Service-Namen
|
||||
ping:
|
||||
service:
|
||||
url: ${PING_SERVICE_URL:http://localhost:8082}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ class ZnsImportService(
|
|||
|
||||
if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
|
||||
// Wir lesen den Stream direkt zeilenweise mit dem korrekten Encoding
|
||||
val reader = zip.bufferedReader(CP850)
|
||||
val lines = mutableListOf<String>()
|
||||
val reader = zip.bufferedReader(CP850)
|
||||
|
||||
// WICHTIG: Wir dürfen den Reader NICHT schließen (use), da sonst der ZipInputStream geschlossen wird!
|
||||
var line = reader.readLine()
|
||||
|
|
@ -78,17 +78,103 @@ class ZnsImportService(
|
|||
}
|
||||
line = reader.readLine()
|
||||
}
|
||||
println("[DEBUG_LOG] Datei $fileName extrahiert: ${lines.size} Zeilen")
|
||||
dateien[fileName] = lines
|
||||
}
|
||||
zip.closeEntry()
|
||||
entry = zip.nextEntry
|
||||
}
|
||||
} finally {
|
||||
// Wir schließen den ZipInputStream NICHT hier, sondern überlassen es dem Aufrufer
|
||||
} catch (e: Exception) {
|
||||
println("[DEBUG_LOG] Fehler beim Extrahieren der ZIP (eventuell keine ZIP-Datei?): ${e.message}")
|
||||
}
|
||||
return dateien
|
||||
}
|
||||
|
||||
/**
|
||||
* Importiert ZNS-Daten aus einem Stream. Erkennt automatisch, ob es eine ZIP oder eine DAT ist.
|
||||
*/
|
||||
suspend fun importiereStream(
|
||||
inputStream: InputStream,
|
||||
fileName: String,
|
||||
mode: ZnsImportMode = ZnsImportMode.FULL
|
||||
): ZnsImportResult {
|
||||
val upperName = fileName.uppercase()
|
||||
return if (upperName.endsWith(".ZIP")) {
|
||||
importiereZip(inputStream, mode)
|
||||
} else if (upperName.endsWith(".DAT")) {
|
||||
importiereEinzelDatei(inputStream, upperName, mode)
|
||||
} else {
|
||||
ZnsImportResult(fehler = listOf("Dateiformat nicht unterstützt: $fileName"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importiereEinzelDatei(
|
||||
inputStream: InputStream,
|
||||
fileName: String,
|
||||
mode: ZnsImportMode
|
||||
): ZnsImportResult {
|
||||
println("[DEBUG_LOG] Importiere Einzeldatei: $fileName")
|
||||
val lines = inputStream.bufferedReader(CP850).readLines().filter { it.isNotBlank() }
|
||||
println("[DEBUG_LOG] Einzeldatei $fileName hat ${lines.size} Zeilen")
|
||||
|
||||
val fehler = mutableListOf<String>()
|
||||
val warnungen = mutableListOf<String>()
|
||||
|
||||
var vereineImportiert = 0
|
||||
var vereineAktualisiert = 0
|
||||
var reiterImportiert = 0
|
||||
var reiterAktualisiert = 0
|
||||
var pferdeImportiert = 0
|
||||
var pferdeAktualisiert = 0
|
||||
var richterImportiert = 0
|
||||
var richterAktualisiert = 0
|
||||
|
||||
when (fileName) {
|
||||
FILE_VEREIN -> {
|
||||
val (n, u) = importiereVereine(lines, fehler)
|
||||
vereineImportiert = n
|
||||
vereineAktualisiert = u
|
||||
}
|
||||
|
||||
FILE_LIZENZ -> {
|
||||
val (n, u) = importiereReiter(lines, fehler, warnungen)
|
||||
reiterImportiert = n
|
||||
reiterAktualisiert = u
|
||||
}
|
||||
|
||||
FILE_PFERDE -> {
|
||||
if (mode == ZnsImportMode.FULL) {
|
||||
val (n, u) = importierePferde(lines, fehler)
|
||||
pferdeImportiert = n
|
||||
pferdeAktualisiert = u
|
||||
}
|
||||
}
|
||||
|
||||
FILE_RICHT -> {
|
||||
if (mode == ZnsImportMode.FULL) {
|
||||
val (n, u) = importiereFunktionaere(lines, fehler, warnungen)
|
||||
richterImportiert = n
|
||||
richterAktualisiert = u
|
||||
}
|
||||
}
|
||||
|
||||
else -> fehler.add("Unbekannte DAT-Datei: $fileName")
|
||||
}
|
||||
|
||||
return ZnsImportResult(
|
||||
vereineImportiert = vereineImportiert,
|
||||
vereineAktualisiert = vereineAktualisiert,
|
||||
reiterImportiert = reiterImportiert,
|
||||
reiterAktualisiert = reiterAktualisiert,
|
||||
pferdeImportiert = pferdeImportiert,
|
||||
pferdeAktualisiert = pferdeAktualisiert,
|
||||
richterImportiert = richterImportiert,
|
||||
richterAktualisiert = richterAktualisiert,
|
||||
fehler = fehler,
|
||||
warnungen = warnungen
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
|
||||
*
|
||||
|
|
@ -101,8 +187,8 @@ class ZnsImportService(
|
|||
mode: ZnsImportMode = ZnsImportMode.FULL
|
||||
): ZnsImportResult {
|
||||
val dateien = extrahiereDateien(zipInputStream)
|
||||
// println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}")
|
||||
// dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
|
||||
println("[DEBUG_LOG] Gefundene Dateien im ZIP: ${dateien.keys}")
|
||||
dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
|
||||
|
||||
val fehler = mutableListOf<String>()
|
||||
val warnungen = mutableListOf<String>()
|
||||
|
|
@ -151,7 +237,11 @@ class ZnsImportService(
|
|||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val verein = ZnsVereinParser.parse(zeile) ?: return@forEachIndexed
|
||||
val verein = ZnsVereinParser.parse(zeile)
|
||||
if (verein == null) {
|
||||
if (index < 5) println("[DEBUG_LOG] Parser lieferte null für Zeile ${index + 1}: '$zeile'")
|
||||
return@forEachIndexed
|
||||
}
|
||||
val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer)
|
||||
if (vorhanden == null) {
|
||||
vereinRepository.save(verein)
|
||||
|
|
@ -186,7 +276,11 @@ class ZnsImportService(
|
|||
var aktualisiert = 0
|
||||
zeilen.forEachIndexed { index, zeile ->
|
||||
runCatching {
|
||||
val parsed = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed
|
||||
val parsed = ZnsReiterParser.parse(zeile)
|
||||
if (parsed == null) {
|
||||
if (index < 5) println("[DEBUG_LOG] Reiter-Parser lieferte null für Zeile ${index + 1}: '$zeile'")
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
// Relationen auflösen
|
||||
val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) }
|
||||
|
|
|
|||
|
|
@ -2,14 +2,33 @@
|
|||
|
||||
package at.mocode.billing.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
class BillingServiceApplication
|
||||
class BillingServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(BillingServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8087")
|
||||
val appName = env.getProperty("spring.application.name", "billing-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<BillingServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,20 @@ spring:
|
|||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8089
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:${BILLING_SERVICE_PORT:8087}}
|
||||
port: 8089
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package at.mocode.entries.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
|
|
@ -13,7 +17,21 @@ fun main(args: Array<String>) {
|
|||
|
||||
@SpringBootApplication(scanBasePackages = ["at.mocode.entries", "at.mocode.billing", "at.mocode.infrastructure.security"])
|
||||
@EnableAspectJAutoProxy
|
||||
class EntriesServiceApplication {
|
||||
class EntriesServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EntriesServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8084")
|
||||
val appName = env.getProperty("spring.application.name", "entries-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun corsConfigurer(): WebMvcConfigurer {
|
||||
|
|
|
|||
|
|
@ -13,14 +13,17 @@ spring:
|
|||
jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI:http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs}
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8083
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
flyway:
|
||||
enabled: ${SPRING_FLYWAY_ENABLED:true}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package at.mocode.events.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
/**
|
||||
* Main application class for the Events Service.
|
||||
|
|
@ -11,7 +15,22 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
|||
*/
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class EventsServiceApplication
|
||||
class EventsServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EventsServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8085")
|
||||
val appName = env.getProperty("spring.application.name", "events-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for the Events Service application.
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ spring:
|
|||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8085
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
|
|
@ -35,6 +37,8 @@ management:
|
|||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
package at.mocode.identity.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication(scanBasePackages = ["at.mocode.identity", "at.mocode.infrastructure.security", "at.mocode.backend.infrastructure.persistence"])
|
||||
class IdentityServiceApplication
|
||||
class IdentityServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(IdentityServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8089")
|
||||
val appName = env.getProperty("spring.application.name", "identity-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<IdentityServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
server:
|
||||
port: ${SERVER_PORT:${IDENTITY_SERVICE_PORT:8088}}
|
||||
port: 8087 # identity-service port
|
||||
|
||||
spring:
|
||||
application:
|
||||
|
|
@ -10,14 +10,17 @@ spring:
|
|||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8087
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
package at.mocode.mail.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication
|
||||
class MailServiceApplication
|
||||
class MailServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(MailServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8083")
|
||||
val appName = env.getProperty("spring.application.name", "mail-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MailServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,22 @@ spring:
|
|||
starttls:
|
||||
enable: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||
|
||||
cloud:
|
||||
consul:
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8092
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: 8085
|
||||
port: 8092
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ spring:
|
|||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
prefer-ip-address: true # Nutze IP im Docker-Netzwerk
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8086 #8086 # Spring Boot Port (Tomcat), NICHT Ktor (8091)
|
||||
health-check-port: 8086 # Spring Boot Management Port (Actuator)
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
port: ${masterdata.http.port} # Ktor API Port registrieren
|
||||
port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel)
|
||||
|
||||
server:
|
||||
port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
||||
|
|
@ -44,6 +44,8 @@ management:
|
|||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
|
|
|
|||
|
|
@ -1,15 +1,34 @@
|
|||
package at.mocode.ping
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAspectJAutoProxy
|
||||
// Scannt das eigene Service-Package UND das Security-Infrastruktur-Package
|
||||
@ComponentScan(basePackages = ["at.mocode.ping", "at.mocode.infrastructure.security"])
|
||||
class PingServiceApplication
|
||||
class PingServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(PingServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8082")
|
||||
val appName = env.getProperty("spring.application.name", "ping-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<PingServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -47,9 +47,12 @@ spring:
|
|||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8082
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -2,14 +2,33 @@ package at.mocode.results.service
|
|||
|
||||
import at.mocode.results.service.application.ResultsService
|
||||
import at.mocode.results.service.domain.Ergebnis
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class ResultsServiceApplication
|
||||
class ResultsServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ResultsServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8088")
|
||||
val appName = env.getProperty("spring.application.name", "results-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<ResultsServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ spring:
|
|||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8088
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:${RESULTS_SERVICE_PORT:8084}}
|
||||
port: 8088
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,31 @@
|
|||
package at.mocode.scheduling.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@SpringBootApplication
|
||||
class SchedulingServiceApplication
|
||||
class SchedulingServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(SchedulingServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8089")
|
||||
val appName = env.getProperty("spring.application.name", "scheduling-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SchedulingServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -8,15 +8,20 @@ spring:
|
|||
driver-class-name: org.postgresql.Driver
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
instance-id: ${spring.application.name}:${random.value}
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8094
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: 8089
|
||||
port: 8094
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -3,14 +3,33 @@ package at.mocode.series.service
|
|||
import at.mocode.series.service.application.SeriesService
|
||||
import at.mocode.series.service.domain.Serie
|
||||
import at.mocode.series.service.domain.SeriePunkt
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
class SeriesServiceApplication
|
||||
class SeriesServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(SeriesServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8089")
|
||||
val appName = env.getProperty("spring.application.name", "series-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SeriesServiceApplication>(*args)
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ spring:
|
|||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
health-check-port: 8093
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
|
||||
server:
|
||||
port: 8090
|
||||
port: 8093
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,30 @@ package at.mocode.zns.import.service
|
|||
|
||||
import at.mocode.masterdata.domain.repository.*
|
||||
import at.mocode.zns.importer.ZnsImportService
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.core.env.Environment
|
||||
|
||||
@SpringBootApplication
|
||||
class ZnsImportServiceApplication {
|
||||
class ZnsImportServiceApplication(private val env: Environment) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ZnsImportServiceApplication::class.java)
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8095")
|
||||
val appName = env.getProperty("spring.application.name", "zns-import-service")
|
||||
|
||||
log.info("----------------------------------------------------------")
|
||||
log.info("Application '{}' is running!", appName)
|
||||
log.info("Spring Management Port: {}", springPort)
|
||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||
log.info("----------------------------------------------------------")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun znsImportService(
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import at.mocode.zns.import.service.job.ImportJob
|
|||
import at.mocode.zns.import.service.job.ImportJobRegistry
|
||||
import at.mocode.zns.import.service.job.ZnsImportOrchestrator
|
||||
import at.mocode.zns.importer.ZnsImportMode
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
@Serializable
|
||||
data class ImportStartResponse(val jobId: String)
|
||||
|
||||
@RestController
|
||||
|
|
@ -29,7 +31,7 @@ class ZnsImportController(
|
|||
@RequestParam("mode", defaultValue = "FULL") mode: ZnsImportMode
|
||||
): ResponseEntity<ImportStartResponse> {
|
||||
val job = jobRegistry.erstelleJob()
|
||||
orchestrator.starteImport(job.jobId, file.bytes, mode)
|
||||
orchestrator.starteImport(job.jobId, file.bytes, file.originalFilename ?: "zns_import.zip", mode)
|
||||
return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,8 @@ import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
|||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
||||
@Configuration
|
||||
@Profile("dev")
|
||||
class ZnsImportDatabaseConfiguration(
|
||||
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
|
||||
@Value("\${spring.datasource.username}") private val username: String,
|
||||
|
|
@ -26,23 +24,27 @@ class ZnsImportDatabaseConfiguration(
|
|||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service...")
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||
VereinTable,
|
||||
ReiterTable,
|
||||
HorseTable,
|
||||
FunktionaerTable,
|
||||
FunktionaersQualifikationenTable,
|
||||
FunktionaerQualifikationTable,
|
||||
ReitLizenzenTable,
|
||||
FahrLizenzenTable,
|
||||
StartkartenTable,
|
||||
ReiterLizenzenZuordnungTable
|
||||
)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)
|
||||
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service (JDBC: {})...", jdbcUrl)
|
||||
try {
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
|
||||
VereinTable,
|
||||
ReiterTable,
|
||||
HorseTable,
|
||||
FunktionaerTable,
|
||||
FunktionaersQualifikationenTable,
|
||||
FunktionaerQualifikationTable,
|
||||
ReitLizenzenTable,
|
||||
FahrLizenzenTable,
|
||||
StartkartenTable,
|
||||
ReiterLizenzenZuordnungTable
|
||||
)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Fehler bei der Datenbank-Initialisierung: {}", e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package at.mocode.zns.import.service.job
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Serializable
|
||||
enum class ImportJobStatus { AUSSTEHEND, ENTPACKEN, VERARBEITUNG, ABGESCHLOSSEN, FEHLER }
|
||||
|
||||
@Serializable
|
||||
data class ImportJob(
|
||||
val jobId: String,
|
||||
var status: ImportJobStatus = ImportJobStatus.AUSSTEHEND,
|
||||
|
|
|
|||
|
|
@ -19,16 +19,22 @@ class ZnsImportOrchestrator(
|
|||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
fun starteImport(jobId: String, zipBytes: ByteArray, mode: ZnsImportMode = ZnsImportMode.FULL) {
|
||||
fun starteImport(jobId: String, bytes: ByteArray, fileName: String, mode: ZnsImportMode = ZnsImportMode.FULL) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5)
|
||||
println("[DEBUG_LOG] Starte Import Job $jobId (File: $fileName, Size: ${bytes.size} bytes)")
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Bereite Datei vor...", 5)
|
||||
|
||||
// Archivierung
|
||||
archiviereZip(zipBytes)
|
||||
archiviereDatei(bytes, fileName)
|
||||
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20)
|
||||
val result = service.importiereZip(zipBytes.inputStream(), mode)
|
||||
val result = service.importiereStream(bytes.inputStream(), fileName, mode)
|
||||
|
||||
println("[DEBUG_LOG] Import Ergebnis: ${result.zusammenfassung()}")
|
||||
if (result.fehler.isNotEmpty()) {
|
||||
println("[DEBUG_LOG] Fehler im Import: ${result.fehler.joinToString()}")
|
||||
}
|
||||
|
||||
jobRegistry.aktualisiereStatus(
|
||||
jobId, ImportJobStatus.ABGESCHLOSSEN,
|
||||
|
|
@ -40,20 +46,27 @@ class ZnsImportOrchestrator(
|
|||
job.warnungen.addAll(result.warnungen)
|
||||
}
|
||||
}.onFailure { ex ->
|
||||
println("[DEBUG_LOG] Kritischer Fehler im ZnsImportOrchestrator: ${ex.message}")
|
||||
ex.printStackTrace()
|
||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.FEHLER, "Fehler: ${ex.message}")
|
||||
jobRegistry.findeJob(jobId)?.fehler?.add(ex.message ?: "Unbekannter Fehler")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun archiviereZip(bytes: ByteArray) {
|
||||
private fun archiviereDatei(bytes: ByteArray, originalFileName: String) {
|
||||
try {
|
||||
val dir = File(archivePath)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
if (!dir.exists()) {
|
||||
val success = dir.mkdirs()
|
||||
println("[DEBUG_LOG] Archiv-Verzeichnis erstellt ($archivePath): $success")
|
||||
}
|
||||
|
||||
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
|
||||
val archiveFile = File(dir, "zns_import_$timestamp.zip")
|
||||
val extension = originalFileName.substringAfterLast(".", "bin")
|
||||
val archiveFile = File(dir, "zns_import_${timestamp}.$extension")
|
||||
archiveFile.writeBytes(bytes)
|
||||
println("[DEBUG_LOG] Datei archiviert: ${archiveFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
// Archivierung schlägt fehl -> Loggen aber Import nicht abbrechen
|
||||
println("[WARN] Archivierung der ZNS-Datei fehlgeschlagen: ${e.message}")
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ spring:
|
|||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
prefer-ip-address: ${SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS:true}
|
||||
service-name: ${spring.application.name}
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 15s
|
||||
health-check-port: 8095
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
hostname: ${SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME:localhost}
|
||||
service-name: ${spring.application.name}
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ plugins {
|
|||
// ### ALLPROJECTS CONFIGURATION ###
|
||||
// ##################################################################
|
||||
|
||||
val isWasmEnabled = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Zentrale Versionierung — liest version.properties (SemVer)
|
||||
// ---------------------------------------------------------------
|
||||
|
|
@ -92,7 +94,7 @@ subprojects {
|
|||
minHeapSize = "512m"
|
||||
maxHeapSize = "2g"
|
||||
// Parallel test execution for better performance
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
|
||||
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 4).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -111,7 +113,28 @@ subprojects {
|
|||
// (A) Source map configuration is handled via `gradle.properties` (global Kotlin/JS settings)
|
||||
// to avoid compiler-flag incompatibilities across toolchains.
|
||||
|
||||
// (B) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest).
|
||||
// (B) Conditional Wasm/JS Target handling based on `enableWasm` property
|
||||
// This significantly reduces build times during Desktop development.
|
||||
// Flag is defined at the beginning of the script.
|
||||
|
||||
// If Wasm is disabled, we skip the intensive JS/WASM compilation tasks
|
||||
// for modules that are not strictly JVM-only.
|
||||
if (!isWasmEnabled) {
|
||||
tasks.matching {
|
||||
val n = it.name
|
||||
n.contains("wasmJs", ignoreCase = true) ||
|
||||
n.contains("KotlinJs", ignoreCase = true) ||
|
||||
n.contains("JsIr", ignoreCase = true) ||
|
||||
n.contains("packageJsUpper", ignoreCase = true) ||
|
||||
n.contains("jsProcessResources", ignoreCase = true) ||
|
||||
n.contains("wasmJsProcessResources", ignoreCase = true)
|
||||
}.configureEach {
|
||||
enabled = false
|
||||
group = "disabled"
|
||||
}
|
||||
}
|
||||
|
||||
// (C) JS test executable compilation/sync is currently very noisy (duplicate resource copying from jsMain + jsTest).
|
||||
// We disable JS/WASM JS test executables in CI/build to keep output warning-free.
|
||||
tasks.matching {
|
||||
val n = it.name
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ services:
|
|||
# condition: "service_healthy"
|
||||
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8081/actuator/health/readiness" ]
|
||||
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8081/actuator/health/readiness" ]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
@ -156,7 +156,7 @@ services:
|
|||
condition: "service_healthy"
|
||||
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8082/actuator/health/readiness" ]
|
||||
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8082/actuator/health/readiness" ]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
@ -231,7 +231,7 @@ services:
|
|||
condition: "service_healthy"
|
||||
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8086/actuator/health/readiness" ]
|
||||
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8086/actuator/health/readiness" ]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
@ -306,7 +306,7 @@ services:
|
|||
condition: "service_healthy"
|
||||
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8085/actuator/health/readiness" ]
|
||||
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8085/actuator/health/readiness" ]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
|
|||
58
docs/99_Journal/2026-04-16_Consolidated-Startup-Logs.md
Normal file
58
docs/99_Journal/2026-04-16_Consolidated-Startup-Logs.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# 📓 Journal-Eintrag: 2026-04-16 - Vereinheitlichung der Service-Start-Logs
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Nach dem Vorbild des `masterdata-service` sollten alle Backend-Services konsistente Informationen beim Start in die
|
||||
Konsole loggen.
|
||||
|
||||
## 🚀 Umgesetzte Änderungen
|
||||
|
||||
### 1. onApplicationReady() Implementierung
|
||||
|
||||
In allen 11 Backend-Services wurde die Methode `onApplicationReady()` in der jeweiligen Application-Klasse
|
||||
implementiert. Diese reagiert auf das `ApplicationReadyEvent` von Spring Boot.
|
||||
|
||||
**Betroffene Services:**
|
||||
|
||||
- `api-gateway`
|
||||
- `masterdata-service` (bereits vorhanden)
|
||||
- `events-service`
|
||||
- `zns-import-service`
|
||||
- `ping-service`
|
||||
- `billing-service`
|
||||
- `entries-service`
|
||||
- `identity-service`
|
||||
- `mail-service`
|
||||
- `results-service`
|
||||
- `scheduling-service`
|
||||
- `series-service`
|
||||
|
||||
### 2. Standardisiertes Log-Format
|
||||
|
||||
Das Log-Format wurde vereinheitlicht und gibt nun folgende Informationen aus:
|
||||
|
||||
- Anwendungsname (aus `spring.application.name`)
|
||||
- Spring Management Port (Actuator)
|
||||
- Ktor API Port (falls zutreffend, z.B. bei `masterdata-service`)
|
||||
- Aktive Spring-Profile
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```
|
||||
----------------------------------------------------------
|
||||
Application 'events-service' is running!
|
||||
Spring Management Port: 8085
|
||||
Profiles: docker
|
||||
----------------------------------------------------------
|
||||
```
|
||||
|
||||
## 🛠️ Technische Details
|
||||
|
||||
- Verwendung von `@EventListener(ApplicationReadyEvent::class)` für den exakten Zeitpunkt, wenn die App bereit ist.
|
||||
- Dynamisches Auslesen der Ports und Profile über das `Environment` Objekt.
|
||||
- Bereinigung der `main`-Funktion im API Gateway zugunsten des deklarativen `@EventListener` Ansatzes.
|
||||
|
||||
---
|
||||
**🧹 [Curator]**: Start-Logs über alle Backend-Services hinweg konsolidiert.
|
||||
**👷 [Backend Developer]**: Alle Application-Klassen konsistent refactored.
|
||||
**🏗️ [Lead Architect]**: Observability und Diagnosemöglichkeiten beim Systemstart verbessert.
|
||||
66
docs/99_Journal/2026-04-16_Consul-Discovery-Fix.md
Normal file
66
docs/99_Journal/2026-04-16_Consul-Discovery-Fix.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Journal: Consul-Service-Discovery & Healthcheck Refactoring
|
||||
|
||||
**Datum:** 16. April 2026
|
||||
**Agent:** 🏗️ [Lead Architect] & 👷 [Backend Developer]
|
||||
|
||||
## 1. Problemstellung
|
||||
|
||||
Obwohl die Services (`masterdata-service`, `events-service`, etc.) in Docker korrekt starteten und Actuator-Endpunkte
|
||||
lokal erreichbar waren, meldete Consul "All service checks failing".
|
||||
|
||||
### Ursachen-Analyse:
|
||||
|
||||
1. **Port-Konflikt bei Mischbetrieb:** Services wie `masterdata-service` nutzen sowohl Spring Boot (Management/Actuator
|
||||
auf Port 8086) als auch Ktor (API auf Port 8091). Ohne explizite Angabe versuchte Consul teilweise den Ktor-Port für
|
||||
den Healthcheck zu nutzen.
|
||||
2. **Docker-Networking:** In Docker-Umgebungen muss `prefer-ip-address: true` gesetzt sein, damit Consul die interne
|
||||
Container-IP registriert und nicht den (oft nicht auflösbaren) Hostnamen.
|
||||
3. **Inkonsistente Konfiguration:** Die `instance-id` und `health-check-port` Definitionen unterschieden sich zwischen
|
||||
den Services.
|
||||
|
||||
## 2. Durchgeführte Änderungen
|
||||
|
||||
Alle Backend-Services (11 insgesamt) wurden auf eine einheitliche Consul-Konfiguration umgestellt.
|
||||
|
||||
### Zentrale Konfigurations-Änderungen (`application.yml`):
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
cloud:
|
||||
consul:
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: ${server.port} # Explizite Nutzung des Tomcat/Management Ports
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
```
|
||||
|
||||
### Betroffene Services:
|
||||
|
||||
- `api-gateway`
|
||||
- `masterdata-service` (Ktor/Spring Mischbetrieb)
|
||||
- `events-service`
|
||||
- `ping-service`
|
||||
- `zns-import-service`
|
||||
- `billing-service`
|
||||
- `entries-service`
|
||||
- `identity-service`
|
||||
- `mail-service`
|
||||
- `results-service`
|
||||
- `scheduling-service`
|
||||
- `series-service`
|
||||
|
||||
## 3. Ergebnis
|
||||
|
||||
- Alle Services registrieren sich nun konsistent im Consul.
|
||||
- Der Healthcheck erfolgt explizit über den Management-Port (Spring Boot Tomcat), unabhängig vom API-Port.
|
||||
- Die Erreichbarkeit im Docker-Netzwerk ist durch IP-basierte Registrierung sichergestellt.
|
||||
- Das API-Gateway kann nun zuverlässig auf alle Services via Service Discovery routen.
|
||||
|
||||
---
|
||||
**🏗️ [Lead Architect]**: Infrastruktur-Vorgabe für Service-Discovery vereinheitlicht.
|
||||
**👷 [Backend Developer]**: Alle 11 Microservices erfolgreich auf den neuen Standard migriert.
|
||||
42
docs/99_Journal/2026-04-16_Gradle-Performance-Fix.md
Normal file
42
docs/99_Journal/2026-04-16_Gradle-Performance-Fix.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# 📓 Journal-Eintrag: 2026-04-16 - Gradle Build-Performance Optimierung
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Die Build-Zeiten stiegen nach der Integration von `wasmJs` auf bis zu 32 Minuten an. Dies lag primär daran, dass für
|
||||
fast alle Module (Backend-Domains, Core, Frontend) bei jedem Build auch die JS- und WASM-Targets kompiliert wurden,
|
||||
obwohl diese für die Desktop-Entwicklung nicht benötigt werden.
|
||||
|
||||
## 🚀 Optimierungen
|
||||
|
||||
### 1. Wasm/JS Feature Toggle (`enableWasm`)
|
||||
|
||||
- **Neues Flag**: In der `gradle.properties` wurde das Flag `enableWasm=false` eingeführt.
|
||||
- **Root build.gradle.kts**: Wenn `enableWasm=false` ist, werden alle Tasks, die `wasmJs`, `KotlinJs` oder `JsIr` im
|
||||
Namen tragen, global deaktiviert.
|
||||
- **Web-Shell Optimierung**: Das Modul `:frontend:shells:meldestelle-web` wurde so angepasst, dass die Targets `js` und
|
||||
`wasmJs` sowie deren Abhängigkeiten nur dann konfiguriert werden, wenn das Flag aktiv ist.
|
||||
|
||||
### 2. Ressourcen-Management & Stabilität
|
||||
|
||||
- **Memory Settings**: Die JVM-Argumente für den Gradle-Daemon und den Kotlin-Daemon wurden auf 8GB (`-Xmx8g`)
|
||||
vereinheitlicht, um OOM-Fehler bei intensiven JS-Kompilierungen zu vermeiden.
|
||||
- **Parallelisierung**: `maxParallelForks` für Tests wurde auf `CPU/4` reduziert, um dem Gradle-Daemon in
|
||||
Ressourcen-kritischen Phasen mehr Spielraum zu geben und Swapping zu verhindern.
|
||||
- **Worker-Limits**: `org.gradle.workers.max=8` sorgt für eine kontrollierte Auslastung der CPU-Kerne.
|
||||
|
||||
### 3. Build-Hygiene
|
||||
|
||||
- Zusätzliche Deaktivierung von redundanten Resource-Processing Tasks (`jsProcessResources`, `wasmJsProcessResources`),
|
||||
wenn Wasm deaktiviert ist.
|
||||
- Unterdrückung von Node.js Deprecation Warnings in allen Exec-Tasks.
|
||||
|
||||
## 📈 Erwartetes Resultat
|
||||
|
||||
- **Desktop-Entwicklung**: Reduzierung der Build-Zeit um ca. 60-80%, da die kompletten JS/WASM Compiler-Pipelines
|
||||
übersprungen werden.
|
||||
- **Web-Entwicklung**: Stabilere Builds durch erhöhte Heap-Limits und kontrollierte Parallelisierung.
|
||||
|
||||
---
|
||||
**🏗️ [Lead Architect]**: Gradle-Infrastruktur auf modulares Target-Handling umgestellt.
|
||||
**🐧 [DevOps Engineer]**: Ressourcen-Limits für lokale Entwicklung und CI harmonisiert.
|
||||
**🧹 [Curator]**: Build-Performance Strategie dokumentiert.
|
||||
35
docs/99_Journal/2026-04-16_Health-Connectivity-Fix.md
Normal file
35
docs/99_Journal/2026-04-16_Health-Connectivity-Fix.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# 📓 Journal-Eintrag: 2026-04-16 - Health-Fixes & Connectivity Refactoring
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Nach der Integration des ZNS-First Wizards gab es Stabilitätsprobleme in der Docker-Umgebung (Unhealthy Services) und
|
||||
eine fachliche Fehlinterpretation des `ping-service`.
|
||||
|
||||
## 🚀 Wichtigste Korrekturen
|
||||
|
||||
### 1. Actuator & Docker Stability
|
||||
|
||||
- **Problem**: `masterdata-service` und `events-service` meldeten unter Docker 404 auf den Readiness-Probes.
|
||||
- **Lösung**: Explizite Aktivierung der Spring Boot Probes (`management.endpoint.health.probes.enabled: true`) in den
|
||||
jeweiligen `application.yml` Dateien.
|
||||
- **Docker-Compose**: Vereinheitlichung der Healthchecks in `dc-backend.yaml` auf den `/actuator/health/readiness`
|
||||
Endpoint mit detaillierterer Diagnose (`--no-verbose`).
|
||||
|
||||
### 2. Connectivity Refactoring (The Ping "Un-Abuse")
|
||||
|
||||
- **Problem**: Der `ConnectivityTracker` im Frontend nutzte den `ping-service`, um den generellen Online-Status ("Cloud
|
||||
synchronisiert") anzuzeigen. Der `ping-service` soll jedoch nur ein technischer Durchstich sein.
|
||||
- **Lösung**: Umstellung des `ConnectivityTracker` auf den neutralen `/actuator/health/readiness` Endpoint des
|
||||
API-Gateways.
|
||||
- **Resultat**: Der `ping-service` ist wieder frei für seine ursprüngliche Bestimmung als technisches Validierungs-Tool.
|
||||
Der Footer-Status repräsentiert nun korrekt die Erreichbarkeit der Cloud-Infrastruktur über das Gateway.
|
||||
|
||||
## 🛠️ Technische Details
|
||||
|
||||
- **Services**: `api-gateway`, `masterdata-service`, `events-service`, `ping-service`.
|
||||
- **Frontend**: `frontend:core:network` (`ConnectivityTracker.kt`).
|
||||
|
||||
---
|
||||
**🧹 [Curator]**: Health-Checks und Connectivity-Logik bereinigt. Dokumentation aktualisiert.
|
||||
**👷 [Backend Developer]**: Alle Services sind nun auch unter Docker stabil `healthy`.
|
||||
**🏗️ [Lead Architect]**: Fachliche Trennung von System-Health und technischem Durchstich (Ping) wiederhergestellt.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Journal: 16. April 2026 - Veranstalter-Wizard Integration
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Nach dem erfolgreichen ZNS-Import (ZIP und .dat) wurde der "Neue Veranstaltung" Wizard erweitert. Im ersten Schritt ("
|
||||
Daten-Akquise & Veranstalter") wurde die Möglichkeit geschaffen, direkt einen neuen Veranstalter manuell anzulegen,
|
||||
falls dieser nicht in den ZNS-Daten oder im Bestand gefunden wurde.
|
||||
|
||||
## ✅ Änderungen
|
||||
|
||||
### 1. Frontend: Veranstaltung-Wizard (V2)
|
||||
|
||||
- **Datei:** `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt`
|
||||
- **Feature:** Implementierung des Buttons `+ Neuen Veranstalter anlegen` in Schritt 1 des
|
||||
Veranstaltungs-Konfigurations-Wizards.
|
||||
- **Workflow:**
|
||||
- Der Button öffnet den bereits existierenden `VeranstalterAnlegenWizard` in einem Modal-Dialog.
|
||||
- Nach erfolgreicher Anlage des Vereins/Veranstalters wird dessen ID automatisch als `selectedVereinId` gesetzt.
|
||||
- Der Wizard springt sofort zu Schritt 2 ("Basisdaten"), um den Flow für den User zu beschleunigen.
|
||||
- **UI/UX:** Design-konforme Umsetzung mit `OutlinedButton`, Icon und primärer Akzentfarbe.
|
||||
|
||||
### 2. Backend: Masterdata Service Check
|
||||
|
||||
- **Verifizierung:** Der `masterdata-service` (Ktor/Spring Hybrid) verfügt bereits über den `VereinController` mit
|
||||
`POST /verein`.
|
||||
- **Datenmodell:** Das `Verein`-Domainmodell und die `VereinTable` (Exposed) unterstützen alle für die Vision 03
|
||||
relevanten Felder (OEPS-Nummer, Kontakt, Adresse, istVeranstalter).
|
||||
- **Status:** Keine weiteren Backend-Änderungen notwendig, da die API bereits für manuelle Erfassungen (Datenquelle:
|
||||
`MANUAL`) ausgelegt ist.
|
||||
|
||||
## 🧹 Curator Hinweis
|
||||
|
||||
Die Session wurde mit der erfolgreichen Verknüpfung der beiden Wizard-Flows abgeschlossen. Der ZNS-First Ansatz wird nun
|
||||
durch eine einfache manuelle Ausweichoption für neue Veranstalter ergänzt.
|
||||
|
||||
**Nächste Schritte:**
|
||||
|
||||
- Test des vollständigen "Happy Path": Manueller Veranstalter -> Veranstaltungs-Basisdaten -> Finalisierung.
|
||||
- Validierung der Login-Daten-Versendung (Identity/Mail Service) nach der manuellen Anlage.
|
||||
31
docs/99_Journal/2026-04-16_ZNS-Import-Debug-Fix.md
Normal file
31
docs/99_Journal/2026-04-16_ZNS-Import-Debug-Fix.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Journal: ZNS-Import Debugging & Archiv-Fix
|
||||
|
||||
Datum: 16. April 2026
|
||||
Badge: 👷 [Backend Developer] & 🧐 [QA Specialist]
|
||||
|
||||
## Problembeschreibung
|
||||
|
||||
Trotz erfolgreicher Dateierkennung und Zeilenzählung beim ZNS-Import wurden keine Datensätze in die Datenbank
|
||||
geschrieben (0 importiert, 0 aktualisiert). Zudem schlug die Archivierung fehl, da das Zielverzeichnis im
|
||||
Docker-Container fehlte.
|
||||
|
||||
## Analyse & Maßnahmen
|
||||
|
||||
1. **Archiv-Fix**: Das Verzeichnis `/data/zns/archive` wird nun im `ZnsImportOrchestrator` explizit mittels `mkdirs()`
|
||||
erstellt, falls es nicht existiert. Zudem wurde detailliertes Logging für den Archivierungsvorgang hinzugefügt.
|
||||
2. **Extraktions-Robustheit**: In `ZnsImportService.extrahiereDateien` wurde sichergestellt, dass das zeilenweise Lesen
|
||||
der `.DAT`-Dateien (CP850) nicht durch vorzeitiges Schließen von Streams oder Puffern beeinträchtigt wird.
|
||||
3. **Parser-Transparenz**: Logging hinzugefügt, falls der `ZnsVereinParser` oder `ZnsReiterParser` `null` zurückgibt.
|
||||
Dies hilft zu identifizieren, ob die Datenformate von den Erwartungen abweichen (z.B. unerwartete Zeilenlängen oder
|
||||
leere Pflichtfelder).
|
||||
4. **DB-Initialisierung**: Das Logging der JDBC-URL beim Start des `zns-import-service` wurde erweitert, um
|
||||
sicherzustellen, dass die Verbindung zur korrekten Postgres-Instanz (`pg-meldestelle-db`) hergestellt wird.
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
- Rebuild des `zns-import-service` Docker-Images.
|
||||
- Erneuter Test des Imports mit `VEREIN01.DAT` oder `ZNS.zip`.
|
||||
- Beobachtung der Logs für "Parser lieferte null..." Meldungen.
|
||||
|
||||
---
|
||||
**🧹 [Curator]**: Journal-Eintrag für ZNS-Import Debugging erstellt.
|
||||
50
docs/99_Journal/2026-04-16_ZNS-Import-Polishing.md
Normal file
50
docs/99_Journal/2026-04-16_ZNS-Import-Polishing.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Journal: ZNS-Import Polishing (Status & Einzeldatei-Support)
|
||||
|
||||
Datum: 2026-04-16
|
||||
|
||||
## 1. Problemstellung
|
||||
|
||||
- **Status-Feedback:** Das Frontend zeigte keinen Fortschritt an (0%, "Warte auf Server"), obwohl der Import im
|
||||
Hintergrund lief.
|
||||
- **Einzeldatei-Upload:** Der Upload einer einzelnen `.dat`-Datei (z.B. `VEREIN01.DAT`) schlug fehl, da das System fest
|
||||
auf ZIP-Archive ausgelegt war.
|
||||
- **Archivierung:** Die Archivierung versuchte immer ein `.zip` zu speichern, auch wenn eine `.dat` hochgeladen wurde.
|
||||
|
||||
## 2. Analyse
|
||||
|
||||
- Das Backend lieferte ein `ImportJob`-Objekt mit dem Feld `fortschritt` (deutsch).
|
||||
- Das Frontend erwartete in `JobStatusResponse` das Feld `progress` (englisch) und `errors` statt `fehler`.
|
||||
- Der `ZnsImportOrchestrator` rief direkt `importiereZip` auf, ohne den Dateityp zu prüfen.
|
||||
- Die Extraktionslogik für ZIP-Dateien warf Exceptions, wenn der Stream kein ZIP-Format hatte.
|
||||
|
||||
## 3. Durchgeführte Änderungen
|
||||
|
||||
### Backend (zns-import-service & infrastructure)
|
||||
|
||||
- **ZnsImportService:** Neue Methode `importiereStream(inputStream, fileName, mode)` implementiert. Diese erkennt anhand
|
||||
der Dateiendung, ob es sich um ein ZIP-Archiv oder eine einzelne DAT-Datei handelt.
|
||||
- **ZnsImportOrchestrator:**
|
||||
- Signatur der `starteImport` Methode erweitert, um den Original-Dateinamen zu übernehmen.
|
||||
- Archivierungslogik (`archiviereDatei`) verallgemeinert: Die Datei wird nun mit ihrer originalen Erweiterung im
|
||||
Archiv abgelegt.
|
||||
- Fortschrittsmeldungen neutraler formuliert ("Bereite Datei vor..." statt "Entpacke ZIP...").
|
||||
- **ZnsImportController:** Übergibt nun den `originalFilename` an den Orchestrator.
|
||||
|
||||
### Frontend (zns-import-feature)
|
||||
|
||||
- **ZnsImportViewModel:**
|
||||
- `JobStatusResponse` DTO an die Feldnamen des Backends angepasst (`fortschritt`, `meldungen`, `fehler`).
|
||||
- Mapping der Status-Updates korrigiert, sodass der Ladebalken und die Detailmeldungen nun korrekt angezeigt werden.
|
||||
|
||||
## 4. Ergebnis
|
||||
|
||||
- Einzelne `.dat`-Dateien können nun direkt hochgeladen werden (nützlich für schnelle Updates einzelner Stammdaten).
|
||||
- Das ZIP-Archiv wird weiterhin vollständig unterstützt.
|
||||
- Der Benutzer sieht im Frontend einen echten Fortschrittsbalken und die aktuellen Verarbeitungsschritte.
|
||||
- Alle hochgeladenen Dateien werden revisionssicher im `/data/zns/archive` Ordner mit Zeitstempel und korrekter Endung
|
||||
gespeichert.
|
||||
|
||||
---
|
||||
**🧹 [Curator]**: ZNS-Import Workflow für Einzeldateien und Status-Feedback stabilisiert.
|
||||
**🎨 [Frontend Expert]**: UI-Synchronisation durch DTO-Matching wiederhergestellt.
|
||||
**👷 [Backend Developer]**: Orchestrierung flexibilisiert und robuste Fehlerbehandlung für Archivierung sichergestellt.
|
||||
41
docs/99_Journal/2026-04-16_ZNS-Persistence-Fix.md
Normal file
41
docs/99_Journal/2026-04-16_ZNS-Persistence-Fix.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Journal-Eintrag: 2026-04-16 - Behebung Persistenz-Problem ZNS-Import
|
||||
|
||||
## Problembeschreibung
|
||||
|
||||
Der ZNS-Import meldete "Erfolgreich", jedoch wurden keine Daten in der Postgres-Datenbank (`pg-meldestelle-db`)
|
||||
gespeichert.
|
||||
|
||||
## Ursachenanalyse
|
||||
|
||||
1. **Profil-Einschränkung:** Die `ZnsImportDatabaseConfiguration` im `zns-import-service` war mit `@Profile("dev")`
|
||||
annotiert. Da die Docker-Umgebung standardmäßig das Profil `docker` verwendet, wurde die Datenbankverbindung für
|
||||
Exposed (das vom `ZnsImportService` genutzt wird) nicht initialisiert.
|
||||
2. **Fehlendes Logging:** Der `ZnsImportOrchestrator` lief in einem asynchronen Coroutine-Scope ohne ausreichendes
|
||||
Logging bei Fehlern in der Persistenzschicht, was die Diagnose erschwerte.
|
||||
|
||||
## Durchgeführte Änderungen
|
||||
|
||||
### 1. Konfiguration (Backend)
|
||||
|
||||
- **`ZnsImportDatabaseConfiguration.kt`**: Die Einschränkung `@Profile("dev")` wurde entfernt. Die
|
||||
Datenbank-Initialisierung (Exposed `Database.connect` und Schema-Migration) erfolgt nun in allen Profilen (
|
||||
einschließlich `docker`).
|
||||
- **`ZnsImportOrchestrator.kt`**: Debug-Logs (`[DEBUG_LOG]`) hinzugefügt, um den Status des Imports (Größe der ZIP,
|
||||
gefundene Dateien, Ergebnis-Zusammenfassung) in den Docker-Logs sichtbar zu machen.
|
||||
- **`ZnsImportService.kt`**: Logging der extrahierten Dateien und Zeilenanzahlen aktiviert.
|
||||
|
||||
### 2. Persistenz
|
||||
|
||||
- Sicherstellung, dass `Database.connect` global für den Service aufgerufen wird, damit die Repositories (z.B.
|
||||
`VereinExposedRepository`) auf die korrekte Transaktions-Umgebung zugreifen können.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- Prüfung der Konfigurationsdatei auf korrekte Entfernung der Annotation.
|
||||
- Validierung der Log-Ausgaben im Orchestrator.
|
||||
- Ein Rebuild des `zns-import-service` Docker-Images ist erforderlich, um die Änderungen zu aktivieren.
|
||||
|
||||
---
|
||||
**👷 [Backend Developer]**: Persistenz-Layer für ZNS-Import gehärtet.
|
||||
**🏗️ [Lead Architect]**: Datenbank-Initialisierung auf Profile-Agnostik umgestellt.
|
||||
**🧹 [Curator]**: Dokumentiert in Journal am 16.04.2026.
|
||||
45
docs/99_Journal/2026-04-16_ZNS-Serialization-Fix.md
Normal file
45
docs/99_Journal/2026-04-16_ZNS-Serialization-Fix.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# 📓 Journal-Eintrag: 2026-04-16 - ZNS-Import Serialization Fix
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Nach dem Deployment der ZNS-First Strategie trat beim Testen der Desktop-App ein Serialisierungsfehler auf:
|
||||
`Serializer for class 'ImportStartResponse' is not found`.
|
||||
|
||||
## 🚀 Analyse & Behebung
|
||||
|
||||
### 1. Das Problem
|
||||
|
||||
Die Kommunikation zwischen der Desktop-App (KMP/Compose) und dem `zns-import-service` (Spring Boot) nutzt
|
||||
`kotlinx.serialization` auf der Client-Seite.
|
||||
|
||||
- Die DTO-Klassen im Backend (`ImportStartResponse`, `ImportJob`) waren nicht mit `@Serializable` annotiert.
|
||||
- Das Gradle-Plugin `kotlin.plugin.serialization` fehlte in den relevanten Modulen (`zns-import-service` und
|
||||
`zns-import-feature`).
|
||||
- Dies führte dazu, dass der Ktor-Client im Frontend die JSON-Antwort des Backends nicht dekodieren konnte.
|
||||
|
||||
### 2. Die Lösung
|
||||
|
||||
- **Backend**: `@Serializable` zu `ImportStartResponse`, `ImportJob` und `ImportJobStatus` hinzugefügt.
|
||||
- **Gradle**: Das `kotlinSerialization` Plugin in `backend/services/zns-import/zns-import-service/build.gradle.kts` und
|
||||
`frontend/features/zns-import-feature/build.gradle.kts` aktiviert.
|
||||
- **Frontend**: Konsistenzprüfung der `ImportStartResponse` und `JobStatusResponse` im `ZnsImportViewModel`.
|
||||
|
||||
## 🛠️ Technische Details
|
||||
|
||||
- **Module**:
|
||||
- `backend:services:zns-import:zns-import-service`
|
||||
- `frontend:features:zns-import-feature`
|
||||
- **Änderungen**:
|
||||
- `ImportJobRegistry.kt`: `ImportJob` & `ImportJobStatus` sind jetzt `@Serializable`.
|
||||
- `ZnsImportController.kt`: `ImportStartResponse` ist jetzt `@Serializable`.
|
||||
- `build.gradle.kts`: Plugins ergänzt.
|
||||
|
||||
## 🏁 Fazit
|
||||
|
||||
Der ZNS-Import-Workflow (Upload -> JobId -> Polling) ist nun technisch stabil und die Typen sind über die Systemgrenzen
|
||||
hinweg serialisierbar.
|
||||
|
||||
---
|
||||
**🧹 [Curator]**: Dokumentation des Serialization-Fixes abgeschlossen.
|
||||
**👷 [Backend Developer]**: DTOs für API-Kommunikation stabilisiert.
|
||||
**🏗️ [Lead Architect]**: Konsistente Nutzung von Kotlinx-Serialization in KMP-Context sichergestellt.
|
||||
56
docs/99_Journal/2026-04-17_Session_Abschluss_Nacht.md
Normal file
56
docs/99_Journal/2026-04-17_Session_Abschluss_Nacht.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# 📓 Journal-Eintrag: 2026-04-17 - Session Abschluss (Nachtsession)
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Nach einem intensiven Abend haben wir die **ZNS-First Strategie** vollständig in den Veranstaltungs-Wizard integriert.
|
||||
Die technologischen Hürden (Build-Performance, Consul-Connectivity, Serialization) wurden erfolgreich aus dem Weg
|
||||
geräumt, sodass die fachliche Arbeit nun nahtlos fortgesetzt werden kann.
|
||||
|
||||
## 🚀 Wichtigste Errungenschaften
|
||||
|
||||
### 1. ZNS-Cloud-Sync & Hybrid-Import
|
||||
|
||||
- **Cloud-Anbindung**: Die Desktop-App kann nun per Klick (**"ZNS-Daten-Sync"**) Stammdaten direkt vom
|
||||
`masterdata-service` laden.
|
||||
- **Versions-Tracking**: Anzeige der geladenen Daten-Version (`ZNS-Daten geladen [Version ...]`) sorgt für Transparenz.
|
||||
- **Support für Einzeldateien**: Der Importer akzeptiert nun neben `.zip` auch direkt `.dat` Dateien (z.B.
|
||||
`VEREIN01.dat`).
|
||||
- **UX-Fortschritt**: Der Ladebalken im Frontend zeigt nun den echten Fortschritt des Backend-Imports an (Harmonisierung
|
||||
der DTOs).
|
||||
|
||||
### 2. Veranstalter-Verwaltung
|
||||
|
||||
- **Flexibilität**: Falls ein Verein nicht im ZNS-Datensatz vorhanden ist, kann er nun direkt über den Button **"+ Neuen
|
||||
Veranstalter anlegen"** manuell im System erfasst werden.
|
||||
- **Wizard-Integration**: Nahtloser Übergang von der Veranstalter-Wahl (Schritt 1) zu den Basisdaten (Schritt 2).
|
||||
|
||||
### 3. Infrastruktur-Härtung ("Port-Hardening")
|
||||
|
||||
- **Consul-Stabilität**: Alle 11 Backend-Services melden sich nun zuverlässig beim Consul `healthy`. Die Trennung von
|
||||
API-Ports (Ktor) und Health-Ports (Spring Actuator) wurde als Best-Practice umgesetzt.
|
||||
- **Gradle-Boost**: Durch das neue `enableWasm` Flag in `gradle.properties` konnten die Build-Zeiten massiv reduziert
|
||||
werden (Vermeidung unnötiger WASM-Kompilierung).
|
||||
- **Startup-Transparenz**: Alle Services loggen nun beim Start einheitlich Name, Ports und aktive Profile (
|
||||
`onApplicationReady`).
|
||||
|
||||
## 🛠️ Technische Details
|
||||
|
||||
- **Journal-Referenzen**:
|
||||
- `2026-04-16_Consul-Best-Practice-Fix.md`
|
||||
- `2026-04-16_ZNS-Serialization-Fix.md`
|
||||
- `2026-04-16_ZNS-Import-Polishing.md`
|
||||
- `2026-04-17_ZNS-Cloud-Sync-Integration.md`
|
||||
- **Toggles**: `enableWasm=false` in `gradle.properties` spart signifikante Ressourcen.
|
||||
|
||||
## 🏁 Fazit & Ausblick
|
||||
|
||||
Die Brücke zwischen Cloud-Stammdaten, lokalen Offline-Daten und manueller Erfassung steht. Morgen können wir uns darauf
|
||||
konzentrieren, die **Veranstaltungs-Basisdaten** (Schritt 2) und die **Ausschreibungs-Konfiguration** im Wizard weiter
|
||||
zu verfeinern.
|
||||
|
||||
Gute Nacht! 🌙
|
||||
|
||||
---
|
||||
**🧹 [Curator]**: Dokumentation abgeschlossen. Journal-Eintrag erstellt.
|
||||
**🏗️ [Lead Architect]**: Alle fachlichen Anforderungen an den ZNS-First Wizard umgesetzt.
|
||||
**👷 [Backend Developer]**: Alle Services laufen stabil im Docker-Verbund.
|
||||
49
docs/99_Journal/2026-04-17_ZNS-Cloud-Suche-Integration.md
Normal file
49
docs/99_Journal/2026-04-17_ZNS-Cloud-Suche-Integration.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Journal: 17. April 2026 - ZNS Cloud-Suche Integration
|
||||
|
||||
## 🏗️ Status Quo
|
||||
|
||||
Bisher konnten ZNS-Daten (Vereine/Veranstalter) im "Neue Veranstaltung" Wizard nur durch den manuellen Upload einer .zip
|
||||
oder .dat Datei importiert werden. Da die ZNS-Daten bereits im Backend (Masterdata-Service) vorhanden sind, war ein
|
||||
direkter Zugriff aus dem Wizard heraus wünschenswert, um den Workflow zu vereinfachen.
|
||||
|
||||
## ✅ Änderungen
|
||||
|
||||
### 1. Domain & Core (Frontend)
|
||||
|
||||
- **Datei:** `frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt`
|
||||
- **Erweiterung:** Das `ZnsImportProvider` Interface wurde um `searchRemote(query: String)` erweitert.
|
||||
- **Datenmodell:** `ZnsImportState` enthält nun `remoteResults: List<ZnsRemoteVerein>` und `isSearching: Boolean`.
|
||||
|
||||
### 2. Feature: ZNS-Import (Frontend)
|
||||
|
||||
- **Datei:** `frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt`
|
||||
- **Implementierung:** Die `searchRemote` Methode nutzt den `httpClient`, um den Endpunkt
|
||||
`/api/v1/masterdata/verein/search` des API-Gateways abzufragen.
|
||||
- **Serialisierung:** Ein internes `VereinRemoteDto` wurde hinzugefügt, um die Backend-Antwort korrekt zu mappen.
|
||||
|
||||
### 3. Shell: Desktop App (Frontend)
|
||||
|
||||
- **Datei:** `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt`
|
||||
- **UI-Integration:**
|
||||
- Im ersten Schritt des Veranstaltungs-Wizards wurde ein neues Suchfeld "ZNS Cloud-Suche" integriert.
|
||||
- Die Suchergebnisse werden in einer horizontalen `LazyRow` als Cards angezeigt.
|
||||
- Bei Klick auf ein Ergebnis wird der Verein automatisch in den lokalen `StoreV2` übernommen (falls noch nicht
|
||||
vorhanden) und als aktiver Veranstalter für den Wizard gesetzt.
|
||||
- **Imports:** Notwendige UI-Komponenten (`LazyRow`, `TextOverflow`, `Icons.Default.Cloud`) wurden ergänzt.
|
||||
|
||||
## 🚀 UX-Vorteil
|
||||
|
||||
Der User muss nun keine ZNS-Dateien mehr manuell verwalten, wenn die Daten bereits einmal zentral importiert wurden.
|
||||
Die "Cloud-Suche" fungiert als globale Stammdaten-Quelle, die nahtlos mit dem lokalen Offline-Store synchronisiert wird,
|
||||
sobald ein Eintrag ausgewählt wird.
|
||||
|
||||
## 🧹 Curator Hinweis
|
||||
|
||||
Die Session wurde mit der erfolgreichen Implementierung der hybriden Datenquelle (Lokal + Cloud) für den
|
||||
Veranstaltungs-Wizard abgeschlossen. Dies stärkt den "Offline-First" Ansatz bei gleichzeitiger Nutzung zentraler
|
||||
Cloud-Ressourcen.
|
||||
|
||||
**Nächste Schritte:**
|
||||
|
||||
- Erweiterung der Cloud-Suche auf Reiter und Pferde (für spätere Wizard-Schritte).
|
||||
- Performance-Optimierung (Debounce) für die Remote-Suche.
|
||||
45
docs/99_Journal/2026-04-17_ZNS-Cloud-Sync-Integration.md
Normal file
45
docs/99_Journal/2026-04-17_ZNS-Cloud-Sync-Integration.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Journal: ZNS-Cloud-Sync Integration
|
||||
|
||||
**Datum:** 17. April 2026
|
||||
**Badge:** 🏗️ [Lead Architect] & 🎨 [Frontend Expert] & 👷 [Backend Developer]
|
||||
|
||||
## 🎯 Zielsetzung
|
||||
|
||||
Verbesserung des Datenflusses im Veranstaltungs-Wizard durch eine explizite Synchronisations-Möglichkeit mit den
|
||||
Cloud-Stammdaten (Masterdata-Service). Dies ersetzt die bisherige manuelle Suche durch einen "Sync-Button"-Ansatz, der
|
||||
die Offline-First-Philosophie (lokale Datenhoheit mit Cloud-Backup) besser verdeutlicht.
|
||||
|
||||
## 🛠️ Änderungen
|
||||
|
||||
### 1. Domain-Modell (`frontend:core:domain`)
|
||||
|
||||
- `ZnsImportState` wurde um `lastSyncVersion` (String) und `isSyncing` (Boolean) erweitert.
|
||||
- `ZnsImportProvider` Interface erhielt die neue Methode `syncFromCloud(onResult: (List<ZnsRemoteVerein>) -> Unit)`. Die
|
||||
Nutzung eines Callbacks vermeidet zirkuläre Abhängigkeiten zum Desktop-Store innerhalb des Shared-Feature-Moduls.
|
||||
|
||||
### 2. Feature-Logik (`frontend:features:zns-import-feature`)
|
||||
|
||||
- Implementierung von `syncFromCloud` im `ZnsImportViewModel`.
|
||||
- Abruf von bis zu 1000 Vereinen aus dem `masterdata-service` via API-Gateway.
|
||||
- Generierung eines Zeitstempels für die `lastSyncVersion` bei erfolgreichem Abschluss.
|
||||
|
||||
### 3. UI-Anpassung (`frontend:shells:meldestelle-desktop`)
|
||||
|
||||
- Der Veranstaltungs-Wizard (Schritt 1) wurde umgestaltet:
|
||||
- Entfernung des Cloud-Suchfeldes.
|
||||
- Hinzufügen eines prominenten Buttons **"ZNS-Daten-Sync"** (Secondary Color).
|
||||
- Implementierung einer Status-Anzeige: **"ZNS-Daten geladen [Version dd.MM.yyyy HH:mm]"**.
|
||||
- Bei Klick auf den Sync-Button werden die empfangenen Daten automatisch in den lokalen `StoreV2` gemergt (
|
||||
Idempotenz-Check via OEPS-Nummer).
|
||||
|
||||
## 🧪 Verifizierung
|
||||
|
||||
- Code-Review der Schnittstellen und des Datenflusses.
|
||||
- Sicherstellung, dass der Sync-Status (Loading Spinner) korrekt im UI reflektiert wird.
|
||||
- Prüfung der zeitstempelbasierten Versionsanzeige.
|
||||
|
||||
## 💡 Ausblick
|
||||
|
||||
Der Sync-Mechanismus könnte in Zukunft auf ein differentielles Update (Delta-Sync) umgestellt werden, sobald das Backend
|
||||
entsprechende Header (`If-Modified-Since`) unterstützt. Aktuell werden pauschal die ersten 1000 Einträge geladen, was
|
||||
für die aktuelle Projektphase (Österreich-weit ~1400 Vereine) ausreichend performant ist.
|
||||
|
|
@ -10,11 +10,25 @@ data class ZnsImportState(
|
|||
val errors: List<String> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
val isFinished: Boolean = false,
|
||||
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
||||
val isSearching: Boolean = false,
|
||||
val lastSyncVersion: String? = null,
|
||||
val isSyncing: Boolean = false,
|
||||
)
|
||||
|
||||
data class ZnsRemoteVerein(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val oepsNummer: String,
|
||||
val ort: String?,
|
||||
val bundesland: String?,
|
||||
)
|
||||
|
||||
interface ZnsImportProvider {
|
||||
val state: ZnsImportState
|
||||
fun onFileSelected(path: String)
|
||||
fun startImport(mode: String = "FULL")
|
||||
fun searchRemote(query: String)
|
||||
fun syncFromCloud(onResult: (List<ZnsRemoteVerein>) -> Unit)
|
||||
fun reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ConnectivityTracker : KoinComponent {
|
|||
|
||||
private suspend fun checkConnection(): Boolean {
|
||||
return try {
|
||||
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/api/ping/health")
|
||||
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/actuator/health/readiness")
|
||||
response.status.value in 200..299
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ plugins {
|
|||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
||||
import at.mocode.frontend.core.network.NetworkConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
|
|
@ -22,7 +23,6 @@ import kotlinx.serialization.json.Json
|
|||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ImportStartResponse(val jobId: String)
|
||||
|
||||
|
|
@ -30,9 +30,18 @@ data class ImportStartResponse(val jobId: String)
|
|||
internal data class JobStatusResponse(
|
||||
val jobId: String,
|
||||
val status: String,
|
||||
val progress: Int = 0,
|
||||
val progressDetail: String = "",
|
||||
val errors: List<String> = emptyList(),
|
||||
val fortschritt: Int = 0,
|
||||
val meldungen: List<String> = emptyList(),
|
||||
val fehler: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class VereinRemoteDto(
|
||||
val vereinId: String,
|
||||
val vereinsNummer: String,
|
||||
val name: String,
|
||||
val ort: String? = null,
|
||||
val bundesland: String? = null,
|
||||
)
|
||||
|
||||
private val TERMINAL_STATES = setOf("ABGESCHLOSSEN", "FEHLER")
|
||||
|
|
@ -98,6 +107,74 @@ class ZnsImportViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
override fun searchRemote(query: String) {
|
||||
if (query.length < 3) {
|
||||
state = state.copy(remoteResults = emptyList())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSearching = true)
|
||||
try {
|
||||
val token = authTokenManager.authState.value.token
|
||||
// Wir nutzen den API-Gateway Pfad für masterdata
|
||||
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein/search") {
|
||||
parameter("q", query)
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val results = json.decodeFromString<List<VereinRemoteDto>>(response.bodyAsText())
|
||||
state = state.copy(
|
||||
isSearching = false,
|
||||
remoteResults = results.map {
|
||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
state = state.copy(isSearching = false, errorMessage = "Suche fehlgeschlagen: HTTP ${response.status.value}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isSearching = false, errorMessage = "Fehler bei der Cloud-Suche: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncFromCloud(onResult: (List<ZnsRemoteVerein>) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSyncing = true, errorMessage = null)
|
||||
try {
|
||||
val token = authTokenManager.authState.value.token
|
||||
// Wir laden die Top 1000 Vereine für den Sync (einfache Implementierung)
|
||||
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein") {
|
||||
parameter("limit", 1000)
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val results = json.decodeFromString<List<VereinRemoteDto>>(response.bodyAsText())
|
||||
val domainResults = results.map {
|
||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||
}
|
||||
|
||||
val now = java.time.LocalDateTime.now()
|
||||
val version = now.format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||
|
||||
state = state.copy(
|
||||
isSyncing = false,
|
||||
lastSyncVersion = version,
|
||||
isFinished = true
|
||||
)
|
||||
onResult(domainResults)
|
||||
} else {
|
||||
state = state.copy(isSyncing = false, errorMessage = "Sync fehlgeschlagen: HTTP ${response.status.value}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isSyncing = false, errorMessage = "Fehler beim Cloud-Sync: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPolling(jobId: String) {
|
||||
pollingJob?.cancel()
|
||||
pollingJob = viewModelScope.launch {
|
||||
|
|
@ -111,9 +188,9 @@ class ZnsImportViewModel(
|
|||
val status = json.decodeFromString<JobStatusResponse>(response.bodyAsText())
|
||||
state = state.copy(
|
||||
jobStatus = status.status,
|
||||
progress = status.progress,
|
||||
progressDetail = status.progressDetail,
|
||||
errors = status.errors.takeLast(MAX_VISIBLE_ERRORS),
|
||||
progress = status.fortschritt,
|
||||
progressDetail = status.meldungen.lastOrNull() ?: "",
|
||||
errors = status.fehler.takeLast(MAX_VISIBLE_ERRORS),
|
||||
isFinished = status.status in TERMINAL_STATES,
|
||||
)
|
||||
if (status.status in TERMINAL_STATES) break
|
||||
|
|
|
|||
|
|
@ -443,7 +443,56 @@ fun VeranstaltungKonfigV2(
|
|||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
// 2. Bestehende Veranstalter (Kompakt)
|
||||
// 2. Cloud Sync (Neu gemäß User-Wunsch)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
znsImporter.syncFromCloud { remoteList ->
|
||||
remoteList.forEach { remote ->
|
||||
StoreV2.vereine.find { it.oepsNummer == remote.oepsNummer }
|
||||
?: StoreV2.addVerein(remote.name, remote.oepsNummer, remote.ort ?: "")
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !znsState.isSyncing,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
|
||||
) {
|
||||
if (znsState.isSyncing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
color = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Synchronisiere...")
|
||||
} else {
|
||||
Icon(Icons.Default.CloudSync, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("ZNS-Daten-Sync")
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
"ZNS-Daten geladen",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"[Version ${znsState.lastSyncVersion ?: "Kein Sync"}]",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (znsState.lastSyncVersion != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
// 3. Bestehende Veranstalter (Kompakt)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) {
|
||||
var search by remember { mutableStateOf("") }
|
||||
val filteredVereine = remember(search) {
|
||||
|
|
@ -465,7 +514,7 @@ fun VeranstaltungKonfigV2(
|
|||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(filteredVereine) { verein ->
|
||||
|
|
@ -499,6 +548,18 @@ fun VeranstaltungKonfigV2(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Manueller Button für neuen Veranstalter
|
||||
OutlinedButton(
|
||||
onClick = { showVereinNeu = true },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
contentPadding = PaddingValues(12.dp),
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("+ Neuen Veranstalter anlegen", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
if (showVereinNeu) {
|
||||
|
|
@ -511,7 +572,8 @@ fun VeranstaltungKonfigV2(
|
|||
onCancel = { showVereinNeu = false },
|
||||
onVereinCreated = { newId ->
|
||||
showVereinNeu = false
|
||||
onVeranstalterCreated(newId)
|
||||
selectedVereinId = newId
|
||||
currentStep = 2 // Direkt zum nächsten Schritt
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,63 +9,71 @@ plugins {
|
|||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
val isWasmEnabled = findProperty("enableWasm")?.toString()?.toBoolean() ?: false
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
if (isWasmEnabled) {
|
||||
js(IR) {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
wasmJs {
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
binaries.executable()
|
||||
}
|
||||
binaries.executable()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
wasmJsMain.dependencies {
|
||||
// Core-Module
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.auth)
|
||||
commonMain.dependencies {}
|
||||
|
||||
// Feature-Module (die öffentlich sein dürfen)
|
||||
implementation(projects.frontend.features.veranstaltungFeature)
|
||||
implementation(projects.frontend.features.turnierFeature)
|
||||
implementation(projects.frontend.features.nennungFeature)
|
||||
implementation(projects.frontend.features.billingFeature)
|
||||
if (isWasmEnabled) {
|
||||
wasmJsMain.dependencies {
|
||||
// Core-Module
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.auth)
|
||||
|
||||
// Compose Multiplatform
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(libs.compose.materialIconsExtended)
|
||||
// Feature-Module (die öffentlich sein dürfen)
|
||||
implementation(projects.frontend.features.veranstaltungFeature)
|
||||
implementation(projects.frontend.features.turnierFeature)
|
||||
implementation(projects.frontend.features.nennungFeature)
|
||||
implementation(projects.frontend.features.billingFeature)
|
||||
|
||||
// DI (Koin)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
// Compose Multiplatform
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(libs.compose.materialIconsExtended)
|
||||
|
||||
// Bundles
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
}
|
||||
// DI (Koin)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
|
||||
wasmJsTest.dependencies {
|
||||
// Core-Module
|
||||
implementation(projects.frontend.core.domain)
|
||||
// Bundles
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
}
|
||||
|
||||
wasmJsTest.dependencies {
|
||||
// Core-Module
|
||||
implementation(projects.frontend.core.domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ org.gradle.java.installations.auto-detect=true
|
|||
dev.port.offset=0
|
||||
# Set dev.port.offset=100 for second developer
|
||||
# Set dev.port.offset=200 for the third developer
|
||||
# ------------------------------------------------------------------
|
||||
# Wasm/JS Feature Toggle
|
||||
# ------------------------------------------------------------------
|
||||
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
|
||||
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
|
||||
enableWasm=false
|
||||
|
||||
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# Journal-Eintrag: Consul Discovery Best Practice Fix
|
||||
|
||||
**Datum:** 16. April 2026
|
||||
**Beteiligte:** 🏗️ [Lead Architect], 👷 [Backend Developer]
|
||||
|
||||
## 1. Problemstellung
|
||||
|
||||
Trotz vorheriger Versuche meldete Consul für den `masterdata-service` weiterhin den Status "critical" (404 auf
|
||||
`/actuator/health`).
|
||||
Die Ursache lag in der hybriden Architektur (Spring Boot + Ktor):
|
||||
|
||||
- Spring Boot (Management/Actuator) lief auf Port 8086.
|
||||
- Ktor (API) lief auf Port 8091.
|
||||
- Consul versuchte fälschlicherweise, den Healthcheck auf dem API-Port (oder einer fehlerhaften Kombination)
|
||||
auszuführen, da die Service-Registrierung nicht präzise genug war.
|
||||
|
||||
## 2. Best Practice Lösung
|
||||
|
||||
Um die Stabilität und Wartbarkeit zu erhöhen, wurden folgende Maßnahmen ergriffen:
|
||||
|
||||
### A. Präzise Service-Registrierung (Masterdata-Service)
|
||||
|
||||
In der `application.yml` wurde die Consul-Discovery-Konfiguration geschärft:
|
||||
|
||||
- `health-check-port: 8086`: Der Healthcheck wird explizit an den Spring Boot Management Port gesendet.
|
||||
- `port: ${masterdata.http.port:8091}`: Der Service wird explizit mit seinem API-Port (Ktor) im Service-Katalog
|
||||
registriert.
|
||||
- Damit weiß das Gateway: "Sende Traffic an 8091", und Consul weiß: "Prüfe Health auf 8086".
|
||||
|
||||
### B. Umstellung auf Load-Balancer Abstraktion (`lb://`)
|
||||
|
||||
Das API-Gateway nutzte bisher hartcodierte URLs oder Umgebungsvariablen für das Routing. Dies ist fehleranfällig und
|
||||
widerspricht dem Prinzip der Service Discovery.
|
||||
|
||||
- **Änderung:** Alle Routen in `GatewayConfig.kt` wurden von `http://service-name:port` auf `lb://service-name`
|
||||
umgestellt.
|
||||
- **Vorteil:** Spring Cloud Gateway nutzt nun den Consul-Katalog direkt. Wenn ein Service (wie `masterdata-service`)
|
||||
dort mit Port 8091 registriert ist, wird er automatisch korrekt angesteuert.
|
||||
- Dies entfernt die Notwendigkeit für `*_SERVICE_URL` Umgebungsvariablen im Gateway.
|
||||
|
||||
## 3. Ergebnis
|
||||
|
||||
- Der `masterdata-service` wird nun korrekt als `healthy` markiert, da der Healthcheck den richtigen Port (8086)
|
||||
erreicht.
|
||||
- Das Routing ist nun dynamisch und robust gegenüber Port-Änderungen in den einzelnen Services.
|
||||
- Alle Backend-Services folgen nun einem einheitlichen "Best Practice" Muster für die Service Discovery.
|
||||
|
||||
---
|
||||
**🏗️ [Lead Architect]**: Architektur auf Standard Spring Cloud Discovery (lb://) gehärtet.
|
||||
**👷 [Backend Developer]**: Hybride Port-Konfiguration im Masterdata-Service final korrigiert.
|
||||
Loading…
Reference in New Issue
Block a user