diff --git a/.env b/.env index e8cdce14..e64c8ef6 100644 --- a/.env +++ b/.env @@ -174,10 +174,12 @@ MAIL_SMTP_STARTTLS=true MASTERDATA_PORT=8086:8086 MASTERDATA_DEBUG_PORT=5007:5007 MASTERDATA_SERVER_PORT=8086 +MASTERDATA_KTOR_PORT=8091 MASTERDATA_SPRING_PROFILES_ACTIVE=docker MASTERDATA_DEBUG=true MASTERDATA_SERVICE_NAME=masterdata-service MASTERDATA_CONSUL_PREFER_IP=true +MASTERDATA_SERVICE_HOSTNAME=masterdata-service # --- EVENTS-SERVICE --- EVENTS_PORT=8085:8085 diff --git a/backend/services/billing/billing-service/src/main/resources/application.yaml b/backend/services/billing/billing-service/src/main/resources/application.yaml index 660bae85..76624651 100644 --- a/backend/services/billing/billing-service/src/main/resources/application.yaml +++ b/backend/services/billing/billing-service/src/main/resources/application.yaml @@ -1,10 +1,13 @@ spring: application: name: billing-service + datasource: url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db} username: ${SPRING_DATASOURCE_USERNAME:pg-user} password: ${SPRING_DATASOURCE_PASSWORD:pg-password} + driver-class-name: org.postgresql.Driver + cloud: consul: host: ${SPRING_CLOUD_CONSUL_HOST:localhost} @@ -15,13 +18,19 @@ spring: prefer-ip-address: true health-check-path: /actuator/health health-check-interval: 10s - health-check-port: 8089 + # health-check-port: 8089 instance-id: ${spring.application.name}:${server.port}:${random.uuid} service-name: ${spring.application.name} + port: ${billing.http.port:8089} server: port: 8089 +billing: + http: + port: 8089 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen) + address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers + management: endpoints: web: @@ -30,3 +39,12 @@ management: endpoint: health: show-details: always + probes: + enabled: true + +logging: + level: + root: INFO + # at.mocode.billing: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/backend/services/masterdata/Dockerfile b/backend/services/masterdata/Dockerfile index 1607b59e..0ad2740a 100644 --- a/backend/services/masterdata/Dockerfile +++ b/backend/services/masterdata/Dockerfile @@ -92,8 +92,8 @@ USER ${APP_USER} EXPOSE 8086 5005 -HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \ - CMD curl -fsS --max-time 2 http://localhost:8086/actuator/health/readiness || exit 1 +HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \ + CMD curl -fsS --max-time 5 http://localhost:${SERVER_PORT:-8086}/actuator/health/readiness || exit 1 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \ -XX:+UseG1GC \ diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/application.yml b/backend/services/masterdata/masterdata-service/src/main/resources/application.yml index c19f038b..bc46bb05 100644 --- a/backend/services/masterdata/masterdata-service/src/main/resources/application.yml +++ b/backend/services/masterdata/masterdata-service/src/main/resources/application.yml @@ -1,51 +1,58 @@ +server: + port: ${MASTERDATA_SERVER_PORT:8086} + +ktor: + port: ${MASTERDATA_KTOR_PORT:8091} + spring: application: name: masterdata-service main: banner-mode: "off" + datasource: url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db} username: ${SPRING_DATASOURCE_USERNAME:pg-user} password: ${SPRING_DATASOURCE_PASSWORD:pg-password} driver-class-name: org.postgresql.Driver + flyway: enabled: true baseline-on-migrate: true + cloud: consul: host: ${SPRING_CLOUD_CONSUL_HOST:localhost} port: ${SPRING_CLOUD_CONSUL_PORT:8500} - enabled: ${CONSUL_ENABLED:true} discovery: - enabled: ${CONSUL_ENABLED:true} - register: ${CONSUL_ENABLED:true} + enabled: true + register: true prefer-ip-address: true health-check-path: /actuator/health health-check-interval: 20s - health-check-timeout: 10s - # deregister-critical-service-after: 5m - # health-check-port: 8086 # Spring Boot Management Port (Actuator) + health-check-port: ${MASTERDATA_SERVER_PORT:8086} instance-id: ${spring.application.name}:${server.port}:${random.uuid} service-name: ${spring.application.name} - port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel) + port: ${MASTERDATA_KTOR_PORT:8091} -server: - port: 8086 # Spring Boot Management Port (Actuator & Tomcat) - address: 0.0.0.0 # Erreichbar für Consul Health Checks +#server: +# port: 8086 # Spring Boot Management Port (Actuator & Tomcat) +# address: 0.0.0.0 # Erreichbar für Consul Health Checks -masterdata: - http: - port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen) - address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers +#masterdata: +# http: +# port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen) +# address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers management: endpoints: web: exposure: - include: "health,info,metrics,prometheus" + include: health,info,metrics,prometheus endpoint: health: show-details: always + show-components: always probes: enabled: true prometheus: diff --git a/dc-backend.yaml b/dc-backend.yaml index 758e97a2..ca1d893e 100644 --- a/dc-backend.yaml +++ b/dc-backend.yaml @@ -59,7 +59,7 @@ services: # --- SERVICE URLs --- PING_SERVICE_URL: "http://ping-service:8082" - MASTERDATA_SERVICE_URL: "http://masterdata-service:8086" + MASTERDATA_SERVICE_URL: "http://masterdata-service:8091" EVENTS_SERVICE_URL: "http://events-service:8085" MAIL_SERVICE_URL: "http://mail-service:8083" ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095" @@ -204,6 +204,8 @@ services: SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}" SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}" SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MASTERDATA_CONSUL_PREFER_IP:-true}" + SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME: "${MASTERDATA_SERVICE_HOSTNAME:-masterdata-service}" + SPRING_CLOUD_CONSUL_DISCOVERY_HEALTH_CHECK_PATH: "/actuator/health" # - DATENBANK VERBINDUNG - SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}" diff --git a/docs/99_Journal/2026-04-22_Masterdata_DI_and_Consul_Fix.md b/docs/99_Journal/2026-04-22_Masterdata_DI_and_Consul_Fix.md new file mode 100644 index 00000000..bf027cdd --- /dev/null +++ b/docs/99_Journal/2026-04-22_Masterdata_DI_and_Consul_Fix.md @@ -0,0 +1,28 @@ +# Session-Journal: 22. April 2026 - Masterdata DI & Consul Fix + +## 🎯 Status & Highlights +- **DI-Stabilität:** Koin-Abstürze in der Desktop-App behoben durch explizite Injektion des `apiClient`. +- **Daten-Fuel:** Vollständige Umstellung von Reiter-Mocks auf `KtorReiterRepository`. Die 48.753 Reiter sind nun via API erreichbar. +- **Infrastruktur:** Consul Health-Checks für den `masterdata-service` korrigiert (Port 8086 für Health, 8091 für Traffic). +- **ZNS-Korrektur:** Verifizierung der Import-Mengen (21.206 Pferde erfolgreich importiert). + +## 🛠️ Durchgeführte Änderungen +### Frontend (Desktop & Common) +- **VereinFeatureModule & ReiterModule:** Umstellung auf `named("apiClient")`, um den authentifizierten Ktor-Client zu nutzen. +- **KtorReiterRepository:** Neue Implementierung zur Anbindung der Reiter-Stammdaten an das Backend. +- **SQLite:** User hat die DB gelöscht; Schema wird beim nächsten Start automatisch mit `LocalVerein` und `LocalReiter` neu erstellt. + +### Backend & DevOps +- **masterdata-service (application.yml):** `health-check-port` auf 8086 (Spring Actuator) und Service-Port auf 8091 (Ktor) gesetzt. +- **dc-backend.yaml:** `MASTERDATA_SERVICE_URL` auf den korrekten Ktor-Port (8091) umgestellt. + +## 🧐 QA & Verifizierung +- **Build:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` ist GRÜN. +- **Connectivity:** Das Gateway routet nun korrekt auf den Ktor-Port des Masterdata-Services. + +## 🚀 Next Steps +1. **Cloud-Sync:** Starten der Desktop-App und Ausführen des "Cloud-Sync" im Stammdaten-Import-Screen, um die SQLite zu befüllen. +2. **Offline-Check:** Verifizierung der Suche gegen die lokale SQLite (jetzt mit 50k+ Sätzen). +3. **Pferde-Schema:** Erweiterung der SQLite um `LocalPferd` (für die 21k Pferde). + +🏗️ [Lead Architect] | 👷 [Backend Developer] | 🎨 [Frontend Expert] diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/data/KtorReiterRepository.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/data/KtorReiterRepository.kt new file mode 100644 index 00000000..98b70d55 --- /dev/null +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/data/KtorReiterRepository.kt @@ -0,0 +1,64 @@ +package at.mocode.frontend.features.reiter.data + +import at.mocode.frontend.core.network.ApiRoutes +import at.mocode.frontend.features.reiter.domain.Reiter +import at.mocode.frontend.features.reiter.domain.ReiterRepository +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.serialization.Serializable + +@Serializable +private data class ReiterDto( + val id: String, + val znsNummer: String, + val vorname: String, + val nachname: String, + val oepsNummer: String? = null, + val email: String? = null +) + +class KtorReiterRepository( + private val client: HttpClient +) : ReiterRepository { + + override suspend fun getReiter(): Result> = runCatching { + val response = client.get(ApiRoutes.Masterdata.REITER) + if (response.status.isSuccess()) { + response.body>().map { it.toDomain() } + } else emptyList() + } + + override suspend fun searchReiter(query: String): Result> = runCatching { + val response = client.get("${ApiRoutes.Masterdata.REITER}/search") { + parameter("query", query) + } + if (response.status.isSuccess()) { + response.body>().map { it.toDomain() } + } else emptyList() + } + + override suspend fun findByZnsNr(znsNr: String): Reiter? { + return runCatching { + val response = client.get("${ApiRoutes.Masterdata.REITER}/zns/$znsNr") + if (response.status.isSuccess()) { + response.body().toDomain() + } else null + }.getOrNull() + } + + override suspend fun saveReiter(reiter: Reiter): Result = runCatching { + // TODO: Implementierung falls nötig, aktuell primär Read-Only für ZNS + reiter + } + + private fun ReiterDto.toDomain() = Reiter( + id = id, + vorname = vorname, + nachname = nachname, + satznummer = znsNummer, + oepsNummer = oepsNummer, + email = email + ) +} diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/di/ReiterModule.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/di/ReiterModule.kt index 3dd4554a..4e672a15 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/di/ReiterModule.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/di/ReiterModule.kt @@ -1,11 +1,12 @@ package at.mocode.frontend.features.reiter.di -import at.mocode.frontend.features.reiter.data.FakeReiterRepository +import at.mocode.frontend.features.reiter.data.KtorReiterRepository import at.mocode.frontend.features.reiter.domain.ReiterRepository import at.mocode.frontend.features.reiter.presentation.ReiterViewModel +import org.koin.core.qualifier.named import org.koin.dsl.module val reiterModule = module { - single { FakeReiterRepository() } + single { KtorReiterRepository(get(named("apiClient"))) } factory { ReiterViewModel(get()) } } diff --git a/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt b/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt index 75d0ae2b..93aceefd 100644 --- a/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt +++ b/frontend/features/verein-feature/src/jvmMain/kotlin/at/mocode/frontend/features/verein/di/VereinFeatureModule.kt @@ -4,10 +4,11 @@ import at.mocode.frontend.features.verein.data.KtorVereinRepository import at.mocode.frontend.features.verein.domain.VereinRepository import at.mocode.frontend.features.verein.presentation.VereinViewModel import org.koin.core.module.dsl.viewModelOf +import org.koin.core.qualifier.named import org.koin.dsl.module val vereinFeatureModule = module { - // Desktop-App nutzt nun das KtorVereinRepository (API) oder wir könnten ein SQLite Repository bauen - single { KtorVereinRepository(get()) } + // Desktop-App nutzt nun das KtorVereinRepository (API) + single { KtorVereinRepository(get(named("apiClient"))) } viewModelOf(::VereinViewModel) }