feat(core+frontend): integrate mDNS-based network discovery and update UI

- **Network Discovery Service:**
  - Added platform-specific `DiscoveryModule` with JmDNS-based `JmDnsDiscoveryService` for JVM and no-op implementation for JS.
  - Implemented service and device discovery using mDNS to enable peer-to-peer synchronization within LAN.
  - Registered the module in Koin for dependency injection and integrated it with `networkModule`.

- **Frontend Integration:**
  - Enhanced `BewerbViewModel` with intents and actions for starting, stopping, and refreshing network scans.
  - Introduced polling for discovered services during an active scan.

- **UI Additions:**
  - Added a `NetworkDiscoveryPanel` in `TurnierBewerbeTab` to display discovered services and indicate scan state.
  - Updated action buttons to include toggle functionality for network scans.
This commit is contained in:
2026-04-10 10:27:16 +02:00
parent c06eb79cba
commit 721d991c5e
15 changed files with 351 additions and 11 deletions
@@ -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
)
}
}
@@ -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)
}
}