feat(onboarding, screens): Logging für Screen-Loads ergänzt & Biest-Referenzen entfernt
Some checks failed
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 1m2s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m7s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m18s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 59s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 2m0s

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-17 13:13:43 +02:00
parent 8857d52f44
commit 8f6044abe3
6 changed files with 231 additions and 5 deletions

View File

@ -1,10 +1,11 @@
# 🤖 Projekt Agenten & Protokoll (Meldestelle-Biest) # 🤖 Projekt Agenten & Protokoll (Meldestelle)
Dieses Dokument definiert die Zusammenarbeit zwischen dem User (Owner) und den spezialisierten KI-Agenten. Dieses Dokument definiert die Zusammenarbeit zwischen dem User (Owner) und den spezialisierten KI-Agenten.
Es dient als zentraler **System-Prompt-Erweiterung** für neue Chat-Sessions. Es dient als zentraler **System-Prompt-Erweiterung** für neue Chat-Sessions.
## 🚀 Strategische Ausrichtung ## 🚀 Strategische Ausrichtung
Das Projekt **"Meldestelle-Biest"** entwickelt eine ÖTO/FEI-konforme, offline-fähige Turnier-Software.
Das Projekt **"Meldestelle"** entwickelt eine ÖTO/FEI-konforme, offline-fähige Turnier-Software.
1. **Desktop-First:** Primäres Ziel ist die Compose Desktop App (KMP). UX & Performance sind auf Profis optimiert. 1. **Desktop-First:** Primäres Ziel ist die Compose Desktop App (KMP). UX & Performance sind auf Profis optimiert.
2. **Offline-First:** Das System muss autark (ohne Internet) funktionieren. Sync-Logik ist Kernbestandteil. 2. **Offline-First:** Das System muss autark (ohne Internet) funktionieren. Sync-Logik ist Kernbestandteil.
3. **Domain-Driven:** 6 Bounded Contexts (SCS) bilden den fachlichen Rahmen. 3. **Domain-Driven:** 6 Bounded Contexts (SCS) bilden den fachlichen Rahmen.
@ -29,7 +30,7 @@ Jede Agenten-Antwort **muss** mit dem entsprechenden Badge beginnen, um den Kont
* **🧹 [Curator]**: Wissens-Management & Dokumentations-Check (ADR, Reference, Journal). Beendet jede Session. * **🧹 [Curator]**: Wissens-Management & Dokumentations-Check (ADR, Reference, Journal). Beendet jede Session.
* [Playbook](docs/04_Agents/Playbooks/Curator.md) * [Playbook](docs/04_Agents/Playbooks/Curator.md)
## 2. Der "Biest"-Workflow ## 2. Der "Meldestelle"-Workflow
1. **Kontext-Check:** Lies immer zuerst die `MASTER_ROADMAP` in `docs/01_Architecture/`. 1. **Kontext-Check:** Lies immer zuerst die `MASTER_ROADMAP` in `docs/01_Architecture/`.
2. **SCS-Rahmen:** Identifiziere, in welchem der 6 Bounded Contexts du arbeitest. 2. **SCS-Rahmen:** Identifiziere, in welchem der 6 Bounded Contexts du arbeitest.
3. **Fokus:** Bearbeite immer nur EINE fachliche Aufgabe pro Session. 3. **Fokus:** Bearbeite immer nur EINE fachliche Aufgabe pro Session.

View File

@ -0,0 +1,49 @@
# Incident Report: Quality Regression during V2 Refactoring & Naming Correction
**Datum:** 17. April 2026
**Status:** KRITISCH / RECOVERY
**Beteiligte:** Alle Agenten (Lead Architect, Frontend Expert, Curator)
## 1. Vorfall-Beschreibung
Während der geplanten Konsolidierung des Codes (Entfernung des `v2`-Präfixes und Ordners) kam es zu einem erheblichen
Verlust an fachlicher Tiefe im Onboarding-Wizard.
Obwohl die strukturelle Bereinigung erfolgreich war, wurden essenzielle Validierungs-Logiken, UI-Elemente für das
Client-Management und die mDNS-Discovery-Integration nicht vollständig in die neue Struktur übernommen.
Zudem wurde fälschlicherweise das Projekt-Präfix "Biest" (welches sich nur auf die Server-Hardware-Konfiguration bezog)
als Projektname verwendet, was zu berechtigtem Unmut beim User führte.
## 2. Fehleranalyse
* **Struktur vor Inhalt:** Der Fokus lag zu stark auf der Paket-Struktur und der Namens-Konsolidierung. Die fachliche
Parität wurde nicht penibel genug geprüft.
* **Husch-Pfusch:** Die Wiederherstellungsversuche nach der ersten Fehlermeldung waren unvollständig und erreichten
nicht den zuvor erarbeiteten Qualitätsstandard (High-Density UI).
* **Mangelnde Kommunikation:** Die Fehlinterpretation des Namens "Biest" wurde nicht rechtzeitig korrigiert, obwohl der
User mehrfach darauf hinwies.
## 3. Der "Meldestelle-Qualitäts-Pakt" (NEU)
Um die Professionalität des Projekts "Meldestelle" zu wahren, werden folgende Regeln verbindlich eingeführt:
1. **NAMENS-DIREKTIV:** Das Projekt heißt ausschließlich **"Meldestelle"**. Der Begriff "Biest" ist aus allen
Software-Komponenten und öffentlichen Dokumenten zu entfernen (außer in rein technischem Bezug auf den
MiniForum-Server MS-R1).
2. **FEATURE-PARITY GATE:** Vor jedem Löschen oder Verschieben von Code muss eine Liste der fachlichen Features (
Validierungen, UI-Details, Edge-Cases) erstellt werden. Diese muss nach dem Refactoring 1:1 nachweisbar sein.
3. **UI-HYGIENE:** Keine "Downgrades" im UI. Der High-Density-Standard (Material 3, ListItem, Badges, korrekte Spacings)
ist nicht verhandelbar.
4. **RECOVERY-PLAN:** Die Abend-Session wird ausschließlich dazu genutzt, den Onboarding-Wizard und die mDNS-Integration
auf den Stand vom 16.04.2026 zurückzuführen jedoch in der neuen, sauberen Paketstruktur.
## 4. Handover für die Abend-Session
* [ ] **Wiederherstellung:** Onboarding-Step 2 muss Client-Management (Liste, Rollen, Löschen) enthalten.
* [ ] **Discovery:** mDNS-Suche im Client-Modus muss Live-Resultate liefern.
* [ ] **Validierung:** Alle Felder im Onboarding benötigen den `OnboardingValidator`.
* [ ] **Review:** Lead Architect prüft jede Datei auf "Biest"-Altlasten und korrigiert diese.
---
**🧹 [Curator]**: Vorfall ist protokolliert. Der Fokus für heute Abend liegt zu 100% auf der Wiederherstellung der
Integrität und Professionalität.

View File

@ -78,6 +78,7 @@ fun DesktopMainLayout(
onBack: () -> Unit, onBack: () -> Unit,
onLogout: () -> Unit, onLogout: () -> Unit,
) { ) {
println("[Navigation] Rendering Screen: ${currentScreen::class.simpleName} (Details: $currentScreen)")
// Onboarding-Daten (On-the-fly geladen oder Default) // Onboarding-Daten (On-the-fly geladen oder Default)
var onboardingSettings by remember { mutableStateOf(SettingsManager.loadSettings() ?: OnboardingSettings()) } var onboardingSettings by remember { mutableStateOf(SettingsManager.loadSettings() ?: OnboardingSettings()) }

View File

@ -82,6 +82,7 @@ fun VeranstalterDetail(
onZurVeranstaltung: (Long) -> Unit, onZurVeranstaltung: (Long) -> Unit,
onNeuVeranstaltung: () -> Unit, onNeuVeranstaltung: () -> Unit,
) { ) {
LaunchedEffect(veranstalterId) { println("[Screen] VeranstalterDetail geladen (VstID: $veranstalterId)") }
DesktopTheme { DesktopTheme {
val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } } val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } }

View File

@ -6,7 +6,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -24,6 +27,7 @@ fun OnboardingScreen(
onSettingsChange: (OnboardingSettings) -> Unit, onSettingsChange: (OnboardingSettings) -> Unit,
onContinue: (OnboardingSettings) -> Unit, onContinue: (OnboardingSettings) -> Unit,
) { ) {
LaunchedEffect(Unit) { println("[Screen] OnboardingScreen geladen") }
var currentStep by remember { mutableStateOf(0) } var currentStep by remember { mutableStateOf(0) }
val discoveryService: NetworkDiscoveryService = koinInject() val discoveryService: NetworkDiscoveryService = koinInject()
val discoveredServices by remember { mutableStateOf(discoveryService.getDiscoveredServices()) } val discoveredServices by remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
@ -40,7 +44,7 @@ fun OnboardingScreen(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text(
"Willkommen beim Meldestelle-Biest", "Willkommen bei der Meldestelle",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@ -116,7 +120,174 @@ fun OnboardingScreen(
} }
} else { } else {
// PHASE 2: ROLLENSPEZIFISCH // PHASE 2: ROLLENSPEZIFISCH
Text("Konfiguration für ${settings.networkRole}") Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("⚙️ Geräte-Konfiguration (${settings.networkRole})", style = MaterialTheme.typography.titleMedium)
OutlinedTextField(
value = settings.geraetName,
onValueChange = { onSettingsChange(settings.copy(geraetName = it)) },
label = { Text("Gerätename") },
placeholder = { Text("z.B. Meldestelle-PC-1") },
modifier = Modifier.fillMaxWidth(),
isError = settings.geraetName.isNotEmpty() && !OnboardingValidator.isNameValid(settings.geraetName),
supportingText = {
if (settings.geraetName.isNotEmpty() && !OnboardingValidator.isNameValid(settings.geraetName)) {
Text("Mindestens ${OnboardingValidator.MIN_NAME_LENGTH} Zeichen erforderlich.")
}
}
)
OutlinedTextField(
value = settings.sharedKey,
onValueChange = { onSettingsChange(settings.copy(sharedKey = it)) },
label = { Text("Sicherheitsschlüssel (Sync-Key)") },
placeholder = { Text("Mindestens 8 Zeichen") },
modifier = Modifier.fillMaxWidth(),
isError = settings.sharedKey.isNotEmpty() && !OnboardingValidator.isKeyValid(settings.sharedKey),
supportingText = {
if (settings.sharedKey.isNotEmpty() && !OnboardingValidator.isKeyValid(settings.sharedKey)) {
Text("Mindestens ${OnboardingValidator.MIN_KEY_LENGTH} Zeichen erforderlich.")
}
}
)
if (settings.networkRole == NetworkRole.MASTER) {
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Text("👥 Erwartete Clients (Richter, Zeitnehmer, etc.)", style = MaterialTheme.typography.titleSmall)
Text(
"Definiere, welche Geräte sich mit diesem Master synchronisieren dürfen.",
style = MaterialTheme.typography.bodySmall
)
settings.expectedClients.forEachIndexed { index, client ->
ListItem(
headlineContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(client.name)
Badge { Text(client.role.name) }
}
},
trailingContent = {
IconButton(onClick = {
val newList = settings.expectedClients.toMutableList().apply { removeAt(index) }
onSettingsChange(settings.copy(expectedClients = newList))
}) {
Icon(
Icons.Default.Delete,
contentDescription = "Löschen",
tint = MaterialTheme.colorScheme.error
)
}
},
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
)
}
var newClientName by remember { mutableStateOf("") }
var newClientRole by remember { mutableStateOf(NetworkRole.RICHTER) }
var showAddClient by remember { mutableStateOf(false) }
if (showAddClient) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newClientName,
onValueChange = { newClientName = it },
label = { Text("Gerätename des Clients") },
modifier = Modifier.weight(1f)
)
// Simple Role Selector (nur ein kleiner Button für den Prototyp hier)
IconButton(onClick = {
val roles = NetworkRole.entries.filter { it != NetworkRole.MASTER }
val nextIndex = (roles.indexOf(newClientRole) + 1) % roles.size
newClientRole = roles[nextIndex]
}) {
Icon(Icons.Default.Settings, null)
}
Text(newClientRole.name, style = MaterialTheme.typography.labelSmall)
Button(
onClick = {
if (newClientName.isNotBlank()) {
val newList = settings.expectedClients + ExpectedClient(newClientName, newClientRole)
onSettingsChange(settings.copy(expectedClients = newList))
newClientName = ""
showAddClient = false
}
},
enabled = newClientName.isNotBlank()
) {
Icon(Icons.Default.Add, null)
}
}
} else {
TextButton(onClick = { showAddClient = true }) {
Icon(Icons.Default.Add, null)
Spacer(Modifier.width(8.dp))
Text("Client hinzufügen")
}
}
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
OutlinedTextField(
value = settings.backupPath,
onValueChange = { onSettingsChange(settings.copy(backupPath = it)) },
label = { Text("Backup-Verzeichnis (Pfad)") },
placeholder = { Text("/pfad/zu/den/backups") },
modifier = Modifier.fillMaxWidth(),
isError = settings.backupPath.isNotEmpty() && !OnboardingValidator.isBackupPathValid(settings.backupPath)
)
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
Slider(
value = settings.syncInterval.toFloat(),
onValueChange = { onSettingsChange(settings.copy(syncInterval = it.toInt())) },
valueRange = 1f..60f,
steps = 59
)
} else {
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall)
if (discoveredServices.isEmpty()) {
Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
Text("Suche nach Master...", modifier = Modifier.padding(start = 40.dp))
}
}
discoveredServices.forEach { service ->
ListItem(
headlineContent = { Text(service.name) },
supportingContent = { Text("${service.host}:${service.port}") },
trailingContent = {
Button(onClick = {
// Master-Daten in die Settings übernehmen (vereinfacht)
onSettingsChange(settings.copy(sharedKey = service.metadata["key"] ?: settings.sharedKey))
}) {
Text("Verbinden")
}
},
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer)
)
}
Text(
"Hinweis: Als Client wird dieses Gerät automatisch versuchen, den Master im Netzwerk zu finden.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View File

@ -48,6 +48,7 @@ fun VeranstaltungVerwaltung(
onNavigateToVeranstalter: () -> Unit, onNavigateToVeranstalter: () -> Unit,
onNavigateToZnsImport: () -> Unit onNavigateToZnsImport: () -> Unit
) { ) {
LaunchedEffect(Unit) { println("[Screen] VeranstaltungVerwaltung geladen") }
DesktopTheme { DesktopTheme {
val allVeranstaltungen = remember { Store.allEvents() } val allVeranstaltungen = remember { Store.allEvents() }
val vereine = Store.vereine val vereine = Store.vereine
@ -825,6 +826,7 @@ fun VeranstaltungKonfig(
onSaved: (Long, Long) -> Unit, // eventId, veranstalterId onSaved: (Long, Long) -> Unit, // eventId, veranstalterId
onVeranstalterCreated: (Long) -> Unit = {}, onVeranstalterCreated: (Long) -> Unit = {},
) { ) {
LaunchedEffect(Unit) { println("[Screen] VeranstaltungKonfig geladen (VeranstalterID: $veranstalterId)") }
val znsImporter: ZnsImportProvider = koinInject() val znsImporter: ZnsImportProvider = koinInject()
val znsState = znsImporter.state val znsState = znsImporter.state
@ -983,6 +985,7 @@ fun VeranstaltungProfilScreen(
onTurnierOpen: (Long) -> Unit, onTurnierOpen: (Long) -> Unit,
onNavigateToVeranstalterProfil: (Long) -> Unit, onNavigateToVeranstalterProfil: (Long) -> Unit,
) { ) {
LaunchedEffect(Unit) { println("[Screen] VeranstaltungProfilScreen geladen (VerID: $veranstaltungId, VstID: $veranstalterId)") }
DesktopTheme { DesktopTheme {
val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) } val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) }