diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsNennungParser.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsNennungParser.kt new file mode 100644 index 00000000..e6bbcf49 --- /dev/null +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsNennungParser.kt @@ -0,0 +1,65 @@ +package at.mocode.zns.parser + +import at.mocode.core.utils.parser.FixedWidthLineReader + +/** + * Domänen-Modell für eine ZNS-Nennung (N-Satz). + */ +data class ZnsNennung( + val bewerbNummer: Int, + val abteilung: Int, + val reiterName: String, + val pferdeName: String, + val verein: String, + val kopfNummer: String, + val reiterNummer: String, + val pferdeNummer: String, + val startWunsch: String? = null // Z.B. "V" für Vorne, "H" für Hinten (falls im N-Satz kodiert) +) + +/** + * Spezialisierter Parser für N-Sätze aus der n2-XXXXX.dat Datei. + * N-Sätze enthalten die konkreten Nennungen pro Bewerb. + */ +object ZnsNennungParser { + + /** + * Parst eine Zeile aus der n2-XXXXX.dat Datei, sofern es sich um einen N-Satz handelt. + * Ein N-Satz beginnt oft mit einem Kennzeichen (z.B. 'N' oder nach einem Bewerbs-Header). + */ + fun parse(line: String): ZnsNennung? { + // Ein valider N-Satz hat typischerweise eine feste Breite + if (line.length < 50) return null + + // N-Sätze in n2-Dateien folgen oft direkt auf B-Sätze + // Wir prüfen hier auf das typische Format (Starts with 'N' or index markers) + // Basierend auf OETO Specs: N-Sätze fangen oft mit 'N' an + if (!line.startsWith("N")) return null + + val reader = FixedWidthLineReader(line) + + // Die Offsets sind beispielhaft und müssen an das reale n2-Format angepasst werden + // Typischerweise: + // N 010 1 Mustermann Max Superpferd 01 001 12345 67890 + + val bewerbNummer = reader.getIntOrNull(2, 3) ?: return null + val abteilung = reader.getIntOrNull(5, 1) ?: 0 + val reiterName = reader.getString(6, 20) + val pferdeName = reader.getString(26, 20) + val verein = reader.getString(46, 20) + val kopfNummer = reader.getString(66, 4) + val reiterNummer = reader.getString(70, 7) + val pferdeNummer = reader.getString(77, 6) + + return ZnsNennung( + bewerbNummer = bewerbNummer, + abteilung = abteilung, + reiterName = reiterName, + pferdeName = pferdeName, + verein = verein, + kopfNummer = kopfNummer, + reiterNummer = reiterNummer, + pferdeNummer = pferdeNummer + ) + } +} diff --git a/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt b/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt index c794eedc..1e3ca869 100644 --- a/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt +++ b/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt @@ -107,4 +107,19 @@ class ZnsParserTest { assertEquals("CSN-C", bewerb.kategorie) assertEquals("2026-04-10", bewerb.datum.toString()) } + + @Test + fun `parseNennung should extract N-Satz correctly`() { + // N(1) + BEWNR(3) + ABT(1) + REITER(20) + PFERD(20) + VEREIN(20) + KOPF(4) + RNR(7) + PNR(6) + val line = "N0101Mustermann Max Superpferd 01 Reitclub Musterdorf 001 123456789012" + val nennung = ZnsNennungParser.parse(line) + + assertNotNull(nennung) + assertEquals(10, nennung.bewerbNummer) + assertEquals(1, nennung.abteilung) + assertEquals("Mustermann Max", nennung.reiterName) + assertEquals("Superpferd 01", nennung.pferdeName) + assertEquals("Reitclub Musterdorf", nennung.verein) + assertEquals("001", nennung.kopfNummer) + } } diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index fdf8e978..3b481932 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -219,7 +219,7 @@ und über definierte Schnittstellen kommunizieren. * [x] **Konzept/ADR:** LAN‑Sync (ADR‑0022) und Offline‑First Desktop↔Backend Konzept definiert und verlinkt. * [x] **Bewerbe-Import:** Implementierung der Merge-Logik (ZNS-XML -> BewerbUiModel). ✓ * [x] **Startlisten-Automatisierung:** Generierung und Zeitberechnung (Pausen, Umbauzeiten). ✓ -* [ ] **Discovery:** Implementierung des mDNS-Service für die Geräte-Suche (Phase 7 Übertrag). +* [x] **Discovery:** Implementierung des mDNS-Service (JmDNS) für die Geräte-Suche. ✓ * [ ] **Transport:** Aufbau der WebSocket-Infrastruktur für P2P-Sync (Phase 7 Übertrag). * [ ] **Offline-First Desktop↔Backend:** Umsetzung gemäß Konzept „Offline-First Synchronisation (Desktop ↔ Backend)“ → `docs/01_Architecture/konzept-offline-first-desktop-backend-de.md`. diff --git a/frontend/core/network/build.gradle.kts b/frontend/core/network/build.gradle.kts index e50f362c..b6d2540b 100644 --- a/frontend/core/network/build.gradle.kts +++ b/frontend/core/network/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { jvmMain.dependencies { implementation(libs.ktor.client.cio) + implementation("org.jmdns:jmdns:3.5.5") } jsMain.dependencies { diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt index 102cef58..816e9539 100644 --- a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt @@ -7,8 +7,10 @@ import io.ktor.client.plugins.logging.* import io.ktor.client.request.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json +import org.koin.core.module.Module import org.koin.core.qualifier.named import org.koin.dsl.module +import at.mocode.frontend.core.network.discovery.discoveryModule /** * Schnittstelle zur Token-Bereitstellung – entkoppelt core-network von core-auth. @@ -20,7 +22,8 @@ interface TokenProvider { fun getAccessToken(): String? } * - "baseHttpClient": Roh-Client für Auth/Keycloak (kein Token-Header) * - "apiClient": Konfigurierter Client für das API-Gateway (Auth-Header, Retry, Timeout) */ -val networkModule = module { +val networkModule: Module = module { + includes(discoveryModule) // 1. Basis-Client (für Auth-Endpunkte, ohne Bearer-Token) single(named("baseHttpClient")) { diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt new file mode 100644 index 00000000..afc8aad7 --- /dev/null +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt @@ -0,0 +1,10 @@ +package at.mocode.frontend.core.network.discovery + +import org.koin.core.module.Module + +/** + * Erwartetes Koin-Modul für die Netzwerk-Discovery. + * Plattform-spezifische Implementierungen (JVM mit JmDNS, JS/Wasm evtl. No-op) + * müssen hier injiziert werden. + */ +expect val discoveryModule: Module diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/discovery/NetworkDiscoveryService.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/discovery/NetworkDiscoveryService.kt new file mode 100644 index 00000000..42dafbe9 --- /dev/null +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/discovery/NetworkDiscoveryService.kt @@ -0,0 +1,38 @@ +package at.mocode.frontend.core.network.discovery + +/** + * Modell für einen entdeckten Dienst im lokalen Netzwerk. + */ +data class DiscoveredService( + val name: String, + val host: String, + val port: Int, + val metadata: Map = emptyMap() +) + +/** + * Interface für die mDNS-basierte Entdeckung von Meldestelle-Instanzen. + * Erlaubt Offline-First Synchronisation im LAN. + */ +interface NetworkDiscoveryService { + /** + * Startet das Scannen nach verfügbaren Diensten im Netzwerk. + */ + fun startDiscovery() + + /** + * Stoppt den Scan-Vorgang. + */ + fun stopDiscovery() + + /** + * Registriert den eigenen Dienst, damit andere Instanzen ihn finden können. + * @param port Der Port, auf dem der lokale WebSocket-Server lauscht. + */ + fun registerService(port: Int) + + /** + * Gibt die Liste der aktuell entdeckten Dienste zurück. + */ + fun getDiscoveredServices(): List +} diff --git a/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt b/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt new file mode 100644 index 00000000..bdb61747 --- /dev/null +++ b/frontend/core/network/src/jsMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt @@ -0,0 +1,18 @@ +package at.mocode.frontend.core.network.discovery + +import org.koin.core.module.Module +import org.koin.dsl.module + +/** + * JS-spezifische Implementierung (vorerst No-op, da mDNS im Browser nicht nativ möglich). + */ +actual val discoveryModule: Module = module { + single { NoOpDiscoveryService() } +} + +class NoOpDiscoveryService : NetworkDiscoveryService { + override fun startDiscovery() {} + override fun stopDiscovery() {} + override fun registerService(port: Int) {} + override fun getDiscoveredServices(): List = emptyList() +} diff --git a/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt new file mode 100644 index 00000000..22189b32 --- /dev/null +++ b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/DiscoveryModule.kt @@ -0,0 +1,11 @@ +package at.mocode.frontend.core.network.discovery + +import org.koin.core.module.Module +import org.koin.dsl.module + +/** + * JVM-spezifische Implementierung des DiscoveryModules. + */ +actual val discoveryModule: Module = module { + single { JmDnsDiscoveryService() } +} diff --git a/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt new file mode 100644 index 00000000..00f2bfe6 --- /dev/null +++ b/frontend/core/network/src/jvmMain/kotlin/at/mocode/frontend/core/network/discovery/JmDnsDiscoveryService.kt @@ -0,0 +1,69 @@ +package at.mocode.frontend.core.network.discovery + +import javax.jmdns.JmDNS +import javax.jmdns.ServiceEvent +import javax.jmdns.ServiceInfo +import javax.jmdns.ServiceListener +import java.net.InetAddress +import java.util.concurrent.ConcurrentHashMap + +/** + * JVM-spezifische Implementierung der Netzwerk-Discovery mittels JmDNS. + */ +class JmDnsDiscoveryService : NetworkDiscoveryService { + + private var jmdns: JmDNS? = null + private val SERVICE_TYPE = "_meldestelle-biest._tcp.local." + private val discoveredServicesMap = ConcurrentHashMap() + + override fun startDiscovery() { + if (jmdns == null) { + jmdns = JmDNS.create(InetAddress.getLocalHost()) + } + + jmdns?.addServiceListener(SERVICE_TYPE, object : ServiceListener { + override fun serviceAdded(event: ServiceEvent) { + // Bei ServiceAdded fordern wir die Details an + jmdns?.requestServiceInfo(event.type, event.name) + } + + override fun serviceRemoved(event: ServiceEvent) { + discoveredServicesMap.remove(event.name) + println("[Discovery] Service entfernt: ${event.name}") + } + + override fun serviceResolved(event: ServiceEvent) { + val info = event.info + val service = DiscoveredService( + name = event.name, + host = info.inetAddresses.firstOrNull()?.hostAddress ?: "unknown", + port = info.port, + metadata = info.propertyNames.asSequence().associateWith { info.getPropertyString(it) } + ) + discoveredServicesMap[event.name] = service + println("[Discovery] Service gefunden: ${service.name} @ ${service.host}:${service.port}") + } + }) + } + + override fun stopDiscovery() { + jmdns?.close() + jmdns = null + discoveredServicesMap.clear() + } + + override fun registerService(port: Int) { + val serviceInfo = ServiceInfo.create( + SERVICE_TYPE, + "Meldestelle-${System.getProperty("user.name")}", + port, + "Offline-First Sync Node" + ) + jmdns?.registerService(serviceInfo) + println("[Discovery] Eigenen Dienst registriert auf Port $port") + } + + override fun getDiscoveredServices(): List { + return discoveredServicesMap.values.toList() + } +} diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/BewerbViewModel.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/BewerbViewModel.kt index 2d0a0b0d..03c3777e 100644 --- a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/BewerbViewModel.kt +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/BewerbViewModel.kt @@ -1,5 +1,7 @@ package at.mocode.turnier.feature.presentation +import at.mocode.frontend.core.network.discovery.DiscoveredService +import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService import at.mocode.turnier.feature.domain.Bewerb import at.mocode.turnier.feature.domain.BewerbRepository import at.mocode.turnier.feature.domain.StartlistenRepository @@ -10,9 +12,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import kotlin.time.Duration.Companion.milliseconds typealias BewerbListItem = Bewerb @@ -36,6 +38,8 @@ data class BewerbState( val showImportDialog: Boolean = false, val showStartlistePreview: Boolean = false, val currentStartliste: List = emptyList(), + val discoveredNodes: List = emptyList(), + val isScanning: Boolean = false, // Verknüpfung zum Dialog-VM für Abteilungs-Logik (optional) val dialogState: BewerbAnlegenState = BewerbAnlegenState(), ) @@ -59,12 +63,16 @@ sealed interface BewerbIntent { data class ConfirmImport(val turnierId: Long) : BewerbIntent data object GenerateStartliste : BewerbIntent data object CloseStartlistePreview : BewerbIntent + data object StartNetworkScan : BewerbIntent + data object StopNetworkScan : BewerbIntent + data object RefreshDiscoveredNodes : BewerbIntent } class BewerbViewModel( private val repo: BewerbRepository, private val startlistenRepo: StartlistenRepository, + private val discoveryService: NetworkDiscoveryService? = null, private val turnierId: Long, ) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -114,9 +122,28 @@ class BewerbViewModel( } is BewerbIntent.GenerateStartliste -> generateStartliste() is BewerbIntent.CloseStartlistePreview -> reduce { it.copy(showStartlistePreview = false) } + is BewerbIntent.StartNetworkScan -> startScan() + is BewerbIntent.StopNetworkScan -> stopScan() + is BewerbIntent.RefreshDiscoveredNodes -> refreshNodes() } } + private fun startScan() { + discoveryService?.startDiscovery() + _state.update { it.copy(isScanning = true) } + refreshNodes() + } + + private fun stopScan() { + discoveryService?.stopDiscovery() + _state.update { it.copy(isScanning = false) } + } + + private fun refreshNodes() { + val nodes = discoveryService?.getDiscoveredServices() ?: emptyList() + _state.update { it.copy(discoveredNodes = nodes) } + } + private fun generateStartliste() { val selectedId = _state.value.selectedId ?: return reduce { it.copy(isLoading = true) } diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt index 7c3a2b70..f471cca8 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt @@ -1,5 +1,6 @@ package at.mocode.turnier.feature.di +import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService import at.mocode.turnier.feature.data.remote.DefaultAbteilungRepository import at.mocode.turnier.feature.data.remote.DefaultBewerbRepository import at.mocode.turnier.feature.data.remote.DefaultStartlistenRepository @@ -24,8 +25,15 @@ val turnierFeatureModule = module { // ViewModels factory { TurnierViewModel(repo = get()) } - // BewerbViewModel: repos + turnierId — turnierId wird per parametersOf übergeben - factory { (turnierId: Long) -> BewerbViewModel(repo = get(), startlistenRepo = get(), turnierId = turnierId) } + // BewerbViewModel: repos + discoveryService + turnierId + factory { (turnierId: Long) -> + BewerbViewModel( + repo = get(), + startlistenRepo = get(), + discoveryService = getOrNull(), + turnierId = turnierId + ) + } // BewerbAnlegenViewModel hat keinen Repository-Parameter (nutzt StoreV2 intern) factory { BewerbAnlegenViewModel() } // AbteilungViewModel: repo + bewerbId + abteilungsNr — per parametersOf übergeben diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierBewerbeTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierBewerbeTab.kt index d47b9d23..1e5a333c 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierBewerbeTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierBewerbeTab.kt @@ -43,6 +43,16 @@ fun BewerbeTabContent( ) { val state by viewModel.state.collectAsState() + // Polling für entdeckte Dienste, wenn Scan aktiv ist + LaunchedEffect(state.isScanning) { + if (state.isScanning) { + while (true) { + viewModel.send(BewerbIntent.RefreshDiscoveredNodes) + kotlinx.coroutines.delay(2000) + } + } + } + // Dialog-ViewModel für "Bewerb anlegen" val bewerbDialogVm = remember { BewerbAnlegenViewModel() } val bewerbDialogState by bewerbDialogVm.state.collectAsState() @@ -64,7 +74,12 @@ fun BewerbeTabContent( viewModel.send(BewerbIntent.OpenImportDialog) } }, - onGenerateStartliste = { viewModel.send(BewerbIntent.GenerateStartliste) } + onGenerateStartliste = { viewModel.send(BewerbIntent.GenerateStartliste) }, + onToggleScan = { + if (state.isScanning) viewModel.send(BewerbIntent.StopNetworkScan) + else viewModel.send(BewerbIntent.StartNetworkScan) + }, + isScanning = state.isScanning ) VerticalDivider() @@ -131,10 +146,20 @@ fun BewerbeTabContent( // ── Rechtes Detail-Panel ────────────────────────────────────────────── val selectedItem = state.list.find { it.id == state.selectedId } - BewerbeDetailPanel( - bewerb = selectedItem?.toUiModel(), - modifier = Modifier.width(340.dp).fillMaxHeight(), - ) + Column(modifier = Modifier.width(340.dp).fillMaxHeight()) { + BewerbeDetailPanel( + bewerb = selectedItem?.toUiModel(), + modifier = Modifier.weight(1f), + ) + + if (state.isScanning || state.discoveredNodes.isNotEmpty()) { + HorizontalDivider() + NetworkDiscoveryPanel( + nodes = state.discoveredNodes, + isScanning = state.isScanning + ) + } + } } if (bewerbDialogState.isOpen) { @@ -298,6 +323,8 @@ private fun BewerbeAktionsSpalte( onBewerbEinfuegen: () -> Unit = {}, onZnsImport: () -> Unit = {}, onGenerateStartliste: () -> Unit = {}, + onToggleScan: () -> Unit = {}, + isScanning: Boolean = false, ) { Column( modifier = modifier.padding(8.dp), @@ -314,6 +341,11 @@ private fun BewerbeAktionsSpalte( AktionsBtn("Bewerb nach\noben verschieben") AktionsBtn("Bewerb nach\nunten verschieben") HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + AktionsBtn( + label = if (isScanning) "Netzwerk-Scan\nStoppen" else "Netzwerk-Scan\nStarten", + onClick = onToggleScan + ) + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) AktionsBtn("Startliste\nGenerieren", onClick = onGenerateStartliste) AktionsBtn("Startliste\nBearbeiten") AktionsBtn("Startliste\nDrucken") @@ -322,6 +354,47 @@ private fun BewerbeAktionsSpalte( } } +@Composable +private fun NetworkDiscoveryPanel( + nodes: List, + isScanning: Boolean +) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .padding(8.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Netzwerk (P2P)", style = MaterialTheme.typography.titleSmall, color = PrimaryBlue) + if (isScanning) { + Spacer(Modifier.width(8.dp)) + CircularProgressIndicator(modifier = Modifier.size(12.dp), strokeWidth = 2.dp) + } + } + Spacer(Modifier.height(4.dp)) + if (nodes.isEmpty()) { + Text("Keine Instanzen gefunden", fontSize = 11.sp, color = Color.Gray) + } else { + LazyColumn { + items(nodes) { node -> + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(16.dp), tint = PrimaryBlue) + Spacer(Modifier.width(8.dp)) + Column { + Text(node.name, fontSize = 12.sp, fontWeight = FontWeight.Bold) + Text("${node.host}:${node.port}", fontSize = 10.sp, color = Color.Gray) + } + } + } + } + } + } +} + @Composable private fun AktionsBtn(label: String, onClick: () -> Unit = {}) { OutlinedButton( diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt index b0e809d5..27bbd17b 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt @@ -128,7 +128,7 @@ fun PreviewTurnierBewerbeTab() { override suspend fun generate(bewerbId: Long): Result> = Result.success(emptyList()) override suspend fun getByBewerb(bewerbId: Long): Result> = Result.success(emptyList()) } - val vm = BewerbViewModel(mockRepo, mockStartlistenRepo, 1L) + val vm = BewerbViewModel(mockRepo, mockStartlistenRepo, null, 1L) MaterialTheme { BewerbeTabContent(viewModel = vm, turnierId = 1L) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 065925ac..b81f8735 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,7 @@ keycloakAdminClient = "26.0.7" # Utilities bignum = "0.3.10" +jmdns = "3.5.12" logback = "1.5.25" caffeine = "3.2.3" reactorKafka = "1.3.23" @@ -260,6 +261,7 @@ reactor-kafka = { module = "io.projectreactor.kafka:reactor-kafka", version.ref jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin" } jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" } jakarta-annotation-api = { module = "jakarta.annotation:jakarta.annotation-api", version.ref = "jakartaAnnotation" } +jmdns = { module = "org.jmdns:jmdns", version.ref = "jmdns" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiter" }