### feat: erweitere ZNS und SQLDelight-Integration
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled

- **SQLDelight:** Füge neue Queries (`countVereine`, `maxUpdated...`) zur SQLite-Datenbank hinzu und aktualisiere `DesktopMasterdataRepository`.
- **ZNS-Sync:** Passe `ZnsImportState` an, um Pferde- und Funktionärsdaten zu unterstützen.
- **Cloud-Sync:** Entferne redundante Auth-Header und setze Limits für Massensynchronisation auf 50.000 Datensätze.
- **Masterdata-Service:** Stabilisiere Consul Health-Checks und implementiere Limit-Beschränkungen auf Controller-Ebene.
This commit is contained in:
2026-04-22 14:14:33 +02:00
parent 98c241fc64
commit beb20e0cf7
16 changed files with 287 additions and 99 deletions
@@ -1,18 +1,24 @@
package at.mocode.frontend.features.funktionaer.data
import at.mocode.frontend.core.network.ApiRoutes
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class KtorFunktionaerRepository(private val client: HttpClient) : FunktionaerRepository {
override fun getFunktionaere(): Flow<List<Funktionaer>> = flow {
try {
val response: List<Funktionaer> = client.get("/api/v1/masterdata/funktionaere").body()
emit(response)
val response = client.get(ApiRoutes.Masterdata.FUNKTIONAERE)
if (response.status.isSuccess()) {
emit(response.body())
} else {
emit(emptyList())
}
} catch (_: Exception) {
emit(emptyList())
}
@@ -20,9 +26,10 @@ class KtorFunktionaerRepository(private val client: HttpClient) : FunktionaerRep
override suspend fun searchFunktionaere(query: String): List<Funktionaer> {
return try {
client.get("/api/v1/masterdata/funktionaere/search") {
val response = client.get("${ApiRoutes.Masterdata.FUNKTIONAERE}/search") {
parameter("q", query)
}.body()
}
if (response.status.isSuccess()) response.body() else emptyList()
} catch (_: Exception) {
emptyList()
}
@@ -30,14 +37,16 @@ class KtorFunktionaerRepository(private val client: HttpClient) : FunktionaerRep
override suspend fun getFunktionaerById(id: Long): Funktionaer? {
return try {
client.get("/api/v1/masterdata/funktionaere/$id").body()
val response = client.get("${ApiRoutes.Masterdata.FUNKTIONAERE}/$id")
if (response.status.isSuccess()) response.body() else null
} catch (_: Exception) {
null
}
}
override suspend fun saveFunktionaer(funktionaer: Funktionaer) {
client.post("/api/v1/masterdata/funktionaere") {
client.post(ApiRoutes.Masterdata.FUNKTIONAERE) {
contentType(ContentType.Application.Json)
setBody(funktionaer)
}
}
@@ -1,18 +1,24 @@
package at.mocode.frontend.features.pferde.data
import at.mocode.frontend.core.network.ApiRoutes
import at.mocode.frontend.features.pferde.domain.Pferd
import at.mocode.frontend.features.pferde.domain.PferdRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class KtorPferdRepository(private val client: HttpClient) : PferdRepository {
override fun getPferde(): Flow<List<Pferd>> = flow {
try {
val response: List<Pferd> = client.get("/api/v1/masterdata/pferde").body()
emit(response)
val response = client.get(ApiRoutes.Masterdata.PFERDE)
if (response.status.isSuccess()) {
emit(response.body())
} else {
emit(emptyList())
}
} catch (_: Exception) {
emit(emptyList())
}
@@ -20,9 +26,10 @@ class KtorPferdRepository(private val client: HttpClient) : PferdRepository {
override suspend fun searchPferde(query: String): List<Pferd> {
return try {
client.get("/api/v1/masterdata/pferde/search") {
val response = client.get("${ApiRoutes.Masterdata.PFERDE}/search") {
parameter("q", query)
}.body()
}
if (response.status.isSuccess()) response.body() else emptyList()
} catch (_: Exception) {
emptyList()
}
@@ -30,14 +37,16 @@ class KtorPferdRepository(private val client: HttpClient) : PferdRepository {
override suspend fun getPferdById(id: String): Pferd? {
return try {
client.get("/api/v1/masterdata/pferde/$id").body()
val response = client.get("${ApiRoutes.Masterdata.PFERDE}/$id")
if (response.status.isSuccess()) response.body() else null
} catch (_: Exception) {
null
}
}
override suspend fun savePferd(pferd: Pferd) {
client.post("/api/v1/masterdata/pferde") {
client.post(ApiRoutes.Masterdata.PFERDE) {
contentType(ContentType.Application.Json)
setBody(pferd)
}
}
@@ -59,7 +59,7 @@ fun ReiterScreen(
Spacer(Modifier.height(16.dp))
ReiterCard(
reiter = uiState.selectedReiter,
onEdit = { viewModel.selectReiter(uiState.selectedReiter!!) }
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
)
}
} else {
@@ -8,7 +8,7 @@ import androidx.lifecycle.viewModelScope
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
import at.mocode.frontend.core.domain.repository.MasterdataRepository
import at.mocode.frontend.core.domain.zns.*
import at.mocode.frontend.core.network.NetworkConfig
import at.mocode.frontend.core.network.ApiRoutes
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
@@ -98,10 +98,8 @@ class ZnsImportViewModel(
)
try {
println("[ZNS] Starte Import Mode=$mode Datei=$fileName")
val token = authTokenManager.authState.value.token
val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") {
val response: HttpResponse = httpClient.post("/api/v1/import/zns") {
parameter("mode", mode)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
val contentType =
if (fileName.endsWith(".zip", ignoreCase = true)) "application/zip" else "application/octet-stream"
setBody(MultiPartFormDataContent(formData {
@@ -140,20 +138,18 @@ class ZnsImportViewModel(
viewModelScope.launch {
state = state.copy(isSearching = true)
try {
val token = authTokenManager.authState.value.token
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein/search") {
val response: HttpResponse = httpClient.get(ApiRoutes.Masterdata.VEREINE + "/search") {
parameter("q", query)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
}
if (response.status.isSuccess()) {
val responseText = response.bodyAsText()
println("[ZNS] Search Response: $responseText")
val results = json.decodeFromString<List<ReiterRemoteDto>>(responseText)
val results = json.decodeFromString<List<VereinRemoteDto>>(responseText)
state = state.copy(
isSearching = false,
remoteReiterResults = results.map {
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
remoteResults = results.map {
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
}
)
} else {
@@ -174,64 +170,79 @@ class ZnsImportViewModel(
viewModelScope.launch {
state = state.copy(isSyncing = true, errorMessage = null)
try {
val token = authTokenManager.authState.value.token
// 1. Vereine
val vResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein") {
parameter("limit", 1000)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
// 1. Vereine (Erhöhtes Limit für Initial-Sync)
val vResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.VEREINE) {
parameter("limit", 50000)
}
val vResults = if (vResponse.status.isSuccess()) {
json.decodeFromString<List<VereinRemoteDto>>(vResponse.bodyAsText()).map {
val text = vResponse.bodyAsText()
println("[ZNS] Sync Vereine: Received ${text.length} chars")
json.decodeFromString<List<VereinRemoteDto>>(text).map {
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
}
} else emptyList()
} else {
println("[ZNS] Sync Vereine failed: ${vResponse.status}")
emptyList()
}
// 2. Reiter
val rResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/reiter") {
parameter("limit", 1000)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
val rResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.REITER) {
parameter("limit", 50000)
}
val rResults = if (rResponse.status.isSuccess()) {
json.decodeFromString<List<ReiterRemoteDto>>(rResponse.bodyAsText()).map {
val text = rResponse.bodyAsText()
println("[ZNS] Sync Reiter: Received ${text.length} chars")
json.decodeFromString<List<ReiterRemoteDto>>(text).map {
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
}
} else emptyList()
} else {
println("[ZNS] Sync Reiter failed: ${rResponse.status}")
emptyList()
}
// 3. Pferde
val pResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.PFERDE) {
parameter("limit", 50000)
}
val pResults = if (pResponse.status.isSuccess()) {
val text = pResponse.bodyAsText()
println("[ZNS] Sync Pferde: Received ${text.length} chars")
json.decodeFromString<List<HorseRemoteDto>>(text).map {
ZnsRemotePferd(it.pferdId, it.kopfnummer, it.pferdeName, it.lebensnummer, it.geschlecht)
}
} else {
println("[ZNS] Sync Pferde failed: ${pResponse.status}")
emptyList()
}
// 4. Funktionäre
val fResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.FUNKTIONAERE) {
parameter("limit", 1000)
}
val fResults = if (fResponse.status.isSuccess()) {
val text = fResponse.bodyAsText()
println("[ZNS] Sync Funktionäre: Received ${text.length} chars")
json.decodeFromString<List<FunktionaerRemoteDto>>(text).map {
ZnsRemoteFunktionaer(it.funktionaerId, it.satzId, it.satzNummer, it.name, it.qualifikationen)
}
} else {
println("[ZNS] Sync Funktionäre failed: ${fResponse.status}")
emptyList()
}
state = state.copy(
remoteResults = vResults,
remoteReiterResults = rResults,
isSyncing = false,
isFinished = true
)
val pResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/horse") {
parameter("limit", 1000)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
}
val pResults = if (pResponse.status.isSuccess()) {
json.decodeFromString<List<HorseRemoteDto>>(pResponse.bodyAsText()).map {
ZnsRemotePferd(it.pferdId, it.kopfnummer, it.pferdeName, it.lebensnummer, it.geschlecht)
}
} else emptyList()
// 4. Funktionäre
val fResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/funktionaer") {
parameter("limit", 1000)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
}
val fResults = if (fResponse.status.isSuccess()) {
json.decodeFromString<List<FunktionaerRemoteDto>>(fResponse.bodyAsText()).map {
ZnsRemoteFunktionaer(it.funktionaerId, it.satzId, it.satzNummer, it.name, it.qualifikationen)
}
} else emptyList()
state = state.copy(
remoteHorseResults = pResults,
remoteFunktionaerResults = fResults,
isSyncing = false,
isFinished = true
)
onResult(vResults, rResults, pResults, fResults)
} catch (e: Exception) {
println("[ZNS] Sync Error: ${e.message}")
e.printStackTrace()
state = state.copy(isSyncing = false, errorMessage = "Fehler beim Cloud-Sync: ${e.message}")
}
}
@@ -242,10 +253,7 @@ class ZnsImportViewModel(
pollingJob = viewModelScope.launch {
while (true) {
try {
val token = authTokenManager.authState.value.token
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/import/zns/$jobId/status") {
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
}
val response: HttpResponse = httpClient.get("/api/v1/import/zns/$jobId/status")
if (response.status.isSuccess()) {
val status = json.decodeFromString<JobStatusResponse>(response.bodyAsText())
state = state.copy(