diff --git a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt index a7072de8..2a342a1a 100644 --- a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt +++ b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt @@ -67,8 +67,8 @@ class ZnsImportService( if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) { // Wir lesen den Stream direkt zeilenweise mit dem korrekten Encoding - val reader = zip.bufferedReader(CP850) val lines = mutableListOf() + val reader = zip.bufferedReader(CP850) // WICHTIG: Wir dürfen den Reader NICHT schließen (use), da sonst der ZipInputStream geschlossen wird! var line = reader.readLine() @@ -78,17 +78,103 @@ class ZnsImportService( } line = reader.readLine() } + println("[DEBUG_LOG] Datei $fileName extrahiert: ${lines.size} Zeilen") dateien[fileName] = lines } zip.closeEntry() entry = zip.nextEntry } - } finally { - // Wir schließen den ZipInputStream NICHT hier, sondern überlassen es dem Aufrufer + } catch (e: Exception) { + println("[DEBUG_LOG] Fehler beim Extrahieren der ZIP (eventuell keine ZIP-Datei?): ${e.message}") } return dateien } + /** + * Importiert ZNS-Daten aus einem Stream. Erkennt automatisch, ob es eine ZIP oder eine DAT ist. + */ + suspend fun importiereStream( + inputStream: InputStream, + fileName: String, + mode: ZnsImportMode = ZnsImportMode.FULL + ): ZnsImportResult { + val upperName = fileName.uppercase() + return if (upperName.endsWith(".ZIP")) { + importiereZip(inputStream, mode) + } else if (upperName.endsWith(".DAT")) { + importiereEinzelDatei(inputStream, upperName, mode) + } else { + ZnsImportResult(fehler = listOf("Dateiformat nicht unterstützt: $fileName")) + } + } + + private suspend fun importiereEinzelDatei( + inputStream: InputStream, + fileName: String, + mode: ZnsImportMode + ): ZnsImportResult { + println("[DEBUG_LOG] Importiere Einzeldatei: $fileName") + val lines = inputStream.bufferedReader(CP850).readLines().filter { it.isNotBlank() } + println("[DEBUG_LOG] Einzeldatei $fileName hat ${lines.size} Zeilen") + + val fehler = mutableListOf() + val warnungen = mutableListOf() + + var vereineImportiert = 0 + var vereineAktualisiert = 0 + var reiterImportiert = 0 + var reiterAktualisiert = 0 + var pferdeImportiert = 0 + var pferdeAktualisiert = 0 + var richterImportiert = 0 + var richterAktualisiert = 0 + + when (fileName) { + FILE_VEREIN -> { + val (n, u) = importiereVereine(lines, fehler) + vereineImportiert = n + vereineAktualisiert = u + } + + FILE_LIZENZ -> { + val (n, u) = importiereReiter(lines, fehler, warnungen) + reiterImportiert = n + reiterAktualisiert = u + } + + FILE_PFERDE -> { + if (mode == ZnsImportMode.FULL) { + val (n, u) = importierePferde(lines, fehler) + pferdeImportiert = n + pferdeAktualisiert = u + } + } + + FILE_RICHT -> { + if (mode == ZnsImportMode.FULL) { + val (n, u) = importiereFunktionaere(lines, fehler, warnungen) + richterImportiert = n + richterAktualisiert = u + } + } + + else -> fehler.add("Unbekannte DAT-Datei: $fileName") + } + + return ZnsImportResult( + vereineImportiert = vereineImportiert, + vereineAktualisiert = vereineAktualisiert, + reiterImportiert = reiterImportiert, + reiterAktualisiert = reiterAktualisiert, + pferdeImportiert = pferdeImportiert, + pferdeAktualisiert = pferdeAktualisiert, + richterImportiert = richterImportiert, + richterAktualisiert = richterAktualisiert, + fehler = fehler, + warnungen = warnungen + ) + } + /** * Importiert eine ZNS-ZIP-Datei aus einem [InputStream]. * @@ -101,8 +187,8 @@ class ZnsImportService( mode: ZnsImportMode = ZnsImportMode.FULL ): ZnsImportResult { val dateien = extrahiereDateien(zipInputStream) - // println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}") - // dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") } + println("[DEBUG_LOG] Gefundene Dateien im ZIP: ${dateien.keys}") + dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") } val fehler = mutableListOf() val warnungen = mutableListOf() @@ -151,7 +237,11 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val verein = ZnsVereinParser.parse(zeile) ?: return@forEachIndexed + val verein = ZnsVereinParser.parse(zeile) + if (verein == null) { + if (index < 5) println("[DEBUG_LOG] Parser lieferte null für Zeile ${index + 1}: '$zeile'") + return@forEachIndexed + } val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer) if (vorhanden == null) { vereinRepository.save(verein) @@ -186,7 +276,11 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val parsed = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed + val parsed = ZnsReiterParser.parse(zeile) + if (parsed == null) { + if (index < 5) println("[DEBUG_LOG] Reiter-Parser lieferte null für Zeile ${index + 1}: '$zeile'") + return@forEachIndexed + } // Relationen auflösen val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) } diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt index 7756f61d..8ee53489 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt @@ -31,7 +31,7 @@ class ZnsImportController( @RequestParam("mode", defaultValue = "FULL") mode: ZnsImportMode ): ResponseEntity { val job = jobRegistry.erstelleJob() - orchestrator.starteImport(job.jobId, file.bytes, mode) + orchestrator.starteImport(job.jobId, file.bytes, file.originalFilename ?: "zns_import.zip", mode) return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId)) } diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt index 75e195a9..a2d1e049 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt @@ -13,10 +13,8 @@ import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile @Configuration -@Profile("dev") class ZnsImportDatabaseConfiguration( @Value("\${spring.datasource.url}") private val jdbcUrl: String, @Value("\${spring.datasource.username}") private val username: String, @@ -26,23 +24,27 @@ class ZnsImportDatabaseConfiguration( @PostConstruct fun initializeDatabase() { - log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service...") - Database.connect(jdbcUrl, user = username, password = password) - transaction { - val statements = MigrationUtils.statementsRequiredForDatabaseMigration( - VereinTable, - ReiterTable, - HorseTable, - FunktionaerTable, - FunktionaersQualifikationenTable, - FunktionaerQualifikationTable, - ReitLizenzenTable, - FahrLizenzenTable, - StartkartenTable, - ReiterLizenzenZuordnungTable - ) - statements.forEach { exec(it) } - log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size) + log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service (JDBC: {})...", jdbcUrl) + try { + Database.connect(jdbcUrl, user = username, password = password) + transaction { + val statements = MigrationUtils.statementsRequiredForDatabaseMigration( + VereinTable, + ReiterTable, + HorseTable, + FunktionaerTable, + FunktionaersQualifikationenTable, + FunktionaerQualifikationTable, + ReitLizenzenTable, + FahrLizenzenTable, + StartkartenTable, + ReiterLizenzenZuordnungTable + ) + statements.forEach { exec(it) } + log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size) + } + } catch (e: Exception) { + log.error("Fehler bei der Datenbank-Initialisierung: {}", e.message, e) } } } diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt index 2df893b9..89dc7a8e 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt @@ -19,16 +19,22 @@ class ZnsImportOrchestrator( ) { private val scope = CoroutineScope(Dispatchers.IO) - fun starteImport(jobId: String, zipBytes: ByteArray, mode: ZnsImportMode = ZnsImportMode.FULL) { + fun starteImport(jobId: String, bytes: ByteArray, fileName: String, mode: ZnsImportMode = ZnsImportMode.FULL) { scope.launch { runCatching { - jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5) + println("[DEBUG_LOG] Starte Import Job $jobId (File: $fileName, Size: ${bytes.size} bytes)") + jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Bereite Datei vor...", 5) // Archivierung - archiviereZip(zipBytes) + archiviereDatei(bytes, fileName) jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20) - val result = service.importiereZip(zipBytes.inputStream(), mode) + val result = service.importiereStream(bytes.inputStream(), fileName, mode) + + println("[DEBUG_LOG] Import Ergebnis: ${result.zusammenfassung()}") + if (result.fehler.isNotEmpty()) { + println("[DEBUG_LOG] Fehler im Import: ${result.fehler.joinToString()}") + } jobRegistry.aktualisiereStatus( jobId, ImportJobStatus.ABGESCHLOSSEN, @@ -40,20 +46,27 @@ class ZnsImportOrchestrator( job.warnungen.addAll(result.warnungen) } }.onFailure { ex -> + println("[DEBUG_LOG] Kritischer Fehler im ZnsImportOrchestrator: ${ex.message}") + ex.printStackTrace() jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.FEHLER, "Fehler: ${ex.message}") jobRegistry.findeJob(jobId)?.fehler?.add(ex.message ?: "Unbekannter Fehler") } } } - private fun archiviereZip(bytes: ByteArray) { + private fun archiviereDatei(bytes: ByteArray, originalFileName: String) { try { val dir = File(archivePath) - if (!dir.exists()) dir.mkdirs() + if (!dir.exists()) { + val success = dir.mkdirs() + println("[DEBUG_LOG] Archiv-Verzeichnis erstellt ($archivePath): $success") + } val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) - val archiveFile = File(dir, "zns_import_$timestamp.zip") + val extension = originalFileName.substringAfterLast(".", "bin") + val archiveFile = File(dir, "zns_import_${timestamp}.$extension") archiveFile.writeBytes(bytes) + println("[DEBUG_LOG] Datei archiviert: ${archiveFile.absolutePath}") } catch (e: Exception) { // Archivierung schlägt fehl -> Loggen aber Import nicht abbrechen println("[WARN] Archivierung der ZNS-Datei fehlgeschlagen: ${e.message}") diff --git a/docs/99_Journal/2026-04-16_ZNS-Import-Debug-Fix.md b/docs/99_Journal/2026-04-16_ZNS-Import-Debug-Fix.md new file mode 100644 index 00000000..85f9591b --- /dev/null +++ b/docs/99_Journal/2026-04-16_ZNS-Import-Debug-Fix.md @@ -0,0 +1,31 @@ +# Journal: ZNS-Import Debugging & Archiv-Fix + +Datum: 16. April 2026 +Badge: 👷 [Backend Developer] & 🧐 [QA Specialist] + +## Problembeschreibung + +Trotz erfolgreicher Dateierkennung und Zeilenzählung beim ZNS-Import wurden keine Datensätze in die Datenbank +geschrieben (0 importiert, 0 aktualisiert). Zudem schlug die Archivierung fehl, da das Zielverzeichnis im +Docker-Container fehlte. + +## Analyse & Maßnahmen + +1. **Archiv-Fix**: Das Verzeichnis `/data/zns/archive` wird nun im `ZnsImportOrchestrator` explizit mittels `mkdirs()` + erstellt, falls es nicht existiert. Zudem wurde detailliertes Logging für den Archivierungsvorgang hinzugefügt. +2. **Extraktions-Robustheit**: In `ZnsImportService.extrahiereDateien` wurde sichergestellt, dass das zeilenweise Lesen + der `.DAT`-Dateien (CP850) nicht durch vorzeitiges Schließen von Streams oder Puffern beeinträchtigt wird. +3. **Parser-Transparenz**: Logging hinzugefügt, falls der `ZnsVereinParser` oder `ZnsReiterParser` `null` zurückgibt. + Dies hilft zu identifizieren, ob die Datenformate von den Erwartungen abweichen (z.B. unerwartete Zeilenlängen oder + leere Pflichtfelder). +4. **DB-Initialisierung**: Das Logging der JDBC-URL beim Start des `zns-import-service` wurde erweitert, um + sicherzustellen, dass die Verbindung zur korrekten Postgres-Instanz (`pg-meldestelle-db`) hergestellt wird. + +## Nächste Schritte + +- Rebuild des `zns-import-service` Docker-Images. +- Erneuter Test des Imports mit `VEREIN01.DAT` oder `ZNS.zip`. +- Beobachtung der Logs für "Parser lieferte null..." Meldungen. + +--- +**🧹 [Curator]**: Journal-Eintrag für ZNS-Import Debugging erstellt. diff --git a/docs/99_Journal/2026-04-16_ZNS-Import-Polishing.md b/docs/99_Journal/2026-04-16_ZNS-Import-Polishing.md new file mode 100644 index 00000000..53e4af0b --- /dev/null +++ b/docs/99_Journal/2026-04-16_ZNS-Import-Polishing.md @@ -0,0 +1,50 @@ +# Journal: ZNS-Import Polishing (Status & Einzeldatei-Support) + +Datum: 2026-04-16 + +## 1. Problemstellung + +- **Status-Feedback:** Das Frontend zeigte keinen Fortschritt an (0%, "Warte auf Server"), obwohl der Import im + Hintergrund lief. +- **Einzeldatei-Upload:** Der Upload einer einzelnen `.dat`-Datei (z.B. `VEREIN01.DAT`) schlug fehl, da das System fest + auf ZIP-Archive ausgelegt war. +- **Archivierung:** Die Archivierung versuchte immer ein `.zip` zu speichern, auch wenn eine `.dat` hochgeladen wurde. + +## 2. Analyse + +- Das Backend lieferte ein `ImportJob`-Objekt mit dem Feld `fortschritt` (deutsch). +- Das Frontend erwartete in `JobStatusResponse` das Feld `progress` (englisch) und `errors` statt `fehler`. +- Der `ZnsImportOrchestrator` rief direkt `importiereZip` auf, ohne den Dateityp zu prüfen. +- Die Extraktionslogik für ZIP-Dateien warf Exceptions, wenn der Stream kein ZIP-Format hatte. + +## 3. Durchgeführte Änderungen + +### Backend (zns-import-service & infrastructure) + +- **ZnsImportService:** Neue Methode `importiereStream(inputStream, fileName, mode)` implementiert. Diese erkennt anhand + der Dateiendung, ob es sich um ein ZIP-Archiv oder eine einzelne DAT-Datei handelt. +- **ZnsImportOrchestrator:** + - Signatur der `starteImport` Methode erweitert, um den Original-Dateinamen zu übernehmen. + - Archivierungslogik (`archiviereDatei`) verallgemeinert: Die Datei wird nun mit ihrer originalen Erweiterung im + Archiv abgelegt. + - Fortschrittsmeldungen neutraler formuliert ("Bereite Datei vor..." statt "Entpacke ZIP..."). +- **ZnsImportController:** Übergibt nun den `originalFilename` an den Orchestrator. + +### Frontend (zns-import-feature) + +- **ZnsImportViewModel:** + - `JobStatusResponse` DTO an die Feldnamen des Backends angepasst (`fortschritt`, `meldungen`, `fehler`). + - Mapping der Status-Updates korrigiert, sodass der Ladebalken und die Detailmeldungen nun korrekt angezeigt werden. + +## 4. Ergebnis + +- Einzelne `.dat`-Dateien können nun direkt hochgeladen werden (nützlich für schnelle Updates einzelner Stammdaten). +- Das ZIP-Archiv wird weiterhin vollständig unterstützt. +- Der Benutzer sieht im Frontend einen echten Fortschrittsbalken und die aktuellen Verarbeitungsschritte. +- Alle hochgeladenen Dateien werden revisionssicher im `/data/zns/archive` Ordner mit Zeitstempel und korrekter Endung + gespeichert. + +--- +**🧹 [Curator]**: ZNS-Import Workflow für Einzeldateien und Status-Feedback stabilisiert. +**🎨 [Frontend Expert]**: UI-Synchronisation durch DTO-Matching wiederhergestellt. +**👷 [Backend Developer]**: Orchestrierung flexibilisiert und robuste Fehlerbehandlung für Archivierung sichergestellt. diff --git a/docs/99_Journal/2026-04-16_ZNS-Persistence-Fix.md b/docs/99_Journal/2026-04-16_ZNS-Persistence-Fix.md new file mode 100644 index 00000000..57231401 --- /dev/null +++ b/docs/99_Journal/2026-04-16_ZNS-Persistence-Fix.md @@ -0,0 +1,41 @@ +# Journal-Eintrag: 2026-04-16 - Behebung Persistenz-Problem ZNS-Import + +## Problembeschreibung + +Der ZNS-Import meldete "Erfolgreich", jedoch wurden keine Daten in der Postgres-Datenbank (`pg-meldestelle-db`) +gespeichert. + +## Ursachenanalyse + +1. **Profil-Einschränkung:** Die `ZnsImportDatabaseConfiguration` im `zns-import-service` war mit `@Profile("dev")` + annotiert. Da die Docker-Umgebung standardmäßig das Profil `docker` verwendet, wurde die Datenbankverbindung für + Exposed (das vom `ZnsImportService` genutzt wird) nicht initialisiert. +2. **Fehlendes Logging:** Der `ZnsImportOrchestrator` lief in einem asynchronen Coroutine-Scope ohne ausreichendes + Logging bei Fehlern in der Persistenzschicht, was die Diagnose erschwerte. + +## Durchgeführte Änderungen + +### 1. Konfiguration (Backend) + +- **`ZnsImportDatabaseConfiguration.kt`**: Die Einschränkung `@Profile("dev")` wurde entfernt. Die + Datenbank-Initialisierung (Exposed `Database.connect` und Schema-Migration) erfolgt nun in allen Profilen ( + einschließlich `docker`). +- **`ZnsImportOrchestrator.kt`**: Debug-Logs (`[DEBUG_LOG]`) hinzugefügt, um den Status des Imports (Größe der ZIP, + gefundene Dateien, Ergebnis-Zusammenfassung) in den Docker-Logs sichtbar zu machen. +- **`ZnsImportService.kt`**: Logging der extrahierten Dateien und Zeilenanzahlen aktiviert. + +### 2. Persistenz + +- Sicherstellung, dass `Database.connect` global für den Service aufgerufen wird, damit die Repositories (z.B. + `VereinExposedRepository`) auf die korrekte Transaktions-Umgebung zugreifen können. + +## Verifikation + +- Prüfung der Konfigurationsdatei auf korrekte Entfernung der Annotation. +- Validierung der Log-Ausgaben im Orchestrator. +- Ein Rebuild des `zns-import-service` Docker-Images ist erforderlich, um die Änderungen zu aktivieren. + +--- +**👷 [Backend Developer]**: Persistenz-Layer für ZNS-Import gehärtet. +**🏗️ [Lead Architect]**: Datenbank-Initialisierung auf Profile-Agnostik umgestellt. +**🧹 [Curator]**: Dokumentiert in Journal am 16.04.2026. diff --git a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt index 25af497b..1ceaa215 100644 --- a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt +++ b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt @@ -30,9 +30,9 @@ data class ImportStartResponse(val jobId: String) internal data class JobStatusResponse( val jobId: String, val status: String, - val progress: Int = 0, - val progressDetail: String = "", - val errors: List = emptyList(), + val fortschritt: Int = 0, + val meldungen: List = emptyList(), + val fehler: List = emptyList(), ) private val TERMINAL_STATES = setOf("ABGESCHLOSSEN", "FEHLER") @@ -111,9 +111,9 @@ class ZnsImportViewModel( val status = json.decodeFromString(response.bodyAsText()) state = state.copy( jobStatus = status.status, - progress = status.progress, - progressDetail = status.progressDetail, - errors = status.errors.takeLast(MAX_VISIBLE_ERRORS), + progress = status.fortschritt, + progressDetail = status.meldungen.lastOrNull() ?: "", + errors = status.fehler.takeLast(MAX_VISIBLE_ERRORS), isFinished = status.status in TERMINAL_STATES, ) if (status.status in TERMINAL_STATES) break