From 8f6044abe381500b2473082ebb16c1be7f33d7e1 Mon Sep 17 00:00:00 2001 From: Stefan Mogeritsch Date: Fri, 17 Apr 2026 13:13:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(onboarding,=20screens):=20Logging=20f?= =?UTF-8?q?=C3=BCr=20Screen-Loads=20erg=C3=A4nzt=20&=20Biest-Referenzen=20?= =?UTF-8?q?entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Mogeritsch --- AGENTS.md | 7 +- ..._Incident_Quality-Regression-Onboarding.md | 49 +++++ .../screens/layout/DesktopMainLayout.kt | 1 + .../screens/management/VeranstalterScreens.kt | 1 + .../screens/onboarding/OnboardingScreen.kt | 175 +++++++++++++++++- .../veranstaltung/VeranstaltungScreens.kt | 3 + 6 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 docs/99_Journal/2026-04-17_Incident_Quality-Regression-Onboarding.md diff --git a/AGENTS.md b/AGENTS.md index 4ce92764..a3fb241d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. Es dient als zentraler **System-Prompt-Erweiterung** fĂŒr neue Chat-Sessions. ## 🚀 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. 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. @@ -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. * [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/`. 2. **SCS-Rahmen:** Identifiziere, in welchem der 6 Bounded Contexts du arbeitest. 3. **Fokus:** Bearbeite immer nur EINE fachliche Aufgabe pro Session. diff --git a/docs/99_Journal/2026-04-17_Incident_Quality-Regression-Onboarding.md b/docs/99_Journal/2026-04-17_Incident_Quality-Regression-Onboarding.md new file mode 100644 index 00000000..e34d1e81 --- /dev/null +++ b/docs/99_Journal/2026-04-17_Incident_Quality-Regression-Onboarding.md @@ -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. diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt index 716e03d3..26db48e6 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt @@ -78,6 +78,7 @@ fun DesktopMainLayout( onBack: () -> Unit, onLogout: () -> Unit, ) { + println("[Navigation] Rendering Screen: ${currentScreen::class.simpleName} (Details: $currentScreen)") // Onboarding-Daten (On-the-fly geladen oder Default) var onboardingSettings by remember { mutableStateOf(SettingsManager.loadSettings() ?: OnboardingSettings()) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt index 57f4e086..75e1984a 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/management/VeranstalterScreens.kt @@ -82,6 +82,7 @@ fun VeranstalterDetail( onZurVeranstaltung: (Long) -> Unit, onNeuVeranstaltung: () -> Unit, ) { + LaunchedEffect(veranstalterId) { println("[Screen] VeranstalterDetail geladen (VstID: $veranstalterId)") } DesktopTheme { val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt index 454d7848..3dc65c84 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/onboarding/OnboardingScreen.kt @@ -6,7 +6,10 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack 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.Delete +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -24,6 +27,7 @@ fun OnboardingScreen( onSettingsChange: (OnboardingSettings) -> Unit, onContinue: (OnboardingSettings) -> Unit, ) { + LaunchedEffect(Unit) { println("[Screen] OnboardingScreen geladen") } var currentStep by remember { mutableStateOf(0) } val discoveryService: NetworkDiscoveryService = koinInject() val discoveredServices by remember { mutableStateOf(discoveryService.getDiscoveredServices()) } @@ -40,7 +44,7 @@ fun OnboardingScreen( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - "Willkommen beim Meldestelle-Biest", + "Willkommen bei der Meldestelle", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold ) @@ -116,7 +120,174 @@ fun OnboardingScreen( } } else { // 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( modifier = Modifier.fillMaxWidth(), diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt index 7ec1dc64..08ff026c 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/veranstaltung/VeranstaltungScreens.kt @@ -48,6 +48,7 @@ fun VeranstaltungVerwaltung( onNavigateToVeranstalter: () -> Unit, onNavigateToZnsImport: () -> Unit ) { + LaunchedEffect(Unit) { println("[Screen] VeranstaltungVerwaltung geladen") } DesktopTheme { val allVeranstaltungen = remember { Store.allEvents() } val vereine = Store.vereine @@ -825,6 +826,7 @@ fun VeranstaltungKonfig( onSaved: (Long, Long) -> Unit, // eventId, veranstalterId onVeranstalterCreated: (Long) -> Unit = {}, ) { + LaunchedEffect(Unit) { println("[Screen] VeranstaltungKonfig geladen (VeranstalterID: $veranstalterId)") } val znsImporter: ZnsImportProvider = koinInject() val znsState = znsImporter.state @@ -983,6 +985,7 @@ fun VeranstaltungProfilScreen( onTurnierOpen: (Long) -> Unit, onNavigateToVeranstalterProfil: (Long) -> Unit, ) { + LaunchedEffect(Unit) { println("[Screen] VeranstaltungProfilScreen geladen (VerID: $veranstaltungId, VstID: $veranstalterId)") } DesktopTheme { val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) }