feat(desktop, masterdata): ZNS-Sync-Status in Footer hinzugefügt & Consul-Healthcheck stabilisiert

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-17 09:54:12 +02:00
parent 4b6a242372
commit 0128f98164
4 changed files with 115 additions and 6 deletions

View File

@ -21,8 +21,10 @@ spring:
register: ${CONSUL_ENABLED:true} register: ${CONSUL_ENABLED:true}
prefer-ip-address: true prefer-ip-address: true
health-check-path: /actuator/health health-check-path: /actuator/health
health-check-interval: 10s health-check-interval: 20s
health-check-port: 8086 # Spring Boot Management Port (Actuator) health-check-timeout: 10s
# deregister-critical-service-after: 5m
# health-check-port: 8086 # Spring Boot Management Port (Actuator)
instance-id: ${spring.application.name}:${server.port}:${random.uuid} instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name} service-name: ${spring.application.name}
port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel) port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel)

View File

@ -0,0 +1,61 @@
# Session Journal: 2026-04-17 - Vormittag
## 🎯 Ziele der Session
1. **Technischer Blocker:** Stabilisierung des Consul-Health-Checks für den `masterdata-service`.
2. **ÖTO-Konformität:** Implementierung von Guardrails für Turnier-Zeitspannen (1-2 Tage für C-Turniere) im
Desktop-Wizard.
3. **OEPS-Validierung:** Sicherstellung korrekter Vereinsnummern (B-NNN) bei manueller Erfassung.
4. **UX-Polishing:** Integration des ZNS-Synchronisationsstatus in die Footer-Bar der Desktop-App.
## 🛠️ Durchgeführte Änderungen
### 🔧 1. Backend: Consul & Master-Data (Infrastruktur)
* **Datei:** `backend/services/masterdata/masterdata-service/src/main/resources/application.yml`
* **Änderung:**
* `health-check-interval` von 10s auf 20s erhöht.
* `health-check-timeout` auf 10s gesetzt.
* `deregister-critical-service-after` auf 5m gesetzt.
* **Grund:** Vermeidung von "Ghost-Failures" im Consul-Dashboard, wenn die Datenbank-Verbindung während des Ktor-Starts
noch nicht vollständig bereit ist.
* **Update:** Problematische Properties `deregister-critical-service-after` und `health-check-port` vorerst
auskommentiert, da diese in der aktuellen Konfiguration zu Auflösungsfehlern führten.
### 📜 2. Frontend: ÖTO-Guardrails (Wizard Schritt 2)
* **Datei:** `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt`
* **Änderung:**
* Logik zur Berechnung der Turniertage (`ChronoUnit.DAYS`) eingebaut.
* Einblendung eines Info-Badges: *"Hinweis: Gemäß ÖTO sind C-Turniere auf 2 Tage begrenzt."*, falls die Zeitspanne > 2
Tage ist.
* **Korrektur:** Typ-Mismatch bei `Icon` behoben (Parameter `color` zu `tint` geändert).
* Kein harter Block, sondern eine fachliche Hilfestellung (Guardrails).
### 🏷️ 3. Frontend: OEPS-Nummer Validierung
* **Datei:** `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt`
* **Änderung:**
* Regex-Validierung (`^[1-9]-[0-9]{3}$`) für die manuelle OEPS-Nummern-Eingabe hinzugefügt.
* Fehlermeldung bei falschem Format (z.B. "4-001" erforderlich).
* **Grund:** Sicherstellung der Datenqualität für den späteren OEPS-Ergebnisexport (A/B/C-Sätze).
### 🎨 4. UX: Status-Bar & ZNS-Badge
* **Datei:**
`frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt`
* **Änderung:**
* `ZnsImportProvider` in die `DesktopFooterBar` injiziert.
* Neues Status-Icon `Dataset` (ZNS) hinzugefügt.
* Anzeige der letzten Sync-Version (z.B. `ZNS: V12` oder `ZNS: Kein Sync`).
* Farbliche Kennzeichnung (Grün/Gelb/Rot) je nach Synchronisationsstand.
## ✅ Ergebnis & Status
* Das Consul-Dashboard sollte nun einen stabilen "Grün"-Status für den `masterdata-service` anzeigen.
* Der Desktop-Wizard leitet den User fachlich korrekt durch die Turnier-Anlage.
* Der User hat jederzeit volle Transparenz über den Stand seiner lokalen ZNS-Daten.
---
**🏗️ [Lead Architect]** & **🧹 [Curator]**
Datum: 17. April 2026 | Status: Abgeschlossen

View File

@ -20,6 +20,7 @@ import at.mocode.desktop.screens.onboarding.OnboardingSettings
import at.mocode.desktop.screens.onboarding.SettingsManager import at.mocode.desktop.screens.onboarding.SettingsManager
import at.mocode.frontend.core.designsystem.theme.AppColors import at.mocode.frontend.core.designsystem.theme.AppColors
import at.mocode.frontend.core.designsystem.theme.Dimens import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.navigation.AppScreen import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.core.network.ConnectivityTracker import at.mocode.frontend.core.network.ConnectivityTracker
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
@ -857,8 +858,10 @@ private fun DesktopContentArea(
private fun DesktopFooterBar(settings: OnboardingSettings) { private fun DesktopFooterBar(settings: OnboardingSettings) {
val connectivityTracker = koinInject<ConnectivityTracker>() val connectivityTracker = koinInject<ConnectivityTracker>()
val discoveryService = koinInject<NetworkDiscoveryService>() val discoveryService = koinInject<NetworkDiscoveryService>()
val znsImporter = koinInject<ZnsImportProvider>()
val online by connectivityTracker.isOnline.collectAsState() val online by connectivityTracker.isOnline.collectAsState()
val znsState = znsImporter.state
val discoveredServices = remember { mutableStateOf(discoveryService.getDiscoveredServices()) } val discoveredServices = remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
val deviceName = settings.geraetName.ifBlank { "Unbekannt" } val deviceName = settings.geraetName.ifBlank { "Unbekannt" }
@ -901,6 +904,17 @@ private fun DesktopFooterBar(settings: OnboardingSettings) {
label = if (deviceCount > 0) "Verbunden: $deviceName ($deviceCount im Netz)" else "Lokal: $deviceName", label = if (deviceCount > 0) "Verbunden: $deviceName ($deviceCount im Netz)" else "Lokal: $deviceName",
color = if (deviceCount > 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline color = if (deviceCount > 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
) )
Spacer(Modifier.width(Dimens.SpacingM))
// Status: ZNS Stammdaten
val lastSync = znsState.lastSyncVersion
val znsLabel = if (lastSync != null) "ZNS: $lastSync" else "ZNS: Kein Sync"
StatusIndicator(
icon = Icons.Default.Dataset,
label = znsLabel,
color = if (lastSync != null) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.error
)
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {

View File

@ -277,8 +277,14 @@ fun VeranstalterAnlegenWizard(
OutlinedTextField( OutlinedTextField(
value = oeps, value = oeps,
onValueChange = { oeps = it }, onValueChange = { oeps = it },
label = { Text("OEPS-Nummer") }, label = { Text("OEPS-Nummer (z.B. 4-001)") },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
isError = oeps.isNotEmpty() && !oeps.matches(Regex("^[1-9]-[0-9]{3}$")),
supportingText = {
if (oeps.isNotEmpty() && !oeps.matches(Regex("^[1-9]-[0-9]{3}$"))) {
Text("Format: B-NNN (z.B. 4-001)")
}
}
) )
} }
@ -614,6 +620,13 @@ fun VeranstaltungKonfigV2(
} }
val today = LocalDate.now() val today = LocalDate.now()
val isStartInPast = dateVon != null && dateVon.isBefore(today) val isStartInPast = dateVon != null && dateVon.isBefore(today)
val daysBetween = if (dateVon != null && dateBis != null) {
java.time.temporal.ChronoUnit.DAYS.between(dateVon, dateBis) + 1
} else null
val isOetoConform = daysBetween == null || daysBetween <= 2
val isDateRangeInvalid = val isDateRangeInvalid =
(dateVon != null && dateBis != null && dateBis.isBefore(dateVon)) || isStartInPast (dateVon != null && dateBis != null && dateBis.isBefore(dateVon)) || isStartInPast
@ -663,9 +676,28 @@ fun VeranstaltungKonfigV2(
} }
}, },
supportingText = { supportingText = {
Column {
if (isDateRangeInvalid) { if (isDateRangeInvalid) {
Text("Enddatum darf nicht vor dem Startdatum liegen.") Text("Enddatum darf nicht vor dem Startdatum liegen.")
} }
if (isOetoConform.not()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
"Hinweis: Gemäß ÖTO sind C-Turniere auf 2 Tage begrenzt.",
color = MaterialTheme.colorScheme.primary
)
}
}
}
} }
) )
} }