359 Commits

Author SHA1 Message Date
stefan 0817d49dfc ```markdown
fix: Entferne veraltete Gradle-Eigenschaft und aktualisiere Dokumentation

- **gradle.properties**: Veraltete Eigenschaft `kotlin.mpp.androidSourceSetLayoutVersion` entfernt.
- **Dokumentation**: Journal-Eintrag für die Gradle-Konfigurationsbereinigung hinzugefügt.

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
```
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-06-09 19:46:41 +02:00
stefan 44cf2b3edc ### fix: Aktualisiere Gradle und Kotlin Versionen
- **gradle-wrapper.properties:** Upgegrade von Gradle 9.4.1 zu 9.5.1.
- **libs.versions.toml:** Upgegrade von Kotlin 2.3.20 zu 2.4.0.
- **gradle.properties:** Entferne veraltete `kotlin.mpp.androidSourceSetLayoutVersion`.
- **.aiassistant/rules/Meldestelle-Guidelines.md:** Füge neue Guidelines hinzu.

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-06-09 19:43:13 +02:00
stefan 4acbd6b0b2 chore(ai): add provider overlays and ignore dist directory 2026-06-03 12:48:22 +02:00
stefan 843bd145a8 chore(ai): establish single source of truth and cleanup legacy rules 2026-06-03 12:12:17 +02:00
stefan 98425b8fa8 chore/ai-guardrails-centralization
PR Guard / no-hardcoded-versions (pull_request) Failing after 12m14s
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been skipped
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-06-02 14:04:17 +02:00
stefan 5b6459a041 chore(ai): centralize guardrail scripts under .ai; add resolve_repo_root; add shims for .junie/.nolik; fix gitea contexts in release.yml
Co-authored-by: Junie <junie@jetbrains.com>
2026-06-02 14:01:26 +02:00
stefan d493734660 ### fix: 41 verbessere Lade- und Navigationslogik
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m50s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m16s
- **OnlineNennungFormular:** `isLoading`-State präziser verwaltet und Erfolgsmeldungen erweitert.
- **WebMainScreen:** Hashwechsel-Logik verbessert, Erfolgsscreen modularisiert.
- **UI-Version:** auf `v2026-04-23.41 - UI NAVIGATION FIX` aktualisiert.
2026-04-23 20:42:01 +02:00
stefan 0aaa160b95 ### fix: 40 aktualisiere SMTP-Port und erweitere Timeout-Parameter
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m53s
- **.env:** SMTP-Port auf `587` geändert.
- **application.yaml:** Timeout-Parameter (`connectiontimeout`, `timeout`, `writetimeout`) hinzugefügt.
- **MailServiceApplication:** Logging um SMTP-Timeout ergänzt.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.40 - SMTP PORT FORCE` aktualisiert.
2026-04-23 20:14:45 +02:00
stefan 03184aa951 ### fix: 39 finalisiere SMTP-Härtung und UI-Synchronisation
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m57s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m56s
- **application.yaml:** Parameter für SMTP-Authentifizierung (`STARTTLS_REQUIRED`) ergänzt.
- **MailServiceApplication:** Logging für SMTP-Passwort und Umgebungsvariablen erweitert.
- **WebMainScreen:** Erfolgsscreen-Logik bei API-Status `200 OK` optimiert.
- **dc-planb.yaml:** SMTP-Konfiguration mit STARTTLS zwingend ergänzt.
- **.env:** Korrekten SMTP-Host und `MAIL_SERVICE_URL` hinzugefügt.
- **Docs:** Änderungslog dokumentiert.
- **UI-Version:** auf `v2026-04-23.39 - FINAL SMTP & UI SYNC` aktualisiert.
2026-04-23 19:47:15 +02:00
stefan 34bd42a009 ### fix: 38 aktualisiere SMTP-Parameter und verbessere Fehlerbehandlung
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m48s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m52s
- **application.yaml:** SMTP-Parameter auf Umgebungsvariablen umgestellt.
- **MailServiceApplication:** Logging erweitert, Passwort maskiert.
- **WebMainScreen:** Fehlerbehandlung und Logging für API-Antworten optimiert, Versionsmarker auf `v2026-04-23.38 - SMTP & UI FINAL FORCE` aktualisiert.
2026-04-23 19:08:11 +02:00
stefan 897394e27e ### fix: 37 aktualisiere SMTP-Konfiguration und Versionsmarker
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m48s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m56s
- **application.yaml:** SMTP-Parameter (Host, Port, Benutzer, Passwort, Auth/StartTLS) hart kodiert.
- **MailServiceApplication:** Logging-Text für SMTP-Konfiguration angepasst.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.37 - SMTP HARD-CODED` aktualisiert.
2026-04-23 18:40:29 +02:00
stefan 9ab914dbfb ### fix: 36 verbessere SMTP-Logging und Versionsmarker
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m48s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m55s
- **dc-planb.yaml:** SMTP-Parameter hart codiert (Host, Port, Benutzer, Passwort).
- **mail-service:** Logging erweitert, SMTP-Konfiguration (`host`, `port`, `user`) wird angezeigt.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.36 - SMTP DIAGNOSE` aktualisiert.
2026-04-23 18:15:49 +02:00
stefan 9659fe3f8a ### fix: 35 verbessere SMTP-Konfiguration und Versionsmarker
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m41s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m51s
- **dc-planb.yaml:** Hard-Coded Passwort entfernt, AUTH/STARTTLS Flags erzwungen.
- **mail-service:** Nutzung der World4You-Credentials gesichert.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.35 - SMTP FIX` aktualisiert.
2026-04-23 17:51:38 +02:00
stefan 5cbf4fdfc0 ### fix: 34 erweitere Logging für Debugging und Callback-Analyse
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m41s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m12s
- **OnlineNennungFormular:** `println`-Logs für Callback-Status (`success`, `error`) hinzugefügt.
- **WebMainScreen:** Erfolgs-, Fehler- und State-Logs ergänzt.
- **Docs:** Neue Logging-Strategie dokumentiert.
- **UI:** Versionsmarker auf `v2026-04-23.34 - CALLBACK LOGGING` aktualisiert.
2026-04-23 17:17:37 +02:00
stefan bd06efe05d ### fix: 33 verbessere API-Antwort-Handling und Versionsmarker
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m48s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m13s
- **NennungRemoteRepository:** Handling für leeren JSON-Body angepasst.
- **MailController:** JSON-Antwort mit Erfolgsstatus und ID hinzugefügt.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.33 - JSON RESPONSE FIX` aktualisiert.
2026-04-23 16:59:11 +02:00
stefan 23c3e40390 ### fix: 32 erweitere Debug-Logging und Proxy-Strategie
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m52s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m13s
- **NennungRemoteRepository:** API-Logs erweitert (Antwortstatus und Body).
- **Caddyfile:** Strategie-Version auf `v32` aktualisiert.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.32 - PROXY DEBUG` angepasst.
2026-04-23 16:39:10 +02:00
stefan 1201755077 ### fix: 31 verbessere Fehlermeldungen und Same-Origin-Strategie
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m58s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m12s
- **OnlineNennungFormular:** Fehlermeldung für Server-Aufrufe präzisiert.
- **Caddyfile:** Header `X-Forwarded-Proto` hinzugefügt und Strategie-Version auf `v31` aktualisiert.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.31 - SAME-ORIGIN v3` aktualisiert.
2026-04-23 16:21:16 +02:00
stefan 162e2ef414 ### fix: 30 verbessere Fehlertransparenz und Debug-Logs
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m44s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m12s
- **OnlineNennungFormular:** Hinweis auf Browser-Konsole zu Fehlermeldungen hinzugefügt.
- **MailController:** Zusätzliche Debug-Logs für SMTP-Flows (Sendeversuch und Fehlerbehandlung).
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.30 - COMPILE FIX` aktualisiert.
2026-04-23 16:02:11 +02:00
stefan 3f291c907c ### fix: v28 verbessere Same-Origin-Strategie und Fehlerbehandlung
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m46s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m40s
- **PlatformConfig.wasmJs.kt:** API-URLs auf leere Strings geändert (Root-Proxy).
- **OnlineNennungFormular:** Fehlermeldungen bei Versandfehlern präzisiert.
- **NennungRemoteRepository:** Detaillierte Fehlerrückmeldungen hinzugefügt.
- **Caddyfile:** Reverse Proxy angepasst, Header und Strategie-Version aktualisiert.
- **WebMainScreen:** Versionsmarker auf `v2026-04-23.28 - SAME-ORIGIN v2` aktualisiert.
2026-04-23 15:40:15 +02:00
stefan 251647a6ab ### fix: implementiere Same-Origin-Strategie zur Umgehung von CORS
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m38s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m35s
- **PlatformConfig.wasmJs.kt:** API-URLs auf relative Pfade (`/api`) geändert.
- **Caddyfile:** `/api/*` Anfragen intern weitergeleitet, `/api` Präfix entfernt. Header angepasst.
- **UI:** Versionsmarker auf `v2026-04-23.27 - SAME-ORIGIN PROXY` aktualisiert.
- **Docs:** Analyse und Lösung zur neuen Strategie hinzugefügt.
2026-04-23 15:16:08 +02:00
stefan 277254ebbd ### fix: verbessere CORS-Handling und UI-Markierungen
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m51s
- **Caddyfile:** Ersetze `Access-Control-Allow-Origin` durch `*`, entferne `Access-Control-Allow-Credentials`, füge `Access-Control-Expose-Headers` hinzu.
- **GlobalSecurityConfig:** Lockere `allowedOrigins`, `allowedOriginPatterns` und `exposedHeaders` auf `*`, setze `allowCredentials` auf `false`.
- **MailServiceApplication:** Passe CORS-Mapping durch `allowedOrigins` und `allowCredentials` an.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.26 - NUCLEAR CORS v2`.
2026-04-23 14:42:49 +02:00
stefan f97bfeff47 ### fix: verbessere CORS-Handling im Caddy-Proxy
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m36s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m51s
- **Caddyfile:** `Access-Control-Allow-Headers` auf `*` gelockert, Versionsmarkers angepasst.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.25 - CADDY CATCH-ALL CORS`.
- **Docs:** Ergänzung der Analyse und Lösung für Version 25.
2026-04-23 14:21:26 +02:00
stefan 02a778751a ### fix: verbessere CORS-Handling im Caddy-Proxy
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m39s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m53s
- **Caddyfile:** OPTIONS-Handling optimiert: Hinzufügen spezifischer Header (`X-Requested-With`), Entfernen von `*` und leere Response (`respond "" 204`) eingeführt.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.24 - CADDY CORS FINAL BOSS`.
- **Docs:** Erweiterung der Analyse um Lösung und Status für Version 24.
2026-04-23 14:02:03 +02:00
stefan af0ece8ded ### fix: verbessere CORS-Handling im Caddy-Proxy
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m56s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m51s
- **Caddyfile:** Separates Handling für OPTIONS-Requests mit spezifischen Headern eingeführt, `defer` entfernt.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.23 - CADDY CORS OPTIONS FIX`.
- **Docs:** Ergänzung der Analyse und Lösung für Version 23.
2026-04-23 13:41:04 +02:00
stefan 03fa74abba ### fix: verbessere CORS-Handling im Caddy-Proxy
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m55s
- **Caddyfile:** Hinzufügen des `defer`-Flags zur korrekten Verarbeitung von CORS-Headern.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.22 - CADDY DEFER CORS FIX`.
- **Docs:** Ergänzung der Analyse und Lösung für Version 22.
2026-04-23 13:25:06 +02:00
stefan 71aea3f41d ### fix: verbessere CORS-Handling im Caddy-Proxy
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m54s
- **Caddyfile:** Verlagerung des CORS-Handlings in den Reverse Proxy, inkl. Unterstützung für Preflight-Anfragen und Header-Optimierungen.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.21 - CADDY CORS PROXY FIX`.
- **Docs:** Ergänzung der Problem- und Lösungshistorie für Version 21.
2026-04-23 13:02:52 +02:00
stefan 16c8674eff ### fix: verbessere CORS-Konfiguration und DNS-Verifizierung
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m55s
- **GlobalSecurityConfig:** Optimierung von `allowedOriginPatterns` und Hinzufügen von `exposedHeaders`.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.20 - CLOUDFLARE DNS VERIFIED`.
- **Docs:** Hinzufügen eines Screenshots zur Cloudflare-DNS-Analyse.
2026-04-23 12:43:39 +02:00
stefan df5276abf2 ### fix: verbessere CORS-Konfiguration
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Has been cancelled
- **GlobalSecurityConfig:** Lockerung von `allowedOriginPatterns` auf `*`.
- **MailServiceApplication:** Hinzufügen einer redundanten `WebMvcConfigurer` Bean für zusätzliches CORS-Mapping.
- **UI:** Aktualisierung des Versionsmarkers auf `v2026-04-23.19 - NUCLEAR CORS FIX`.
2026-04-23 12:35:50 +02:00
stefan 636ecc9883 ### fix: verbessere CORS-Konfiguration
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m51s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m54s
- **GlobalSecurityConfig:** Füge `allowedOriginPatterns` für Subdomains von `mo-code.at` hinzu.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.18 - RADICAL CORS PERMISSIVENESS`.
2026-04-23 12:15:49 +02:00
stefan 92950dbbe6 ### fix: behebe fehlende Spring Security Abhängigkeiten
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m40s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 9m25s
- **build.gradle.kts:** Füge `spring-boot-starter-security`, `spring-boot-starter-oauth2-resource-server` und `infrastructure:security` hinzu.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.17 - SECURITY DEPENDENCY FIX`.
2026-04-23 11:47:51 +02:00
stefan 5c51664e6c ### fix: behebe CORS- und Config-Probleme
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m58s
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
- **MailServiceApplication:** Füge `scanBasePackages` hinzu, um `GlobalSecurityConfig` korrekt zu laden.
- **GlobalSecurityConfig:** Erlaube Zugriff auf `/api/mail/nennung` ohne Authentifizierung.
- **MailController:** Entferne redundante `@CrossOrigin` Annotation.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.16 - CORS & CONFIG FIX`.
2026-04-23 11:16:18 +02:00
stefan 3244efd5e0 ### fix: behebe CORS-Probleme und Stabilitätsfehler
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 6m0s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m55s
- **MailController:** Erweitere `@CrossOrigin`-Headers und Methoden für Preflight-Checks.
- **GlobalSecurityConfig:** Reaktiviere CORS und füge explizite `CorsConfigurationSource` hinzu.
- **Tests:** Fix für `NoSuchBeanDefinitionException` bei Integrationstests.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.15 - CORS STABILITY`.
2026-04-23 10:53:55 +02:00
stefan af02e14f2d ### feat: verbessere Feedback- und Fehlerhandling im Nennformular
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 6m0s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m39s
- **OnlineNennungFormular:** Ladeindikator und Fehleranzeige bei API-Fehlermeldungen hinzugefügt.
- **WebMainScreen:** Navigation zum Erfolgsscreen erfolgt erst nach erfolgreicher API-Bestätigung.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.11 - NETWORK STATUS FIX`.
2026-04-23 10:16:16 +02:00
stefan 8730ffa7db ### feat: verbessere Feedback- und Fehlerhandling im Nennformular
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m51s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m44s
- **OnlineNennungFormular:** Ladeindikator und Fehleranzeige bei API-Fehlermeldungen hinzugefügt.
- **WebMainScreen:** Navigation zum Erfolgsscreen erfolgt erst nach erfolgreicher API-Bestätigung.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.11 - NETWORK STATUS FIX`.
2026-04-23 09:34:59 +02:00
stefan f7d11ccf97 ### feat: verbessere Feedback- und Fehlerhandling im Nennformular
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m49s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m13s
- **OnlineNennungFormular:** Ladeindikator und Fehleranzeige bei API-Fehlermeldungen hinzugefügt.
- **WebMainScreen:** Navigation zum Erfolgsscreen erfolgt erst nach erfolgreicher API-Bestätigung.
- **UI:** Aktualisiere Versionsmarker auf `v2026-04-23.11 - NETWORK STATUS FIX`.
2026-04-23 09:06:24 +02:00
stefan 76e6cebd90 ### fix: behebe HTTPS- und CORS-Probleme
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m51s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m5s
- **MailController:** Erweitere `@CrossOrigin`-Whitelist um `https://app.mo-code.at`.
- **dc-planb.yaml:** Passe API-URLs auf HTTPS an.
- **WebMainScreen:** Aktualisiere UI-Versionsmarker auf `v2026-04-23.10 - HTTPS FIX`.
2026-04-23 08:31:15 +02:00
stefan dbbca96c69 ### feat: verbessere PDF-Handling und füge neuen Versionsmarker hinzu
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m52s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m19s
- **WebMainScreen:** Implementiere Öffnen von Ausschreibungen (PDFs) in neuem Tab.
- **UI:** Ergänze dezentralen Versions-Marker in der Web-Oberfläche.
- **Docker-Publish:** Kopiere Turnier-Ausschreibungen (PDFs) in den Zielordner.
- **Assets:** Füge neue PDF-Dateien für Neumarkt2026 hinzu.
2026-04-23 08:11:15 +02:00
stefan eea022b862 ### feat(WebMainScreen, OnlineNennungFormular, index.html)
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m58s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m18s
- **WebMainScreen:** Implementiere hash-basiertes Routing für URLs und Synchronisation mit Adressleiste.
- **OnlineNennungFormular:** Aktualisiere Versionsanzeige mit neuer Farbe und Stil.
- **index.html:** Füge Runtime-Konfiguration per JavaScript hinzu (API, Mail, Keycloak).
2026-04-23 07:16:19 +02:00
stefan 6de5b55810 - **feat(OnlineNennungFormular):** verbessere Anzeige der Versionsnummer mit hervorgehobener Formatierung.
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m58s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m21s
- **chore(Caddy):** füge Cache-Buster für `meldestelle-web.js` hinzu.
2026-04-23 06:36:33 +02:00
stefan 07bd114df1 - **feat(OnlineNennungFormular):** Zeige eindeutige Versionsnummer zur besseren Nachverfolgbarkeit an.
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 6m11s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m17s
- **chore(Caddyfile):** Passe Caching-Strategie für `.wasm` und `.js` zur Vermeidung von Ladeproblemen während der Entwicklung an.
2026-04-23 06:14:39 +02:00
stefan 84d38f5eb5 chore: aktualisiere Dockerfile und CI-Workflow
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m48s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 2m48s
- **Dockerfile:** Füge `BUILD_DATE` als Argument hinzu, um Layer-Cache zu invalidieren.
- **CI:** Aktualisiere Build-Args mit `BUILD_DATE` aus Commit-Timestamp.
2026-04-23 05:53:26 +02:00
stefan 9db85236ec chore: füge Leerzeile zur README hinzu
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m28s
2026-04-23 05:23:48 +02:00
stefan f2a6078421 ### feat: erweitere und optimiere Online-Nennformular
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Has been cancelled
- **Display:** Link zur Nennungs-URL nur für Desktop hinzugefügt.
- **Inputs:** Unterstütze `singleLine` in allen Texteingabefeldern.
- **UX:** Hinzufügen von `ImeAction.Done` für Bemerkungen mit direkter Verarbeitung bei Abschluss.
2026-04-23 05:18:19 +02:00
stefan 568d9dbb32 ### feat: optimiere Online-Nennformular und Turnier-Integration
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 6m27s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 4m18s
- **`OnlineNennungFormular.kt`:**
  - Erweiterung um Felder für Telefon und Pferdename.
  - Dynamische Validierung und UI-Anpassungen für mobile Geräte.
  - Zusätzliche Bewerbslisten und Auswahlbeschränkungen hinzugefügt.
- **`WebMainScreen.kt`:**
  - Aktualisierte Turniermetadata und verbesserte Responsivität.
2026-04-23 04:48:51 +02:00
stefan f620f46d15 ### chore: aktualisiere Docker-Publish-Workflow
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m47s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 2m48s
- Entferne veraltete Cache- und Bereinigungsschritte.
- Implementiere neue Staging-Methode für Web-Assets ohne rsync.
- Aktualisiere Tags für Docker-Build und kommentiere ungenutzte Build-Args aus.
- Füge neuen Screenshot für Dokumentation hinzu.
2026-04-23 03:17:22 +02:00
stefan 46d3d7cf35 ### chore: aktualisiere Plan-B-Konfiguration und CI-Workflows
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 6m11s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 4m20s
- **dc-planb.yaml:** Passe Ports und füge Zipkin-Deaktivierung hinzu.
- **Docker-Publish:** Ergänze Bereinigung und Sicherheitsprüfung für Web-Assets.
2026-04-23 02:41:57 +02:00
stefan cb22b1bb96 chore: aktualisiere CI-Workflows
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been skipped
- **Workflows:** Deaktivierbar via `DESKTOP_CI_ENABLED` Repo-Variable.
- **Workflows:** Ergänze zusätzliche Prüfung auf Plan-B-Commits `[planb]`.
2026-04-23 00:40:11 +02:00
stefan 5544b04b07 ### chore: aktualisiere Desktop-Test-Workflow
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 4m23s
- **Workflows:** Begrenze Ausführung auf Änderungen am Desktop-Shell-Modul oder Workflow-Datei.
- **Workflows:** Füge `workflow_dispatch` für manuelles Starten hinzu.
- **Tests:** Korrigiere Run-Name für headless Tests (`xvfb`).
2026-04-23 00:29:55 +02:00
stefan 49d8b205d7 ### chore: aktualisiere Desktop-Test-Workflow
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 4m27s
- **CI:** Ergänze Installation von `xauth` im Desktop-Test-Workflow.
2026-04-23 00:14:36 +02:00
stefan f296a076dc ### chore: aktualisiere Docker-Build und CI für Web-Assets
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 55s
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m56s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 3m57s
- **Dockerfile:** Passe COPY-Pfad für Web-Assets auf neuen CI-Workflow an.
- **CI:** Füge Schritt zum Staging von Web-Assets vor dem Docker-Build hinzu.
2026-04-23 00:02:33 +02:00
stefan 1caefe6603 ### feat: optimiere Plan-B-Builds und CI/CD-Workflows
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been skipped
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m56s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 6m57s
- **Docker-Publish:** Reduziere auf Mail-Service und Web-App für schnellere Builds.
- **Workflows:** Überspringe Plan-B-Builds basierend auf Commit-Message ([planb]).
- **Frontend:** Aktualisiere Build-Skripte für Wasm-Distribution statt JS.
2026-04-22 23:41:03 +02:00
stefan 6b690232ff ### feat: füge Mail-Service-
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 58s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 5m59s
Build and Publish Docker Images / build-and-push (., backend/services/mail/Dockerfile, mail-service, mail-service) (push) Successful in 5m48s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m2s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 1m52s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m44s
2026-04-22 23:17:19 +02:00
stefan 309834d90c ### feat: verbessere DTO-Handling und füge Feature-Flag hinzu
- **NennungViewModel:** Definiere lokales `NennungDto` zur Optimierung der Backend-Response-Verarbeitung.
- **MailPollingService:** Ergänze Bedingung für Scheduler-Aktivierung (`ConditionalOnProperty`).
- **.env & application.yaml:** Füge `MAIL_POLLING_ENABLED` als Feature-Flag hinzu.
- **Dependencies:** Refaktor Import-Reihenfolge für Konsistenz.
2026-04-22 20:27:04 +02:00
stefan 8b44edda90 ### feat: aktualisiere Netzwerk-URLs und vereinfachte Online-Nennung
- **PlatformConfig:** Passe Standard-URLs für lokale Dienste an (`8092` statt `8083/8085`).
- **OnlineNennungFormular:** Entferne zusätzliche Felder und vereinfachere Validierungslogik.
- **OnlineNennungViewModel:** Ersetze HttpClient-Logik durch Repository-Injektion.
- **DI:** Aktualisiere Dependencies für `NennungRemoteRepository`.
2026-04-22 16:22:59 +02:00
stefan 255343145d ### feat: optimiere Architektur und verbessere E-Mail-Handling
- **ArchTests:** Passe Slices-Matching für `FrontendArchitectureTest` an Package-Struktur an.
- **Mail-Service:** Füge Plan-B-Benachrichtigung für Nennungen an Meldestelle hinzu; entferne Plus-Addressing (Fallback).
- **Build:** Deaktiviere Desktop-Build standardmäßig (`enableDesktop=false`) und mache Module-Registrierung optional.
2026-04-22 16:01:55 +02:00
stefan 5baa971b46 ### docs: aktualisiere ADR 0028 für E-Mail-basiertes Routing
- Ersetze Catch-All-Ansatz durch Betreff-basiertes Routing.
- Reduziere Infrastruktur-Aufwände durch generische Zieladresse.
2026-04-22 15:18:35 +02:00
stefan e65384768f ### feat: initialisiere Plan-B für E-Mail-basierte Online-Nennung
- **ADR 0028:** Dokumentiere MVP-Entscheidung für E-Mail-gesteuertes Nennsystem.
- **Gradle:** Aktiviere `enableWasm` für die Web-App-Generierung.
2026-04-22 15:11:00 +02:00
stefan beb20e0cf7 ### feat: erweitere ZNS und SQLDelight-Integration
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
- **SQLDelight:** Füge neue Queries (`countVereine`, `maxUpdated...`) zur SQLite-Datenbank hinzu und aktualisiere `DesktopMasterdataRepository`.
- **ZNS-Sync:** Passe `ZnsImportState` an, um Pferde- und Funktionärsdaten zu unterstützen.
- **Cloud-Sync:** Entferne redundante Auth-Header und setze Limits für Massensynchronisation auf 50.000 Datensätze.
- **Masterdata-Service:** Stabilisiere Consul Health-Checks und implementiere Limit-Beschränkungen auf Controller-Ebene.
2026-04-22 14:14:39 +02:00
stefan 98c241fc64 ### feat: erweitere Stammdaten-Integration
- **Repositories:** Implementiere und integriere `KtorPferdRepository` und `KtorFunktionaerRepository`.
- **SQLite:** Erweitere Schema um `LocalPferd` und `LocalFunktionaer` mit passenden Queries.
- **ViewModels:** Passe `PferdeViewModel` und `FunktionaerViewModel` an, um Flows und Repository-Injektion zu nutzen.
- **DI-Module:** Aktualisiere `PferdeModule` und `FunktionaerModule` für Backend-Anbindung.
2026-04-22 12:25:43 +02:00
stefan d4cc0eb77d ### feat: verbessere DI, Healthcheck-Logik und Reiter-API
- **Healthcheck:** Aktualisiere Dockerfile und konsolidiere Ports für konsistente Service-Gesundheitsprüfungen (8086 für Actuator, 8091 für API-Traffic).
- **ReiterRepository:** Implementiere `KtorReiterRepository` zur Nutzung der Backend-Stammdaten über API.
- **DI-Module:** Passe `ReiterModule` und `VereinFeatureModule` an, um den authentifizierten `apiClient` zu verwenden.
- **Masterdata-Service:** Synchronisiere Environment-Variablen und Konsul-Konfiguration mit aktualisierten Ports.
2026-04-22 12:11:38 +02:00
stefan e0b1ce8836 ### feat: implementiere SQLite-Integration und Repository-Refactoring
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 58s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m0s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m10s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 2m0s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m55s
- Erstelle Persistenz-Layer mit SQLite-Tabellen für `Verein` und `Reiter` inkl. Queries.
- Entferne Mock-Daten in `ReiterViewModel` und nutze Repository-Injektion.
- Integriere neue Tabellen und Queries im `DesktopMasterdataRepository`.
- Erweitere `VeranstalterWizardViewModel` um lokale Suche mit SQLite-Queries.
- Harmonisiere Feldnamen (`remoteReiterResults`) über alle Module hinweg.
- Aktualisiere DI-Module (`VeranstalterModule`, `ReiterModule`, `DesktopModule`) mit SQLite-Injektionen.
- Refaktor UI-Komponenten und Screens (`ReiterScreen`, `StammdatenImportScreen`) mit neuer Logik.
2026-04-22 02:20:55 +02:00
stefan f18b002f4e ### feat: füge Validierung und Fehler-Handling zur Veranstalter-Erstellung hinzu
- Implementiere Validierungslogik im `VeranstalterWizardViewModel` (Pflichtfelder, E-Mail-Format).
- Zeige Validierungsfehler direkt in der `VeranstalterNeuScreen` an.
- Erweiterung der State-Klasse um `errors` für direktes UI-Feedback.
2026-04-22 00:06:47 +02:00
stefan f8913f81b8 ### feat: überarbeite Veranstalter-Erstellung mit ZNS-Integration
- Implementiere "Search & Populate"-Logik im `VeranstalterWizardViewModel` und aktualisiere die Abhängigkeiten (`MasterdataRepository`, `ZnsImportProvider`).
- Integriere ZNS-Datensuche (Verein, Reiter) und automatisches Feld-Mapping bei Auswahl.
- Überarbeite `VeranstalterNeuScreen` zu einem zweispaltigen Layout mit Suche und Echtzeit-Vorschau.
- Aktualisiere Koin-Modul und entferne veraltete Wizard-Aufrufe in `ContentArea`.
- Füge zusätzliche ScreenPreviews hinzu und passe `ScreenPreviews.kt` an.
- Aktualisiere Dokumentation (`2026-04-21_Veranstalter-Neu-Overhaul.md`), Screenshots und relevante UI-Komponenten.
2026-04-21 23:22:14 +02:00
stefan 9195cdb14d ### feat: verbessere Wizard-Validierung und UI-Feedback
- Integriere Fortschrittsanzeige während der Veranstalter-Suche (`isCheckingStats`).
- Zeige Fehlermeldungen bei Suchfehlern im `EventWizardScreen`.
- Füge `hasSelectedVeranstalter`-Guard und zugehörige Tests hinzu.
- Präzisiere `DemoEventFlow` mit expliziter Guard-Logik.
- Aktualisiere Unit-Tests zur Abdeckung neuer Guard-Szenarien.
2026-04-21 21:26:06 +02:00
stefan 3f4ba9eea9 ### feat: erweitere Wizard- und UI-Logik
- Füge automatische Re-Evaluierung im `EventWizardViewModel` nach Import hinzu.
- Aktualisiere `StammdatenImportScreen` mit `onBack`-Callback für Status-Prüfung.
- Erweitere `PingScreen` im `ConnectivityCheck`-Screen um Navigation zum Login.
- Präzisiere `hasZns`-Guard mit Prüfung auf Import-Datum.
2026-04-21 21:10:58 +02:00
stefan 92028d9e02 ### feat: erweitere Wizard- und UI-Logik
- Füge automatische Re-Evaluierung im `EventWizardViewModel` nach Import hinzu.
- Aktualisiere `StammdatenImportScreen` mit `onBack`-Callback für Status-Prüfung.
- Erweitere `PingScreen` im `ConnectivityCheck`-Screen um Navigation zum Login.
- Präzisiere `hasZns`-Guard mit Prüfung auf Import-Datum.
2026-04-21 20:43:15 +02:00
stefan bdb45eefe4 ### feat: verbessere Validierungs- und Draft-Funktionalität im Wizard
- Entferne `onNavigateToVeranstalterNeu` aus `EventWizardScreen` und zugehörigen Komponenten.
- Füge persistente Speicherung für Drafts über `DraftStore` hinzu (JSON für JVM, No-op für Wasm).
- Ergänze WizardScaffold um `errorSummary` zur Anzeige von Validierungsfehlern.
- Bereinige und optimiere Schritt-Logik in `EventWizardViewModel`.
2026-04-21 20:12:53 +02:00
stefan 148b71db48 chore(devops): remove .idea files from git index and update .gitignore 2026-04-21 19:26:15 +02:00
stefan c54ad3830d feat: füge Wizard-Orchestrator mit Runtime, Scaffold und DraftStore (MVP) hinzu
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 18:30:16 +02:00
stefan d66bd63cc9 feat: füge DraftStore und Speichern/Resume von Wizard-Status hinzu
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 18:26:34 +02:00
stefan 3b4e3db51d feat: erweitere DemoEventFlow und EventWizardViewModel um neue Schritte
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 18:16:56 +02:00
stefan 2d7046d0e3 feat: passe EventWizardViewModel-Initialisierung für optionale Parameter an
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 18:03:55 +02:00
stefan d9b5c6bfea feat: aktiviere neues EventWizardScreen-Scaffold hinter Feature-Flag
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:58:48 +02:00
stefan 91a8c38b25 feat: implementiere WizardScaffold und Hotkey-Integration mittels Compose
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:56:56 +02:00
stefan 19ba044ec0 feat: integriere WizardRuntime in EventWizardViewModel und erweitere Schritt-Logik
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:44:31 +02:00
stefan 9556e0ac67 test: füge Unit-Tests für WizardRuntime hinzu
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:03:41 +02:00
stefan 4692bd186c chore: füge Core-Wizard-Modul hinzu und integriere in Veranstaltung-Feature
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:02:28 +02:00
stefan b11432df16 chore: implementiere Ping-Screen mit UI-Logik, ViewModel und Preview-Komponenten
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:02:15 +02:00
stefan 319cb52160 chore: implementiere Ping-Feature mit Repository, Sync-Service und API Client
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:02:03 +02:00
stefan a35dfa1434 chore: füge Event-Wizard-Screen und Schritt-Logik für neue Veranstaltungen hinzu
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:01:49 +02:00
stefan 237c71e5a0 chore: implementiere Wizard-Framework mit State- und Flow-Logik sowie Feature-Flags für Migration
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 17:01:31 +02:00
stefan ec124e9acd chore(docs): füge ADRs 0025–0027 und Wizard-DSL-Referenz hinzu, aktualisiere Roadmap und ADR-Index
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 16:21:22 +02:00
stefan 0ab1807235 chore: vereinheitliche Imports und ersetze androidx.compose.foundation.Image durch Image im Veranstalter-Wizard
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 15:18:04 +02:00
stefan 7cfdd06d1e chore: integriere Logo-Upload und Vorschau in Veranstalter-Wizard, verbessere Navigationslogik und erweitere Datenmodelle
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 15:16:05 +02:00
stefan 544fbf792c chore: erweitere Veranstalter-Wizard um Bearbeitungsmodus, füge Kontaktdaten und Step-Logik hinzu
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 14:37:43 +02:00
stefan 18e619abfc chore: erweitere Datenmodelle um neue Felder, verbessere Styling und aktualisiere Veranstalter-UI
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 14:24:54 +02:00
stefan 5eeff24b3a chore: refaktoriere Veranstaltungs-Wizard zu Event-Wizard, entferne überflüssige Komponenten und passe DI-Konfiguration an
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 14:00:04 +02:00
stefan f13c2eb35b chore: erweitere Datenmodelle um Nation und Bundesland, verbessere UI im Profil- und Veranstaltungs-Wizard
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 13:56:08 +02:00
stefan 2662d4e82e chore: erweitere Pferd-, Funktionär- und Reiter-Modelle um neue Felder, verbessere UI und Suche
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 13:47:37 +02:00
stefan 574f8c470c chore: refaktoriere Veranstaltungs-UI zu Events, implementiere ZNS-Suche und verbessere Navigationslogik
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 13:42:45 +02:00
stefan 9b4af2bb56 chore: füge Navigation zum Veranstalter-Wizard hinzu, erweitere Mock-Daten und verbessere Veranstaltungs-Flow
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 10:57:30 +02:00
stefan 1a295c18c8 chore: integriere Turnier-Wizard und ZNS-Importer in Veranstaltungsscreen, implementiere Profil-Onboarding und aktualisiere Modulabhängigkeiten
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 10:42:47 +02:00
stefan 01bf440f21 chore: behebe Kompilierungsfehler in ScreenPreviews.kt durch Implementierung von getStats() in Mock-Repos
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 09:41:57 +02:00
stefan 7acd9ea4c2 chore: implementiere Suche nach Veranstalter via OEPS-Nummer, verbessere UI-Flow im Veranstaltungs-Wizard und erweitere VereinRepository um OEPS-Abfrage
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 09:27:38 +02:00
stefan 30b53584f8 chore: implementiere Suche nach Veranstalter via OEPS-Nummer, verbessere UI-Flow im Veranstaltungs-Wizard und erweitere VereinRepository um OEPS-Abfrage
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-21 00:50:14 +02:00
stefan c1327f3186 chore: erweitere Veranstaltungs-Wizard um Ansprechperson-Anzeige, verbessere Fehlerhandling bei fehlenden Stammdaten und implementiere MsStringDropdown
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 23:53:43 +02:00
stefan 7a2c5700f9 chore: füge Warn-Dialoge für Rollenwechsel und Bearbeitungsmodus hinzu, verbessere Zustandshandhabung im Device-Setup und implementiere Turnierverwaltung im Veranstaltungs-Wizard
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 23:48:53 +02:00
stefan 5b8ef5ea2d chore: implementiere Lockscreen-Logik für Geräte- und Veranstaltungsinitialisierung, füge Zustandsprüfungen und neue UI-Komponenten hinzu
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 23:37:58 +02:00
stefan db58c24613 chore: entferne settings.json und Veranstaltungskomponenten, refaktoriere Veranstaltungsverwaltung und implementiere StoreVeranstaltungRepository
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 18:33:45 +02:00
stefan edfe05cbe3 chore: entferne ungenutzte Importe, reduziere Redundanz in DesktopMainLayout und verlagere Komponenten in Module
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 15:59:36 +02:00
stefan 6feb139a46 chore: füge SyncManager und Peer-Zähler hinzu, verbessere Navigation-Breadcrumbs und passe MD3-Stil an
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 15:09:25 +02:00
stefan b94e0f2d9d chore: implementiere Zustandsprüfung für DiscoveryService und ConnectivityTracker, verbessere Plug-and-Play-Kompatibilität und optimiere LAN-Discovery
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 14:39:48 +02:00
stefan 8806d11e3c chore: implementiere Auth-Status-abhängige Navigation und Icons, deaktiviere Module ohne Initialisierung und passe NavRail sowie Header für besseren UX an
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-20 14:23:47 +02:00
stefan a1bf93342e chore: fix 500 errors in ping-service, improve security annotations, update parameter mapping, integrate Resilience4j with Kotlin, and refine test suite 2026-04-20 14:21:53 +02:00
stefan 5887ac7b6c chore: update roadmap with finalized frontend modernization phase, refactor turnier and veranstalter features to adhere to Plug-and-Play architecture, and improve code hygiene in core frontend modules
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-20 10:55:28 +02:00
stefan 8aef46bba1 chore: refactor Veranstalter screens and ViewModels to decouple from Koin, implement hoisting for better testability, consolidate legacy shell logic into modern components, and add mock repositories for previews 2026-04-20 10:31:45 +02:00
stefan 2489beab59 chore: refactor TurnierDetailScreen and related components, remove unused parameters, centralize date validation logic, implement TurnierStammdatenViewModel, and eliminate reflection dependencies 2026-04-20 10:11:10 +02:00
stefan f8820847fa chore: implement Turnier domain logic, add repository interfaces and default implementations, and disable WASM builds 2026-04-20 09:38:39 +02:00
stefan 345c329350 chore: enhance Stammdaten-Verwaltung and refine desktop UX across multiple features, fix typo in settings.json, enable WASM builds, and add Master-Detail layout for Funktionäre
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-20 02:49:34 +02:00
stefan d4aeba4666 chore: implementiere MsFilePicker-Komponente, ersetze veraltete Input-Felder in Geräteneukonfiguration und ZNS-Importer, verbessere Vereinskarten-Darstellung und Detail-UX, behebe Tippfehler in settings.json 2026-04-20 02:00:36 +02:00
stefan 9fe889b2c1 chore: bereinige unbenutzte Importe in VereinScreens.kt und LogoUploadZone.jvm.kt, verbessere Code-Readability durch konsistenten Importstil 2026-04-20 01:21:18 +02:00
stefan 85ac1cae9c chore: implementiere Logo-Upload-Zone mit Base64-Unterstützung, verbessere Vereinsverwaltung mit kompakten Feldern und nutzerspezifischen Uploadoptionen, optimiere Desktop-UX und Navigation 2026-04-20 01:20:20 +02:00
stefan dfaa2e8545 chore: consolidate redundant controllers in mail-service, improve backend stability, refine desktop UX, and enhance Vereinsverwaltung functionality 2026-04-20 00:21:20 +02:00
stefan bcabb86841 chore: fix DI binding for ZnsImportProvider in zns-import-feature, ensure proper Koin resolution 2026-04-19 22:18:18 +02:00
stefan 189ebc6565 chore: entferne XML-Deklaration aus .idea/misc.xml
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
2026-04-19 21:50:42 +02:00
stefan 83adb4ae07 chore: erweitere Resilience4j-Bundle um Kotlin-Support, aktualisiere PingController um Fallback-Logik, füge Fehlerhandler hinzu, verbessere PingControllerTest, synchronisiere .env und dc-infra.yaml 2026-04-19 21:50:33 +02:00
stefan 54f91c7309 chore: entferne unbenutzten PingEvent Import aus PingSyncIntegrationTest
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
2026-04-19 17:46:48 +02:00
stefan a645bb4dbc chore: migriere ping-feature Modul auf Module Structure Blueprint, füge Fake-Repository und neue Integrationstests hinzu, dokumentiere Änderungen 2026-04-19 17:46:29 +02:00
stefan 691861a706 chore: entferne veraltete turnier-feature Artefakte und ViewModels nach Migration auf Module Structure Blueprint 2026-04-19 17:39:34 +02:00
stefan ef5d4fdc81 chore: migriere meldestelle-web Shell auf Module Structure Blueprint, aktualisiere group, setze version, passe DI-Imports an, dokumentiere Änderungen 2026-04-19 17:36:20 +02:00
stefan afad3c5a02 chore: migriere Artefakte für Module auf Module Structure Blueprint (meldestelle-desktop, sync, network), aktualisiere Ausgabepfade, füge wasmJsMain und jvmMain Outputs hinzu 2026-04-19 17:33:31 +02:00
stefan 512eb730b0 chore: migriere meldestelle-desktop Shell auf Module Structure Blueprint, aktualisiere group, setze version, passe DI-Imports an, dokumentiere Änderungen 2026-04-19 17:33:18 +02:00
stefan 8c1abaebad chore: migriere zns-import-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:24:01 +02:00
stefan 3428261bff chore: migriere verein-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:13:05 +02:00
stefan 34cab61567 chore: migriere veranstaltung-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:12:07 +02:00
stefan 4419e55ee1 chore: migriere veranstalter-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:11:07 +02:00
stefan bd8899a829 chore: migriere turnier-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:10:03 +02:00
stefan 8148ceb318 chore: migriere reiter-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:08:43 +02:00
stefan 58454ec9af chore: migriere profile-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:07:38 +02:00
stefan 2e7078424d chore: migriere ping-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:06:37 +02:00
stefan 56ecee4cba chore: migriere pferde-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:05:27 +02:00
stefan 9222ae7a1c chore: migriere nennung-feature Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:04:04 +02:00
stefan 9578b92e7a chore: migriere funktionaer-feature Modul auf Module Structure Blueprint, passe group an, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:02:34 +02:00
stefan f02e172ff0 chore: migriere device-initialization Modul auf Module Structure Blueprint, aktualisiere group, füge wasmJsMain Dependency hinzu, dokumentiere Änderungen 2026-04-19 17:00:52 +02:00
stefan cef579f91b chore: migriere billing-feature Modul auf Module Structure Blueprint, setze group und version, füge wasmJsMain Dependency hinzu 2026-04-19 16:59:12 +02:00
stefan c8655bfc7f chore: dokumentiere Migration des core/sync Moduls auf Module Structure Blueprint, inkl. group und version Anpassungen 2026-04-19 16:53:16 +02:00
stefan 28a7c5dc44 chore: migriere core/sync Modul auf Module Structure Blueprint, setze group und version, füge wasmJsMain Dependency hinzu 2026-04-19 16:52:08 +02:00
stefan b19f7cadb8 chore: migriere core/network Modul auf Module Structure Blueprint, setze group und version, dokumentiere Änderungen 2026-04-19 16:50:33 +02:00
stefan cb6db36adb chore: migriere core/navigation Modul auf Module Structure Blueprint, aktualisiere group und füge wasmJsMain Dependency hinzu 2026-04-19 16:48:37 +02:00
stefan 0e694341b8 chore: füge Artifact-Konfigurationsdateien für Domain- und LocalDb-Module hinzu 2026-04-19 16:45:56 +02:00
stefan 2ab1840237 chore: setze group und version in Core-LocalDb-Modul, dokumentiere Migration auf Blueprint 2026-04-19 16:45:32 +02:00
stefan 96bdc92723 chore: setze group und version in Core-Domain-Build, füge wasmJsMain Dependency hinzu 2026-04-19 16:42:46 +02:00
stefan cee0a8437f chore: füge fehlende Artifact-Konfigurationsdateien für Projektmodule hinzu 2026-04-19 16:39:50 +02:00
stefan 2b05eebad9 chore: migriere ComponentPreview-Annotation in konsistente Paketstruktur und bereinige referenzierende Importe 2026-04-19 16:39:32 +02:00
stefan 9037b6ce1c chore: entferne veraltete .gitignore-Einträge und nicht genutzte IDE-Konfigurationsdateien 2026-04-19 16:15:59 +02:00
stefan ec861b8f81 chore: entferne DesktopApp und DesktopMainLayout, da diese nicht mehr benötigt werden 2026-04-19 16:07:58 +02:00
stefan 767d78af27 chore: bereinige Import-Anweisungen und entferne nicht genutzten Code in DeviceInitialization-Präsentationsmodulen 2026-04-19 15:59:29 +02:00
stefan 8a3ef98c44 chore: entferne AuthApiClient, AuthTokenManager und DeviceInitializationConfig.jvm, da diese nicht mehr benötigt werden 2026-04-19 15:58:03 +02:00
stefan dc66dfb537 chore: konsolidiere Exception-Handling durch _-Platzhalter, bereinige Import-Anweisungen und entferne nicht genutzten Code
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 58s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m13s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m27s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 1m59s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m50s
2026-04-19 00:54:34 +02:00
stefan ae39eb4637 chore: entferne nicht genutzte NennungsMaske-Komponente, extrahiere AktionsButtonLeiste in separaten Komponentenordner 2026-04-19 00:52:36 +02:00
stefan 64d749be3a chore: entferne nicht genutzte NennungsMaske-Komponente, extrahiere AktionsButtonLeiste in separaten Komponentenordner 2026-04-19 00:52:12 +02:00
stefan 1b20e480f4 feat: verbessere Device-Setup-UX durch präzise Fokus-Navigation, Plug-and-Play-Optimierungen und Logging-Standardisierung
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 1m0s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
2026-04-18 22:29:15 +02:00
stefan c29c8179a1 feat: flexibilisiere JWT-Validierung durch benutzerdefinierte Decoder und verbessere CORS-Konfiguration 2026-04-18 20:40:40 +02:00
stefan 2bd2a26ab9 chore: entferne Backup-Dateimuster *~ aus .gitignore 2026-04-18 20:40:03 +02:00
stefan fb520c6607 chore: entferne nicht mehr genutzten Code und Backup-Dateien aus shared-Modul 2026-04-18 20:39:42 +02:00
stefan bad6f44122 chore: entferne nicht mehr genutzte Backup-Dateien, IDE-Konfigurationen und Kotlin-Build-Skripte 2026-04-18 20:38:57 +02:00
stefan 280debce09 refactor(web): Komplettumstellung auf WASM, Altlasten aus Gradle und Architektur-Tests entfernt
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-18 15:48:35 +02:00
stefan fb77a5065b refactor(device-initialization): Code-Bereinigung, ungenutzte Parameter entfernt und WasmJS-Unterstützung vervollständigt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-18 14:47:39 +02:00
stefan e91b10daa3 JS-spezifische Module und Dateien entfernt, Multiplattform-Targets korrigiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-18 14:16:29 +02:00
stefan 7bbb991e69 refactor(desktop, core): Onboarding zu DeviceInitialization umbenannt, Navigation und Screens angepasst
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-18 11:10:05 +02:00
stefan 315517f03f feat(onboarding): Property isConfigured ergänzt, Build-Fehler behoben
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-18 09:08:42 +02:00
stefan 88983f2b4e feat: verbessere Onboarding-Workflow, verbessere mDNS-Discovery & ZNS-Import
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 1m1s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m29s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m14s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 1m17s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m48s
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-17 22:52:37 +02:00
stefan 8f6044abe3 feat(onboarding, screens): Logging für Screen-Loads ergänzt & Biest-Referenzen entfernt
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>
2026-04-17 13:13:47 +02:00
stefan 8857d52f44 refactor(desktop): Alte Verwaltungsscreens entfernt und Code reduziert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-17 12:26:28 +02:00
stefan 3949ab21db refactor(desktop): V2-Suffixe entfernt und VeranstaltungKomponenten modularisiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-17 11:40:09 +02:00
stefan 0128f98164 feat(desktop, masterdata): ZNS-Sync-Status in Footer hinzugefügt & Consul-Healthcheck stabilisiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-17 09:54:26 +02:00
stefan 4b6a242372 feat: ZNS-Cloud-Sync und manuellen Veranstalter-Button im Wizard hinzugefügt
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 59s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m6s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m10s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 1m13s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m51s
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-17 00:31:40 +02:00
stefan a1194adeac feat: unterstütze Einzeldatei-Import, verbessere Fortschrittsanzeige und Logging im ZNS-Import
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 23:45:23 +02:00
stefan 26b3b193ca feat: Health-Check-Ports und Service-URLs konsolidiert, Consul-Best-Practices umgesetzt
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 21:26:58 +02:00
stefan dd76ad6d14 feat: konsistente Consul-Discovery- und Healthcheck-Konfiguration für alle Dienste implementiert
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 19:34:28 +02:00
stefan cfc412878f feat: füge Wasm/JS-Feature-Toggle hinzu, optimiere Gradle-Build-Zeit durch bedingte Task-Deaktivierung
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 19:18:58 +02:00
stefan 0426d4ee9a feat: vereinheitliche Startup-Logs in allen Backend-Services, verbessere Konsistenz und Diagnosemöglichkeiten
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 18:47:35 +02:00
stefan 8f45544fe1 feat: refactor Health-Check-Probes und Connectivity-Logik, stabilisiere Docker-Services
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 18:47:21 +02:00
stefan edd33c34dc docs(journal): Eintrag zu ZNS-First Enrollment und Onboarding-Evolution ergänzt
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 16:59:30 +02:00
stefan b8bd2744ac feat(onboarding): Netzwerkrollen und automatisches Discovery im Onboarding hinzugefügt
- Unterstützung für Master- und Client-Rollen mit angepasster Konfiguration.
- Automatische Dienstsuche (Discovery) für Clients implementiert.
- Erweiterte UI für Drucker-, Backup- und Rollenspezifische Einstellungen.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 16:57:20 +02:00
stefan b2e6c2427b refactor(core, veranstaltung): Exception-Handling vereinfacht und Delay-Angabe optimiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 16:16:52 +02:00
stefan 3b7abc55a4 feat(zns-import): DAT-Dateisupport hinzugefügt, Fehlerbehebung und UI-Anpassungen
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 16:12:53 +02:00
stefan 29c35c524b feat(zns-import): Healthchecks optimiert und Konsul-Discovery erweitert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 14:57:34 +02:00
stefan f3d5651ab7 refactor(veranstaltung): VeranstaltungVerwaltungV2 in VeranstaltungVerwaltung umbenannt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 14:31:52 +02:00
stefan ba812e230d feat(veranstaltung): ZNS-Import-Assistent hinzugefügt und Workflow verbessert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 14:05:46 +02:00
stefan cb4f2f855c feat(veranstaltung): Wizard für neue Veranstaltung implementiert und ZNS-Light-Integration hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 13:26:48 +02:00
stefan 10f9e82718 docs(adr): ZNS-First Enrollment Pattern und ZNS-Light Strategie dokumentiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 12:41:12 +02:00
stefan eb0fac5989 feat(veranstaltung): UI-Refactoring und Validierung für Veranstaltungsverwaltung hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 12:37:49 +02:00
stefan 82a4a13505 feat(onboarding): Explicit Device Enrollment für Master-Geräte hinzugefügt
- Master-Geräte können erwartete Clients inkl. Name & Rolle definieren.
- Neue Rollen (`RICHTER`, `ZEITNEHMER` etc.) integriert.
- Backend- und Frontend-Validierung erweitert, UI-Komponente für Client-Verwaltung.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-16 10:24:52 +02:00
stefan f98a9075ae feat: erweitere Changelog um Onboarding, UX-Verbesserungen und Fehlerbehebungen, aktualisiere Settings-Datei
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 6m3s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 5m59s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 3m27s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m48s
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 00:45:53 +02:00
stefan 7581f15dfb feat: füge ConnectivityTracker hinzu, erweitere networkModule, aktualisiere DesktopFooterBar mit Gerätestatus und mDNS-Discovery
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-16 00:00:11 +02:00
stefan 67d7b38d79 feat: integriere Live-Daten in NennungsEingangScreen, erweitere NennungRemoteRepository um holeNennungen und markiereAlsGelesen, aktualisiere Port-Konfiguration
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-15 22:59:24 +02:00
stefan 6d631acce6 refactor: entferne toJavaInstant und passe DeviceRepository sowie verwandte Modelle an
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-15 22:28:41 +02:00
stefan 1cefc26be9 feat: Mail-Service-Ports aktualisiert, Consul- und Zipkin-Konfiguration hinzugefügt, neue Felder in BewerbService eingefügt
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
2026-04-15 21:45:20 +02:00
stefan 18e41a90b6 Refactor and rename NennungViewModel to TurnierNennungViewModel, implement online registration workflow with new UI state, ViewModel logic, and API integration, and update dependencies and documentation accordingly. 2026-04-15 20:55:05 +02:00
stefan d026e7f83c Remove unused background import from TurnierOnlineNennungenTab to clean up code. 2026-04-15 20:55:05 +02:00
stefan 26ac3007b9 Implement online registration (Nennung) workflow: add API integration, ViewModel logic, UI updates, backend endpoint, and roadmap adjustments. 2026-04-15 20:55:00 +02:00
stefan a6fcb81594 feat(desktop-onboarding): neue Onboarding-UI implementiert, Backup- und Rollenmanagement hinzugefügt
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 3m10s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m37s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 5m59s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
- Einbindung eines komplett überarbeiteten Onboarding-Screens mit validierten Eingaben für Gerätename, Sicherheitsschlüssel und Backup-Pfad.
- `SettingsManager` eingeführt zur Speicherung der Onboarding-Daten in `settings.json`.
- Navigation verbessert: Onboarding-Workflow startet, wenn Konfiguration fehlt; neues "Setup"-Icon in der Navigationsleiste hinzugefügt.
- Backend: Geräte-API und `DeviceSecurityFilter` für Authentifizierung per Sicherheitsschlüssel implementiert.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-15 15:49:01 +02:00
stefan a5f5e7a24b feat(mail-service): Port-Konflikt behoben, SMTP-Konfig optimiert und dynamisches Plus-Addressing eingefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-15 13:44:34 +02:00
stefan d0b756694b feat(frontend): Struktur und Kommentare verfeinert, Mail-Service-Konfiguration erweitert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-15 11:49:31 +02:00
stefan 8c804832d8 feat(billing): add automatic booking for Sportförderbeitrag in compliance with § 16 ÖTO
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-15 11:17:35 +02:00
stefan c542094196 feat(online-nennung): integrate online nomination workflow via REST and mail service
- Enabled web-to-backend nominations with `MailController` and REST endpoint (`/api/mail/nennung`).
- Added `NennungRemoteRepository` for frontend API integration using Ktor.
- Linked `WebMainScreen` to backend API for nomination handling and confirmation display.
- Implemented automated confirmation emails for received nominations.
- Updated `MASTER_ROADMAP` to reflect progress on Phase 13 milestones.
- Improved Nennung UI, backend persistence, and QA tracking for Neumarkt tournament.
2026-04-15 10:37:12 +02:00
stefan b4c400efea docs(agents): expand playbooks and refine agent collaboration protocols
- Added `Bounded Context Awareness` section to the Architect playbook, emphasizing adherence to SCS boundaries.
- Refined agent definitions and responsibilities, highlighting domain-driven principles and offline-first focus.
- Introduced strategic project goals in AGENTS.md, clarifying the operational scope of Meldestelle-Biest.
- Enhanced workflow and session protocols for better alignment with the MASTER_ROADMAP and DDD principles.
- Updated role descriptions to emphasize tools, technologies, and accountability.
2026-04-15 09:06:09 +02:00
stefan 03f0c3a90b chore(frontend): remove unused imports and update delay syntax in OnlineNennungFormular and NennungsEingangScreen
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-14 17:01:40 +02:00
stefan da3b57a91d feat(mail-service): introduce persistence and REST support for Nennungen
- Added `NennungRepository` with methods for saving, updating status, and retrieving entries.
- Created `NennungController` to expose REST endpoints for Nennungen.
- Defined `NennungTable` schema with relevant fields and indices.
- Extended `MailPollingService` to parse incoming emails into `NennungEntity` and persist them.
- Updated `build.gradle.kts` with database dependencies and H2 configuration for local dev.
- Refined frontend layout in `OnlineNennungFormular` for improved usability and responsiveness.
2026-04-14 16:50:34 +02:00
stefan 4de44623c2 feat(desktop): add NennungsEingang screen and integrate into navigation
- Introduced `NennungsEingangScreen` for managing online nomination entries.
- Added `NennungsEingang` to `AppScreen` with corresponding route configuration.
- Updated `DesktopMainLayout` to include navigation and UI components for `NennungsEingang`.
- Adjusted `PreviewMain` for screen integration and testing.
2026-04-14 15:27:08 +02:00
stefan adfa97978e feat(mail-service): initialize Mail-Service and integrate online nomination workflow
- Created `MailServiceApplication` with Spring Boot setup.
- Added `MailPollingService` for IMAP polling, `TurnierNr` extraction, and auto-reply functionality.
- Implemented structured email sending for online nominations via `OnlineNennungFormular`.
- Updated frontend with `Erfolgsscreen` for nomination confirmation and fallback handling.
- Added build configurations for Mail-Service and frontend nomination module.
- Documented phase-based roadmap for Online-Nennung and Mail-Service rollout.
2026-04-14 14:59:15 +02:00
stefan 5f87eed86a chore(billing-service): remove unused Duration.hours import from TagesabschlussService 2026-04-14 13:11:46 +02:00
stefan cfe12e4dd0 feat(billing): implement support for Tagesabschluss and Buchung cancellations
- Added `Tagesabschluss` entity and repository to handle daily cash closing logic.
- Introduced cancellation logic for `Buchung`, enabling creation of offsetting entries.
- Extended schema definitions with `TagesabschlussTable` and nullable `storniertBuchungId` in `BuchungTable`.
- Updated services to support `Tagesabschluss` creation and `Buchung` cancellation.
- Implemented tests for `TagesabschlussService` and cancellation functionality.
- Updated documentation to reflect completed roadmap items related to cash management.
2026-04-14 13:10:57 +02:00
stefan 2a1508c6a5 chore(tests): standardize schema usage with constants and resolve IDE warnings
- Replaced hardcoded schema names with constants (`TEST_SCHEMA`, `CONTROL_SCHEMA`) across multiple tests.
- Resolved IDE warnings by removing unused variables (`result`), suppressing `SqlResolve`, and using ASCII-compliant strings.
- Corrected typos in test data (`testdb` -> `test_db`, `Produktions` -> `Production`).
- Improved readability and maintainability in migration and tenant registry tests by introducing companion object constants.
2026-04-14 12:53:33 +02:00
stefan a15cc5971f chore(tests+config): enhance EntriesIsolationIntegrationTest and add missing Spring metadata
- Improved schema isolation logic with constants for tenant schemas and search path management in PostgreSQL.
- Added `withTenant` utility in `TenantContextHolder` to simplify tenant context usage.
- Removed unused imports, variables, and helper functions (`random()` and redundant `NennungRepository` references).
- Included missing `multitenancy.*` configuration keys in `additional-spring-configuration-metadata.json` to address IDE warnings.
2026-04-14 12:39:56 +02:00
stefan f961b6e771 chore(docs+tests): reactivate EntriesIsolationIntegrationTest and resolve tenant data isolation issues
- Fixed schema isolation handling in Exposed by switching table creation to JDBC and explicitly setting `search_path` in PostgreSQL.
- Removed redundant `runBlocking` calls, unused variables, and IDE warnings in the test.
- Added `JwtDecoder` mock in `@TestConfiguration` to prevent application context loading errors.
- Verified that writes in one tenant schema are no longer visible in another.

chore(config): add `application-test.yaml` for better test environment setup

- Configured H2 as an in-memory database for tests.
- Disabled Flyway and Consul to avoid unnecessary dependencies during testing.
2026-04-14 12:25:27 +02:00
stefan 7e3a5aa49e Set static health-check-port to 8086 in application.yml for consistent configuration.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
2026-04-13 23:30:45 +02:00
stefan bef09791ae Update series-service startup configuration: change default port to 8090, improve Docker stability with prefer-ip-address, and document fixes in curator log. 2026-04-13 23:27:57 +02:00
stefan 2ee9ccf8e9 Add startup fix for scheduling-service: configure application.yml, set service port to 8089, include spring-boot-starter-actuator for health checks, and document changes. 2026-04-13 23:25:35 +02:00
stefan d4509d6c5a Make health-check-port configurable using SERVER_PORT environment variable in application.yml. 2026-04-13 23:19:53 +02:00
stefan 19934e2a96 Add service discovery and health fixes: configure Consul registration, update health-check paths and ports, expand scanBasePackages, and ensure consistent service startup across modules. 2026-04-13 23:18:08 +02:00
stefan 8e40d13954 Add missing spring.datasource configuration to entries-service, comment out conflicting index creation in Flyway migration script, and update log documentation for startup fix. 2026-04-13 22:47:22 +02:00
stefan 43a98ec9ef Set SPRING_APPLICATION_NAME for billing-service in Docker Compose to fix Consul registration issues and add stability log documentation. 2026-04-13 22:29:00 +02:00
stefan 8d0d8898cb Organize imports and clean up exceptions: remove unused imports across multiple modules, replace exception variable usage with wildcard, and improve code readability. 2026-04-13 22:03:46 +02:00
stefan fb1c1ee4ce Remove domain models and services related to Abteilung, AbteilungsRegelService, and Bewerb: cleanup unnecessary entities, validation logic, and tests across backend modules. 2026-04-13 21:58:25 +02:00
stefan 76d7019d30 Add PDF invoice generation: implement backend API, introduce PdfService, update frontend repository and UI with download logic, and mark roadmap task complete. 2026-04-13 17:18:50 +02:00
stefan 9b9c068e7f chore(frontend): opt in to ExperimentalWasmDsl in core modules
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
2026-04-13 14:42:42 +02:00
stefan f719764914 chore(turnier-feature): remove unused ViewModels and UI components
- Removed `AbteilungViewModel`, `BewerbAnlegenViewModel`, `BewerbViewModel`, and `CreateBewerbWizardScreen`.
- Cleaned up related imports and unused domain models.
2026-04-13 14:38:16 +02:00
stefan 5c7ba28b1e Mark C-2 "Design-System konsolidieren" as complete: unify MaterialTheme color palette, define typography scale and spacings, optimize MsTextField for desktop standards, and update related documentation and logs.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
2026-04-12 23:13:13 +02:00
stefan a2efe8a7f6 Organize imports across multiple modules: remove unused dependencies and consolidate key input-related imports for better readability and maintenance. 2026-04-12 23:09:03 +02:00
stefan 126522e606 Refine MsTextField component: introduce compact mode, enhance visual styling and error handling, and improve placeholder and keyboard interaction logic. Add Dimens and Colors updates, implement navigation rail and header layout for the desktop shell, and update ROADMAP documentation with planned phases. 2026-04-12 23:07:12 +02:00
stefan 5eb2dd6904 Remove outdated BillingController implementation, resolve conflicting bean definitions across modules, and retain the updated BillingController for consistency with frontend API logic. 2026-04-12 21:51:52 +02:00
stefan 9754f3e36b Enhance billing logic: add REST support for manual and automated transactions, refine billing routes, adapt frontend API integration, and implement transaction type validation. 2026-04-12 18:35:52 +02:00
stefan 03950f8b0c Update billing-service and series-service: refine frontend API integration, stabilize JPA entities, add Flyway migrations, and enhance roadmap documentation. 2026-04-12 18:09:24 +02:00
stefan 0f2060fc14 Integrate billing-service microservice: add API gateway routing, service discovery with Consul, Docker support, and Spring configuration. Update frontend with API integration, BillingRepository, and BillingViewModel. 2026-04-12 18:00:43 +02:00
stefan 11abbf0179 Add explicit @Column and @Table annotations for Serie and SeriePunkt entities to align with SQL schema, include @Id annotations in JPA entities, and resolve schema mapping warnings. 2026-04-12 17:48:05 +02:00
stefan 5b207a2b9d Convert Serie and SeriePunkt from data class to regular class, implement manual copy, equals, hashCode, and toString methods for JPA compliance, adjust column mappings, and add Flyway migration for database schema creation. 2026-04-12 17:43:48 +02:00
stefan 62aaf6100e Fix backend infrastructure issues for results-service, series-service, and events-service; integrate @EnableDiscoveryClient and Consul support; convert Series domain entities to data class; and update Gradle dependencies and configurations. 2026-04-12 17:39:26 +02:00
stefan c380537520 Update MASTER_ROADMAP.md: adjust billing and series context statuses, detail Phases 11–13, and refine phase descriptions. 2026-04-12 17:09:01 +02:00
stefan a79e612693 Implement ranking logic with SerieStandEntry, add support for streak results and binding types (Reiter+Pferd, Reiter, Pferd), update UI for detailed ranking display, and finalize Phase 10. 2026-04-12 17:03:11 +02:00
stefan 6e99bc97fd Integrate series-service microservice with API gateway routing, implement Series domain and point aggregation logic, and update frontend with SeriesViewModel, SeriesScreen, and dynamic state handling. 2026-04-12 16:58:29 +02:00
stefan 4ad9b274e8 Add Platzierungsberechnung and PDF-Export functionality to ErgebnisRepository, update BewerbViewModel for new actions, and enhance TurnierErgebnislistenTab with dynamic UI elements. 2026-04-12 16:49:13 +02:00
stefan 9c520d1b71 Add results-service microservice with API gateway integration, implement Ergebnis repository and edit dialog, update BewerbViewModel for Ergebniserfassung, and enhance Turnier UI with result management features. 2026-04-12 16:37:14 +02:00
stefan eb06c85013 Add microservices for masterdata, events, and ZNS import; configure API gateway routes; implement real Turnier and Verein repository integrations; and update infrastructure, frontend, and documentation. 2026-04-12 16:21:10 +02:00
stefan b07d5d7386 Enhance edit dialogs with validation and error handling, implement dropdown searches for Funktionäre, and update Masterdata logs with completion notes. 2026-04-12 16:00:33 +02:00
stefan f82d06f3e7 Add Reiter and Pferd edit dialogs, extend Masterdata repository with save and fetch methods, and integrate editors into Nennungen tab UI. Fix DI configuration and update previews. 2026-04-12 15:56:09 +02:00
stefan 4ca25b6417 Integrate Stammdaten and Nennungen features: implement repositories, ViewModels, and full UI integration for Reiter, Pferde, Funktionäre, and Vereine. Expand ApiRoutes, enhance Nennung tab with search and real data, and update CHANGES.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
2026-04-11 22:21:53 +02:00
stefan 2d6ff49629 Remove unused imports and update type references for improved code readability and maintenance.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-11 22:02:50 +02:00
stefan 15b3f17d1d Integrate Nennungen and Masterdata features: expand ApiRoutes, add repositories and ViewModels for Nennungen and Masterdata. Update navigation and UI components to include Meisterschaften and Cups tabs. 2026-04-11 21:58:55 +02:00
stefan edfbbb805f Mark Phase 9 as complete: finalize Zeitplan-Optimierung, add audit logging for Bewerb modifications, implement ZNS B-Satz export, and enhance Zeitplan tab with drag-and-drop scheduling and conflict validation. 2026-04-11 21:27:00 +02:00
stefan 92aecf9abf Add audit logging for Bewerb changes, implement ZNS B-Satz export, enhance Zeitplan tab with audit log display, export dialog, and clickable Bewerb items, and integrate FixedWidthLineBuilder utility. 2026-04-11 21:23:38 +02:00
stefan d224e2c521 Remove unused imports in CompetitionWarningService and AuditLogTable. 2026-04-11 21:01:34 +02:00
stefan 3515d40fcb Add audit logging for Zeitplan updates, implement conflict validation for overlapping schedules and judge assignments, and enhance frontend with detailed warning visualizations in Zeitplan tab. 2026-04-11 21:00:18 +02:00
stefan bc46054412 Add Zeitplan fields to domain and DTO models, implement UpdateZeitplan intent and API integration, and update ViewModel for Zeitplan state consistency. 2026-04-11 20:42:39 +02:00
stefan 52bc8f3fbe Implement "Zeitplan" feature in tournament details: add TurnierZeitplanTab.kt, update navigation, and integrate visual scheduling with drag-and-drop support. Relocate Detekt configuration. 2026-04-11 20:37:28 +02:00
stefan b91d1953a4 refactor(desktop-layout): remove unused FakeVeranstalterStore import
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-11 13:30:12 +02:00
stefan ccefcd4588 Remove veranstalter-feature (repositories, UI components, and domain models). 2026-04-11 13:29:08 +02:00
stefan eda18a8ff2 Remove nennung-feature (domain models, DI modules, and UI components). 2026-04-11 13:04:23 +02:00
stefan 84128432e3 docs(architecture): add specification for status automaton and time schedule synchronization logic
- Added conceptual documentation detailing the status automaton for handling entry states and its integration with dynamic time schedule adjustments (`status-automat-nennungen-de.md`).
- Updated master roadmap with the completion of the status automaton concept.
- Extended changelog to reflect the addition of the specified architecture document.
2026-04-11 12:28:14 +02:00
stefan 7480aed4d1 refactor(entries-service): clean up unused imports and adjust isTurnierPublished visibility 2026-04-11 12:23:53 +02:00
stefan 0aa1a1b9b7 feat(entries+time-scheduling): add support for automatic breaks and inspection type configurations
- **Domain Enhancements:**
  - Introduced `PausenKonfiguration` and `BesichtigungsBlock` entities to handle automatic breaks and inspection scheduling.
  - Added `BesichtigungsTypE` enum for inspection types (`ZU_FUSS`, `ZU_PFERD`).
  - Updated `Bewerb` and `Abteilung` models to include pause and inspection type fields.

- **Service Updates:**
  - Enhanced `StartlistenService` to calculate start times, accounting for breaks and inspection buffers.
  - Extended `BewerbService` to support patchable time scheduling via new `updateZeitplan` API.

- **Persistence Changes:**
  - Updated tables (`BewerbTable`, `AbteilungTable`) to persist break configurations and inspection types.
  - Implemented repository mappings to include these new fields.

- **Testing:**
  - Introduced `BewerbeZeitplanIntegrationTest` to validate new scheduling behaviors, including automatic pauses and inspection handling.

- **Documentation:**
  - Added rulebook and conceptual documents for inspection and scheduling logic in `docs/01_Architecture/`.
2026-04-11 12:21:42 +02:00
stefan 97ed8ad20a refactor(tests): clean up unused imports in EntriesIsolationIntegrationTest
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
2026-04-10 14:42:52 +02:00
stefan 1ba4845f6c feat(frontend+billing): integrate billing UI and navigation into Turnier module
- **Navigation Updates:**
  - Added `AppScreen.Billing` route for participant billing linked to an event and tournament.

- **UI Additions:**
  - Introduced `BillingScreen` and `BillingViewModel` for participant account management and manual transactions.
  - Updated `TurnierAbrechnungTab` to include `BillingScreen` and enable account interaction.

- **Turnier Enhancements:**
  - Enhanced `NennungenTabContent` to support navigation to billing via a new interaction.
  - Added billing feature as a dependency to `turnier-feature`.

- **Billing Domain:**
  - Extended `Money` to include subtraction operation and improved formatting for negative amounts.
  - Added DTOs (`TeilnehmerKontoDto`, `BuchungDto`, `BuchungRequest`) for seamless data exchange with backend.

- **Test Improvements:**
  - Updated `PreviewTurnierAbrechnungTab` to include interactive billing placeholder.

- **Misc Updates:**
  - Enhanced breadcrumb navigation for billing in `DesktopMainLayout` for better user experience.
2026-04-10 14:30:54 +02:00
stefan a7e1872d10 Update roadmaps to reflect completion of billing, entries integration, and ZNS importer tasks. 2026-04-10 13:05:32 +02:00
stefan 02c6da146e refactor(billing+entries): remove unused import and adjust tenant transaction signature 2026-04-10 12:57:30 +02:00
stefan c1fadac944 feat(entries+billing): integrate automatic fee booking for entries with billing service
- **Entries-Service Updates:**
  - Implemented automatic booking of fees (entry fees and late fees) during entry submission using `TeilnehmerKontoService`.
  - Enhanced `Bewerb` entity with financial fields (`nenngeldCent`, `nachnenngebuehrCent`).
  - Added Flyway migration to update `bewerbe` table with new financial fields.
  - Updated `EntriesServiceApplication` to include billing package scanning for integration.

- **Billing-Service Enhancements:**
  - Adjusted `TeilnehmerKontoService` to support fetching accounts by event and person.
  - Improved database configuration to handle missing JDBC URLs during tests.

- **Tests:**
  - Added integration tests to validate fee booking logic for entries, including late fee scenarios.
  - Introduced H2 database setup for test isolation.

- **Misc:**
  - Updated tenant-aware transactions to support H2 and PostgreSQL dialects.
  - Adjusted log and error handling for robust integration between services.
2026-04-10 12:49:03 +02:00
stefan eef17b3067 feat(billing): implement REST API, database config, and tests for billing service
- **REST API:** Added `BillingController` with endpoints for managing participant accounts and transactions, including history retrieval.
- **Database Configuration:** Introduced `BillingDatabaseConfiguration` to initialize database schema using Exposed.
- **Testing:** Added integration tests for `TeilnehmerKontoService` using H2 in-memory database.
2026-04-10 12:27:02 +02:00
stefan 21f3a57e6e feat(billing): introduce billing domain and service infrastructure
- **Billing Domain:**
  - Added Kotlin Multiplatform project with domain models (`TeilnehmerKonto`, `Buchung`, `BuchungsTyp`) to represent billing entities.
  - Defined serialization strategies using `InstantSerializer`.

- **Service Implementation:**
  - Introduced `BillingServiceApplication` as the main entry point for the billing service.
  - Developed `TeilnehmerKontoService` for account management and transactions.

- **Persistence Layer:**
  - Implemented Exposed repositories (`ExposedTeilnehmerKontoRepository`, `ExposedBillingRepositories`) for database interaction.
  - Added table definitions (`TeilnehmerKontoTable`, `BuchungTable`) with indexes for efficient querying.

- **Build Configuration:**
  - Setup Gradle build files for billing domain and service modules with dependencies for Kotlin, Serialization, Spring Boot, and Exposed.

- **Test Additions:**
  - Extended ZNS importer tests with new scenarios for qualification parsing
2026-04-10 12:18:03 +02:00
stefan bab95d14f4 feat(frontend): update tooltip positioning in TurnierBewerbeTab for improved UI clarity
- Refined `TooltipBox` in `TurnierBewerbeTab` to use `TooltipAnchorPosition.Above`, enhancing tooltip visibility and alignment for warnings.
2026-04-10 11:44:17 +02:00
stefan e7d7e43ccf feat(domain+frontend): implement structured division warnings and enhance validation rules
- **Domain Updates:**
  - Introduced `AbteilungsWarnung` entity for structured warning handling compliant with ÖTO § 39.
  - Added validation rules for mandatory and optional division thresholds and structural completeness.
  - Implemented `CompetitionWarningService` and `AbteilungsRegelService` for domain-centric validations.
  - Updated domain models (`Bewerb`, `Abteilung`) to reflect structured warning logic.

- **Services:**
  - Expanded `BewerbService` to include warning validation through `CompetitionWarningService`.

- **Frontend Enhancements:**
  - Updated `TurnierBewerbeTab` to display warnings using tooltips with clear descriptions and structured formatting.
  - Modified `BewerbUiModel` to handle warnings and integrate them into the UI.

- **Persistence:**
  - Implemented `CompetitionRepositoryImpl` to map database rows to the new domain models and validation logic.

- **Testing:**
  - Added comprehensive unit tests for `validateStrukturellesTeilung` and division-specific warnings.
  - Enhanced existing tests to validate the new warning structure and code-based assertions.

- **Docs:**
  - Updated roadmap to reflect the completion of structural warnings implementation.
2026-04-10 11:37:34 +02:00
stefan 22c631ec43 feat(core+frontend): enhance SyncEvent model and integrate sync handling in BewerbViewModel
- **Core Updates:**
  - Expanded `SyncEvent` model with additional fields (`eventId`, `sequenceNumber`, `originNodeId`, `createdAt`, `checksum`, `schemaVersion`) for improved event tracking and validation.
  - Updated event classes (`PingEvent`, `PongEvent`, `DataChangedEvent`, `DataRequestEvent`) to align with the extended `SyncEvent`.

- **Frontend Enhancements:**
  - Enhanced `BewerbViewModel` to handle sync events (`PingEvent`, `DataChangedEvent`) and observe connected peers using `SyncManager`.
  - Added support for
2026-04-10 11:09:33 +02:00
stefan 0d75c9b664 refactor(core+frontend): remove unused imports and improve coroutine syntax consistency
- Removed unnecessary imports across multiple modules for cleaner code.
- Updated `kotlinx.coroutines.delay` to use `Duration.milliseconds` for improved readability and type safety in `SyncManager`.
2026-04-10 10:58:45 +02:00
stefan 8726129b96 feat(core+frontend): add P2P sync infrastructure with WebSocket support
- **Core Updates:**
  - Implemented `P2pSyncService` interface with platform-specific WebSocket implementations (`JvmP2pSyncService` and no-op for JS).
  - Developed `SyncEvent` sealed class hierarchy to handle peer synchronization events (e.g., `PingEvent`, `PongEvent`, `DataChangedEvent`, etc.).

- **Frontend Integration:**
  - Introduced `SyncManager` to manage peer discovery and synchronization, coupled with `NetworkDiscoveryService`.
  - Updated dependency injection to include `syncModule` for platform-specific sync service initialization.
  - Enhanced `BewerbViewModel` to support new sync capabilities, including observing sync events and UI updates for connected peers.

- **Backend Enhancements:**
  - Added ZNS-specific fields (`zns_nummer`, `zns_abteilung`) to Bewerb table for idempotent imports.
  - Introduced import ZNS logic to handle duplicates and align with SyncManager updates.

- **UI Improvements:**
  - Enhanced `TurnierBewerbeTab` with updated dialogs (ZNS imports, sync status) and dynamic previews.
  - Improved network syncing feedback and error handling in frontend components.

- **DB Changes:**
  - Added migration for new column fields in the Bewerb table with relevant indexing for ZNS import optimizations.
2026-04-10 10:55:00 +02:00
stefan 6b6965bbbb refactor(frontend): use Duration.milliseconds for delay in BewerbeTab coroutine loop 2026-04-10 10:29:37 +02:00
stefan 721d991c5e 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.
2026-04-10 10:27:20 +02:00
stefan c06eb79cba feat(frontend+domain): add start list repository, enhance Bewerb model, and update view models
- **StartlistenRepository:**
  - Introduced a new repository for generating and retrieving start lists, with `DefaultStartlistenRepository` implementation for remote API integration.

- **Bewerb Enhancements:**
  - Updated `Bewerb` and `BewerbDto` models to include additional details (e.g., `tag`, `platz`, `sparte`, etc.).
  - Adjusted mappers to align with model updates.

- **ViewModel Updates:**
  - Extended `BewerbViewModel` to integrate with `StartlistenRepository` for start list generation and preview.
  - Refactored loading logic in `BewerbViewModel` to display errors and handle repository responses properly.

- **UI Enhancements:**
  - Improved start list preview layout in `TurnierBewerbeTab` with additional styling and dynamic fields.
  - Added buttons to confirm or cancel start list changes in the preview modal.

- **Dependency Injection:**
  - Registered `DefaultStartlistenRepository` in the `TurnierFeatureModule` and updated `BewerbViewModel` factory.
2026-04-10 10:10:46 +02:00
stefan fbed4d34cc refactor(frontend): simplify imports and update syntax for wizard steps and delay durations
- Consolidated `material3` imports in `CreateBewerbWizardScreen` and `TurnierBewerbeTab` for cleaner code.
- Switched `WizardStep.values()` to `WizardStep.entries.toTypedArray()` for improved readability.
- Changed `kotlinx.coroutines.delay` argument to use `Duration.milliseconds` for enhanced clarity and type safety.
2026-04-10 10:04:25 +02:00
stefan 363aa80fe4 feat(core+frontend+domain): add ZNS Bewerb parser and integrate start list feature
- **Parser Implementation:**
  - Introduced `ZnsBewerbParser` to parse n2-XXXXX.dat files and map B-Satz lines to the `ZnsBewerb` domain model.
  - Added test coverage for parsing B-Satz lines and edge cases in `ZnsParserTest`.

- **Frontend Integration:**
  - Integrated ZNS import functionality into the `BewerbeTabContent` for uploading and previewing Bewerb data before import.
  - Enhanced `BewerbViewModel` with state and intents for managing ZNS import, preview dialogs, and import confirmation.
  - Supported start list generation and added modal for previewing generated start lists.

- **Domain Services:**
  - Implemented `StartlistenService` to generate and calculate start times for start lists with respect to participant preferences.
  - Added extensive test coverage in `StartlistenServiceTest` to validate sorting, preferences, and time calculations.

- **UI Enhancements:**
  - Updated `Bewerbe` tab layout with search, filtering, and action buttons for ZNS import and start list generation.
  - Introduced dialogs for ZNS import previews and start list previews.
2026-04-10 09:59:31 +02:00
stefan a3007b01ee Add archived screenshots documentation and migrate new assets to docs/80_Assets/. Include system mockups, tournament files, and dashboard visuals for Neumarkt 2026.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
2026-04-09 22:10:55 +02:00
stefan ffe73b9061 Remove obsolete chat logs and migrate relevant assets from docs/Neumarkt2026/ and docs/BilderSuDo/ to docs/80_Assets/. Update references and initiate consolidation of documentation. 2026-04-09 22:02:14 +02:00
stefan 452c50c31b docs(frontend): add navigation and flow diagrams for wizard steps
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
- Created `Navigation_Wizard_Flows.drawio` to map the Navigation Rail and 3-step wizard process (Veranstaltung → Turnier → Bewerbe) with detailed routes and guards.
- Introduced `_mapping_alt-zu-neu.md` to document screenshot renaming and restructuring for enhanced organization.
- Refined folder layout in `docs/06_Frontend/Screenshots/` for flows and components.
2026-04-09 15:52:44 +02:00
stefan 4d6a1d5f16 docs(c4): migrate architecture diagrams to draw.io and refine container/workflow visualizations
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
- Replaced outdated `.puml` and `.mermaid` diagrams with modernized `.drawio` versions for enhanced clarity.
- Added `container_diagram.drawio` to depict offline-first architecture and backend synchronization workflow.
- Introduced `workflow_turnieranlage.drawio` to visualize the tournament creation process (3-step wizard).
- Archived legacy diagrams under `docs/01_Architecture/_archive` for reference.
2026-04-09 14:15:31 +02:00
stefan 276e3cc3dd docs(c4): migrate architecture diagrams to draw.io and refine container/workflow visualizations
- Replaced outdated `.puml` and `.mermaid` diagrams with modernized `.drawio` versions for enhanced clarity.
- Added `container_diagram.drawio` to depict offline-first architecture and backend synchronization workflow.
- Introduced `workflow_turnieranlage.drawio` to visualize the tournament creation process (3-step wizard).
- Archived legacy diagrams under `docs/01_Architecture/_archive` for reference.
2026-04-09 11:32:06 +02:00
stefan c11bffef22 feat(docs+domain): refine event and tournament models, align with updated domain terminology
- **Documentation:**
  - Added `2026-04-09_Session_Log_Turnier_Veranstaltung_Domain_Alignment.md` to document validation and updates to `Veranstaltung` and `Turnier` domain models.
  - Updated `Overview.md` to reflect new terminology and added attributes for `Veranstaltung` (`Austragungsplätze`, `Artikel-Preisliste`) and `Turnier`
(`Turnierbeauftragter`, updated `Reglement`, `Nennschluss`, `Nachnenngebühr`, and `Nenntauschbörse`).

- **Domain Updates:**
  - Adjusted `Turnier` model: renamed `richterObmannId` → `turnierbeauftragterId`, added missing fields (`nennschluss`,
2026-04-09 10:09:21 +02:00
stefan f8662e973e docs: add class diagram for core entities in C4 architecture
- Created `class_diagram_core.drawio` to visualize core domain entities (`Veranstaltung`, `Platz`, `Turnier`, etc.).
- Included relationships and associations between entities such as 1:N and reference mappings.
- Structured diagram to align with Clean Architecture principles for better domain comprehension.
2026-04-09 09:24:35 +02:00
stefan d7095bef47 Document findings and planned corrections from architecture review: outline five key gaps in the domain models (Veranstaltung, Turnier) and propose updates to align with business requirements.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-08 23:53:12 +02:00
stefan 6ba6192684 Rename DomBewerbTest to BewerbTest to align with updated domain model naming conventions. 2026-04-08 23:52:59 +02:00
stefan ee520073f0 Standardize exception naming in VeranstaltungController: replace unused exception variable names with _ for cleaner and more concise code. 2026-04-08 23:01:18 +02:00
stefan f2dff2a4d8 Remove Dom prefix from domain models: rename classes to reflect business terminology, update references in tests, repositories, and services; add ADR for new naming conventions. 2026-04-08 23:00:52 +02:00
stefan e83b09fd63 Refactor events-service Gradle build: streamline plugin and dependency declarations, resolve naming inconsistencies, and update module references for improved maintainability. 2026-04-08 23:00:36 +02:00
stefan 5d1c89438c Migrate UUID handling in VeranstaltungRepositoryImpl: standardize conversion functions, update methods for entity mapping, and refactor queries for enhanced consistency and type safety. 2026-04-08 23:00:23 +02:00
stefan bb4b5924d1 Extend Bewerb table and add BewerbRichterEinsatz table: introduce new properties, indexes, and mappings to support richer domain model and scheduling capabilities. 2026-04-08 23:00:16 +02:00
stefan 84403287a1 Extend Bewerb table and add BewerbRichterEinsatz table: introduce new properties, indexes, and mappings to support richer domain model and scheduling capabilities. 2026-04-08 23:00:03 +02:00
stefan e4f22096ed Extend Bewerb repository and service: add RichterEinsatz handling, enhance property mapping, and align DTOs with updated domain model. 2026-04-08 22:59:38 +02:00
stefan 8b6ea11d46 Extend Bewerb DTOs and APIs: add comprehensive properties, mapping functions, and RichterEinsatz support, and align backend and frontend implementations. 2026-04-08 22:59:26 +02:00
stefan 2d42578378 Remove Dom prefix from domain models: delete DomVeranstaltung, rename models like DomNennung to Nennung, update references in repositories, services, and tests for consistency. 2026-04-08 22:59:15 +02:00
stefan 085656a85b Extend Bewerb domain model: add new properties (e.g., Beschreibung, Aufgabe, BeginnZeitTyp), update Enums, and align repository structures. 2026-04-08 22:58:58 +02:00
stefan d91d88855e Add Bewerb extensions and related infrastructure: introduce V5 and V6 migrations with new columns, constraints, indexes, and tables (e.g., Austragungsplatz, RichterEinsätze, TurnierArtikel) to support extended domain models and reporting capabilities. 2026-04-08 22:58:40 +02:00
stefan da7afec9d7 Add domain models: Austragungsplatz, TurnierArtikel, and RichterEinsatz. 2026-04-08 22:58:26 +02:00
stefan 8bc6f8e1df Register events modules in Gradle build and refactor VeranstaltungController: remove unused use cases, streamline request handling, and improve error responses. 2026-04-08 22:56:21 +02:00
stefan df8bce4277 Add new chat session and extend Gemini conversation: document architecture discussions, domain model updates, Dom prefix removal plan, and phase-based roadmap for Bewerb and related entities. 2026-04-08 20:17:26 +02:00
stefan 2e4cb6d042 Enhance ZNS import documentation: add detailed steps for database initialization, parser validation, import strategy optimization, and frontend integration. 2026-04-08 20:17:20 +02:00
stefan 3ce085ea18 docs: add class diagram for core entities in C4 architecture
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
- Created `class_diagram_core.drawio` to visualize core domain entities (`Veranstaltung`, `Platz`, `Turnier`, etc.).
- Included relationships and associations between entities such as 1:N and reference mappings.
- Structured diagram to align with Clean Architecture principles for better domain comprehension.
2026-04-08 14:39:06 +02:00
stefan 8f0640810b docs: add C4 model for renamed and extended domain components
- Introduced `Kernentitaeten_Umbenennung.puml` to document the planned renaming and structural updates to core domain entities.
- Visualized key contexts (`Event Management`, `Billing`, `Competition`, `Registration`, `Actor`) and their relationships.
- Included recommendations for simplification of naming conventions in Clean Architecture.
2026-04-08 14:12:07 +02:00
stefan 7d8596570a docs: add session notes on hierarchy visualization and class naming discussion
- Logged additional ideas in `Chat-Verlauf-Gemini_2026-04-07.md` regarding visualizing hierarchy, inheritance, and class structure.
- Documented discussion on reevaluating "Dom..." class prefixes and planning for implementation steps.
2026-04-08 14:11:45 +02:00
stefan 31eb8f083d docs: improve formatting and structure for "Chat-Verlauf" logs
- Standardized section headers in `Chat-Verlauf-Gemini_2026-04-07.md` for better clarity.
- Adjusted markdown formatting for participant labels ("Ich", "Gemini") to ensure consistent separation and readability.
- Leveraged markdown lists to enhance the readability of ÖTO § 3 guidelines and constraints.
- Resolved minor inconsistencies in indentation to ensure visual alignment across all sections.
2026-04-08 13:10:37 +02:00
stefan 9ff543dc14 feat(domain+infra+docs): define and integrate essential tournament foundation components
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
- **Domain Enhancements:**
  - Introduced `TurnierNummer` (mandatory, 5 digits) field in `DomTurnier` to establish unique tournament identification.
  - Added `TeilnehmerKreisE` enum representing participation constraints defined in ÖTO § 3 (e.g., J, P, H).
  - Extended `DomTurnier` model with `einschraenkungen` field to store applicable participant restrictions.

- **Infrastructure Updates:**
  - Updated `TurnierTable` to include `turnier_nummer` (varchar) and `einschraenkungen` (text/JSON).
  - Created Flyway migration to add new columns to `turniere` table and backfill existing data.
  - Enhanced repository methods (`TurnierRepositoryImpl`) to handle the new fields.

- **Validation and API Expansion:**
  - Added validation for mandatory 5-digit format of `TurnierNummer` and ensured `einschraenkungen` values conform to the ÖTO spec.
  - Updated relevant APIs (`CreateTurnierUseCase`, `TurniereController`) to accept and process new fields.

- **Documentation:**
  - Updated the glossary in `Ubiquitous_Language.md` to include new concepts (`TurnierNummer` and participation constraints).

This enhancement lays the groundwork for ÖTO-compliant tournament management with precise scope and constraint controls.
2026-04-07 19:29:03 +02:00
stefan 92f22faf2f docs: enhance glossary formatting for consistency and readability
- Standardized table layouts by aligning column headers and content across all alphabetical sections.
- Improved text organization and adjusted spacing for better readability.
- Retained all existing references and definitions without modifications to their meaning.
2026-04-07 19:28:38 +02:00
stefan 6b9177e818 feat(db+domain): add turniernummer and einschraenkungen fields for tournament scope and constraints
- **Database Changes:** Introduced `turnier_nummer` (mandatory, 5 digits) and `einschraenkungen` (mandatory, ÖTO-specific constraints) columns in `turniere` table. Seeded `turnier_nummer` with `oeps_turniernummer` where applicable.
- **Domain Models:** Extended `Turnier` and `DomTurnier` with `turnierNummer` and `einschraenkungen` fields. Added `TeilnehmerKreisE` enum for mapping restriction types.
- **Services and Controllers:** Updated repository and service operations to handle the new fields. Controllers reflect the new request models for creation and updates.
- **Validation:** Enforced input validation for `turnierNummer` format and `einschraenkungen` values.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-07 15:07:25 +02:00
stefan 7bf89c58d3 Refactor license matrix and tokenizer logic: rename LicenseTable to LizenzTable, replace LicenseMatrixService with LizenzMatrixService, enhance tokenizer with normalized and fallback token handling, improve ZNS import for license extraction, and update related documentation.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
2026-04-06 23:52:06 +02:00
stefan b7fa2d26a9 Refactor domain models and repositories: align imports, improve formatting consistency, remove unused imports, and harmonize implementation for better ZNS import compatibility. 2026-04-06 19:57:43 +02:00
stefan abaaeddaaf Standardize and refactor master data infrastructure: rename tables for plural consistency, remove unused entity tables, improve ZNS import mappings with enriched license properties, introduce Altersklasse domain model, activate Consul service discovery, and update application configuration accordingly. 2026-04-06 19:50:37 +02:00
stefan bc13a58a14 Remove unused imports in BundeslandRepository and BundeslandExposedRepository. 2026-04-06 16:52:34 +02:00
stefan 1b6f8e7c59 Refactor BundeslandRepository, implement V012 migration for Reiter master data changes, harmonize domain models, resolve repository interface inconsistencies, and enhance ZNS import with relational mapping improvements. 2026-04-06 16:51:18 +02:00
stefan 0ae9a1f1b8 Refactor master data infrastructure to streamline Reiter and Bundesland relationships, add V012 migration, harmonize domain models, implement repository methods for enhanced ZNS import logic, and update associated tests. 2026-04-06 16:39:09 +02:00
stefan 1a6f2ea7ad Remove deprecated ZnsLegacyParsers, update MASTER_ROADMAP to reflect cleanup of parser remnants, stabilize ZNS import tests, and improve ZnsImportService with refined ZipInputStream management and enhanced functionary-reiter matching logic. 2026-04-06 14:41:50 +02:00
stefan e219116609 Make V010 migration idempotent: wrap verein.name to verein_name column rename in a conditional DO block to avoid errors on existing schema. Update changelog. 2026-04-06 14:25:21 +02:00
stefan 9223305613 Adjust V011 migration: clarify redundant field removal in V010, ensure reiter_id addition and foreign key constraint establishment. Update changelog accordingly. 2026-04-06 14:23:09 +02:00
stefan 3cab4c4f47 Link Funktionaer to Reiter via reiter_id, implement findByName in ReiterRepository, optimize ZNS import for functionary-reiter matching, remove redundant fields from FunktionaerTable, and add database migration V011. 2026-04-06 14:21:11 +02:00
stefan 9237882437 Integrate qualification master data (QualifikationMasterTable) with functionary models, update schema and repository logic, refactor satzID references, and harmonize database migration (V010). 2026-04-06 13:59:14 +02:00
stefan c35869f8ee Integrate qualification master data system (QualifikationMasterTable) for functionaries, refactor mapping logic in repositories, enhance database initialization for ZNS and Masterdata services, and add a seeder for ÖTO/FEI qualification data. Fix PSQLException during ZNS imports. 2026-04-06 13:53:15 +02:00
stefan 933ef9cd6c Resolve port conflicts in masterdata-service: update application.yml with separated bind addresses (Spring: 127.0.0.1, Ktor: 0.0.0.0), change management port to 8086, and add explicit datasource and Flyway configuration. Fix startup issues with Database.connect() in MasterdataDatabaseConfiguration. 2026-04-06 09:43:31 +02:00
stefan aa9e2da3a3 Remove deprecated ZnsLegacyParsersTest.kt, synchronize database schema with Exposed domain models (migration V010), add license-related fields to Reiter, integrate updated LicenseMatrixService fallback logic, improve ZnsImportService with file archiving, and add ZNS testing runbook. 2026-04-06 01:46:26 +02:00
stefan e94dc5a803 Introduce modular ZNS parsers (ZnsVereinParser, ZnsReiterParser, ZnsPferdParser, ZnsFunktionaerParser), replace legacy parsers, and update the import service/test suites to support streamlined parsing and isolated imports. Deprecate ZnsLegacyParsers. Add comprehensive tests (ZnsParserTest) and revise extraction logic for ZNS data. 2026-04-06 01:19:56 +02:00
stefan f50d4deb16 Refactor domain models (DomFunktionaer, DomReiter, DomPferd) to align with ZNS conventions: simplify naming, update properties, and enhance parser logic. Adjust related controllers, repository methods, and tests. Update MASTER_ROADMAP with changes to domain models. 2026-04-06 00:00:23 +02:00
stefan 1e5fa3d053 Remove unused imports across multiple modules to streamline code and improve readability. 2026-04-05 08:23:04 +02:00
stefan a61dda69d1 Refactor domain models (DomFunktionaer, DomReiter, DomPferd) for ZNS alignment: update properties, streamline validation logic, and enhance parser to support new format. Adjust controllers and repository methods accordingly. 2026-04-05 08:21:16 +02:00
stefan aba7b58dd4 Upgrade Gradle Wrapper to 9.4.1 for compatibility and stability improvements. 2026-04-04 21:02:15 +02:00
stefan 8771c7ce27 Archive legacy specifications document OETO-2026_Meldestelle_Pflichtenheft_V2.4_2021-07-28.md and replace it with a Markdown version for improved readability and metadata inclusion. Confirm compliance with structured documentation standards. 2026-04-04 21:00:18 +02:00
stefan ef234747bc Create C-3 proposal for reorganizing docs/ folder structure: document hotspots, define target hierarchy, and outline migration plan. Update README.md and Curator roadmap with proposal details.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
2026-04-03 23:19:28 +02:00
stefan 7150622e1d Update setup guide and README to emphasize desktop-first development. Add detailed instructions for local setup, including prerequisites (JDK 25, Gradle 9.4.0, Docker), and clarify Docker backend stack as optional. Mark Sprint C tasks C-1 and C-2 as complete. 2026-04-03 23:11:20 +02:00
stefan c6f28462eb Update MVVM + UDF documentation: add metadata, extend examples, and consolidate references. Mark Curator roadmap tasks as complete. 2026-04-03 23:05:17 +02:00
stefan dbe7c74a9c Document tenant-aware database schema, multi-tenant strategy, and API references:
- Add database schema documentation: `Database_Schema_V1-V009.md` for tenant-isolated entities (`veranstaltungen`, `turniere`, `bewerbe`, etc.).
- Draft initial Kassa API reference: `Kassa_API.md` (status: DRAFT).
- Finalize Stammdaten API reference: `API_Uebersicht_Stammdaten.md` (status: ACTIVE).
- Summarize tenant isolation and multi-tenant strategy in `Multi_Tenant_Kurz.md`.
- Update `README.md` with links to new references. Mark B-2 roadmap tasks as partially complete.
2026-04-03 22:59:45 +02:00
stefan 6e484ee9a1 Update MASTER_ROADMAP with Offline-First product focus and Phase 8 milestones. Add detailed concept document for Desktop↔Backend synchronization (konzept-offline-first-desktop-backend-de.md). Mark Sprint C tasks C-1 and C-2 as complete and link ADR-0021/0022 dependencies. 2026-04-03 22:52:19 +02:00
stefan 6c64444a98 Add journal log: Fix Flyway migration issues in ping-service with service-specific schema history configuration.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-03 21:52:30 +02:00
stefan 35f8b46e6c Archive outdated journal logs and documents. Add Postman Runbook structure and centralize API testing documentation. Update Flyway configuration for ping-service with service-specific schema history. 2026-04-03 21:48:39 +02:00
stefan b9ec070993 docs: log session outcomes and apply enhancements across multiple components
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
- **Docker Fixes:** Resolved failed builds for Gateway and Ping services by switching to `eclipse-temurin:21-jdk-alpine`, correcting Gradle configurations, and fixing cache mount paths.
- **ZNS-Import Consul Registration:** Enabled Consul service discovery by updating `application.yaml` and `build.gradle.kts`.
- **pgAdmin Provisioning:** Preconfigured the database server in `servers.json` and updated `dc-ops.yaml` for seamless setup.
- **Postman Documentation:** Added a detailed Postman test guide covering environment setup, endpoint groups, and recommended test sequences.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 14:24:46 +02:00
stefan ed3d327c82 docs(curator): log session outcomes and finalize B-1 roadmap updates
- Logged session in `2026-04-03_Curator_B1_Roadmaps-Verzeichnis.md`, summarizing all roadmap reviews and corrections.
- Updated `Curator_Roadmap.md`: marked Sprint B-1 as complete, adjusted dependencies, and refined upcoming priorities.
- Corrected `QA_Roadmap.md` Sprint-B header to reflect partial completion (`🔴🟡`).
- Added detailed revisions for QA, backlog, and dependency management in all roadmaps.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 11:58:10 +02:00
stefan 52f2a54e0b feat(docs): finalize editing forms guideline and define empty state specification
- Marked `Editier-Formulare_Dialog-vs-Fullscreen_v1.md` as APPROVED with finalized mapping for all edit screens.
- Created `Empty-States_Spezifikation_v1.md` to outline design, behavior, and implementation plan for empty states across 10 screens.
- Logged session outcomes in `2026-04-03_UIUX_B1_B4_Editier-Formulare_Empty-States_Curator_Log.md`.
- Updated `UIUX_Roadmap.md` to reflect Sprint B completion and tasks for Sprint C-1.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 11:54:44 +02:00
stefan 59f7f8d4ad feat(tests): add QA test suites for onboarding and departmental logic validation
- **Onboarding (B-2):** Extracted `OnboardingValidator` and added `OnboardingValidatorTest` for edge-case validations (17 new unit tests: field guards, double-click prevention, cancel/reset behavior, `rememberSaveable` regression fix).
- **Departmental Logic (B-3):** Extended `AbteilungsRegelServiceTest` with 14 new tests covering CSN-C-NEU splitting logic (≤95 cm: license-free/licensed, ≥100 cm: R1/R2+), Caprilli regressions, and organizational/separate award scenarios.
- Updated `AbteilungsRegelService.kt` to implement CSN-C-NEU logic and added `ORGANISATORISCH` + `SEPARATE_SIEGEREHRUNG` enums for new rules.
- Updated Changelog and QA roadmap with completed tasks.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 11:46:05 +02:00
stefan 7ff48ed3d7 feat(devops): configure desktop packaging and introduce semantic versioning
- Added `nativeDistributions` for Linux (.deb), Windows (.msi), and macOS (.dmg) in `build.gradle.kts` with platform-specific settings, embedded JRE, and JVM-args.
- Implemented centralized semantic versioning via `version.properties` as the single source of truth, applying it across all builds.
- Introduced CI/CD release workflow (`.gitea/workflows/release.yml`) for auto-tagging, artifact builds, and release summaries.
- Created `CHANGELOG.md` following Keep-a-Changelog format for tracking changes.
- Documented icon requirements and packaging steps in `ICONS_PLACEHOLDER.md`.
- Updated DevOps roadmap to reflect completed Sprint C-1 and C-2 tasks.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 11:26:46 +02:00
stefan c696b8c50e feat(db): add regulation-as-data tables for license height and horse age matrices
- Introduced `license_height_matrix` for mapping minimum license requirements to jump heights (ÖTO § 231).
- Added `horse_min_age_matrix` for defining minimum horse ages by discipline, height, or level (ÖTO § 103, FEI GR Art. 136).
- Populated both tables with ÖTO 2026 and FEI-compliant seed data.
- Updated Flyway V008 to remove incorrect `RD4` entry and annotated corrected enum references.
- Created Flyway V009 for introducing the new tables and their seeds.
- Aligned documentation, validation rules, and roadmaps for backend implementation handover.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 11:10:45 +02:00
stefan 1f9f528554 fix(tests): resolve EntriesIsolationIntegrationTest failures with test-specific DB config
- Added `TestExposedConfiguration` to connect Exposed with Spring `DataSource` in the `test` profile.
- Downgraded `springdoc` version from `3.0.0` to `2.8.9` for Spring Boot 3.x compatibility.
- Applied `@ActiveProfiles("test")` to `EntriesIsolationIntegrationTest`.
- Updated roadmap documentation to reflect bugfix and test success.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 10:52:40 +02:00
stefan f4844eb428 fix(tests): resolve EntriesIsolationIntegrationTest failures with test-specific DB config
- Added `TestExposedConfiguration` to connect Exposed with Spring `DataSource` in the `test` profile.
- Downgraded `springdoc` version from `3.0.0` to `2.8.9` for Spring Boot 3.x compatibility.
- Applied `@ActiveProfiles("test")` to `EntriesIsolationIntegrationTest`.
- Updated roadmap documentation to reflect bugfix and test success.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 10:24:08 +02:00
stefan 2270f9602f feat(api): extend Masterdata API with CRUD endpoints for Pferde and Funktionäre
- Added full CRUD support for Pferde (list, search, get by ID, create, update, delete) with filters for Jahrgang and BesitzerId.
- Introduced FunktionaerController, offering CRUD operations for Funktionäre (list, search, get by ID, create, update, delete) with filtering by Rolle.
- Enhanced HorseController and ReiterController with updated data models, additional request validation, and detailed filtering options.
- Extended backend configurations to register new controllers and route handlers.
- Updated roadmaps and progress documents to reflect completed Sprint B-1 tasks.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 10:12:36 +02:00
stefan 2dd5453365 docs: add ADR-0022 for LAN-Sync protocol implementation
- Documented a hybrid "Event-Sourcing Light with Lamport Clocks" approach for offline-first LAN synchronization between Meldestelle and Richter-Turm.
- Included detailed options analysis (Event-Sourcing, CRDT, Timestamp-Sync) and rationale for the selected solution.
- Added specifications: SyncEvent model, Lamport clock rules, WebSocket protocol (handshake, sync, recovery), and domain mastership rules.
- Defined snapshot strategy to ensure scalable logs and efficient replay.
- Outlined implementation plan in four phases, highlighting task breakdown for backend and frontend teams.
- Updated architect, backend, and frontend roadmaps to reflect ADR-0022 integration steps.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 09:45:55 +02:00
stefan 236876a043 docs: restructure and streamline sprint execution order
- Consolidated and removed redundant steps in `SPRINT_EXECUTION_ORDER.md`.
- Simplified descriptions and roadmap formatting for improved clarity.
- Updated progress and dependencies to align with Phase 8 objectives.
- Adjusted role-specific roadmaps to reflect the latest sprint updates.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 09:43:11 +02:00
stefan 14b458860c feat(validation): integrate ÖTO/FEI rule validations and centralized validators
- Added `OetoValidators` with live-validation for OEPS numbers, FEI-IDs, license classes, and horse data to align with ÖTO/FEI 2026 standards.
- Expanded `ReiterProfilViewModel` and `PferdProfilViewModel` to include validation states (`ValidationResult`) for enhanced form feedback and dirty state tracking.
- Standardized mock data in `Stores.kt` and `NennungModels.kt` to comply with updated validation rules.
- Created `OetoValidatorsTest` to ensure validation logic accuracy (30 unit tests, all green).
- Updated `build.gradle.kts` to include `kotlin.test` dependency for JVM testing.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-04-03 09:04:12 +02:00
stefan 2c8d16b27f Refactor DefaultVeranstalterRepository to use exception classes instead of objects. Clean up unused imports in VeranstalterModule and VeranstalterAuswahlV2.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-03 01:12:14 +02:00
stefan f82dbd64a5 Integrate Ktor HTTP clients and repositories for Veranstalter and Turnier features:
- Add `ApiRoutes` for central backend routing configuration.
- Implement `DefaultVeranstalterRepository` and `DefaultTurnierRepository` with Ktor clients.
- Add domain models (`Turnier`, `Bewerb`, `Abteilung`, `Veranstalter`) and respective repository interfaces.
- Replace fake VeranstalterRepository with real implementation.
- Update DI with `veranstalterModule` and HTTP client injection.
- Simplify TokenProvider and update HttpClient setup (timeouts, retries, logging).
- Mark roadmap tasks B-2 as partially complete.
2026-04-03 01:09:35 +02:00
stefan a5c1fb5bae Mark Sprint B-1 and B-2 tasks as complete: finalize CI/CD pipeline for headless Compose Desktop tests, implement Gradle build optimizations, and update roadmap. Add new Gitea Actions workflow for desktop builds and tests. 2026-04-03 00:48:58 +02:00
stefan 62c0d9d75c Mark sprint tasks A-1 and parts of B-1 through B-3 as complete. Finalize design inventory, add Editier-Formulare guidelines, Bewerb creation workflow with Abteilungs-Logik, and Veranstaltungs-Kassa wireframes to documentation. 2026-04-03 00:39:59 +02:00
stefan 2b3e2d8c1b Implement MVVM for all V3 screens: add ViewModels for Turniere, Bewerbe, Abteilungen, Pferde, Reiter, Vereins, and Funktionaer workflows. Update roadmap to mark B-1 tasks as complete. 2026-04-03 00:26:02 +02:00
stefan 48ffadaaa2 Remove unused imports in BewerbRepositoryImpl and TurnierService. 2026-04-03 00:07:55 +02:00
stefan c483f4925d Introduce tournament structure management for Entries Service:
- Add multi-layered entity support for `Turniere`, `Bewerbe`, and `Abteilungen` with tenant isolation.
- Implement Flyway schema migrations with constraints, indices, and default values for `Turniere`.
- Add Kotlin repositories and services for CRUD operations and validation across entities.
- Ensure tenant-safe transactions and implement new exception handling for `LockedException` and `ValidationException`.
- Provide REST APIs with controllers for managing lifecycle, hierarchy, and relationships between entities (`Turniere`, `Bewerbe`, and `Abteilungen`).
- Update Spring configuration with dependency wiring for new services and repositories.
2026-04-03 00:06:38 +02:00
stefan 85282ea7b4 Update documentation for Navigation V3 and tenant concept: Mark Navigation V2 as deprecated, link replacement documentation, and expand tenant concept details with frontend and backend integration guidelines.
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
2026-04-02 23:17:07 +02:00
stefan 6595ec674f Implement tournament category validation: Add Turnier.validateKategorieLimits with a policy interface and descriptor for decoupled validation against ÖTO limits. Introduce TurnierkategoriePolicy and implement OeToTurnierkategoriePolicy for CSN and CDN max limits. Add comprehensive unit tests and update roadmap with completed A-3 sub-tasks. 2026-04-02 22:54:13 +02:00
stefan dc68a6b749 Mark A-1 as complete: Finalize domain model glossary tasks in Curator Roadmap. Update definitions for hierarchy, account structures, cashbox aggregation, and transaction handling. 2026-04-02 22:41:14 +02:00
stefan 78e153f768 Clean up unused imports in Veranstalter and Turnier features 2026-04-02 22:30:29 +02:00
stefan 5e4c292f0c Implement MVVM + UDF: Add BewerbAnlegenViewModel, VeranstalterViewModel, and state management for Veranstalter and Bewerb workflows. Refactor existing Composables to use ViewModels and intents. Update Turnier UI for Bewerb creation with mandatory division logic, and add documentation for MVVM patterns and guidelines. Mark A-1 and A-2 as complete in the roadmap. 2026-04-02 22:29:21 +02:00
stefan a8bc82eb91 Mark A-2 as complete: Create domain hierarchy tables for events and tournaments, add Flyway migration script and tests for V2 schema; update roadmap with completed tasks. 2026-04-02 22:03:50 +02:00
stefan 9902b2bb44 Implement tenant isolation for Entries Service: switch transaction handling to tenantTransaction, introduce Flyway-based migrations for tenant schemas, and add JdbcTenantRegistry with control schema support. Include migration tests, schema initializations, and E2E tenant isolation. Update configuration and roadmap with completed A-1 tasks. 2026-04-02 21:59:41 +02:00
stefan b787504474 Migrate frontend navigation to V3: archive Navigation V2, implement updated screen-tree and back-stack rules, and adapt documentation for startable MVP flow. 2026-04-02 20:09:26 +02:00
stefan 8b40a0624b Mark A-1 as complete: Update Docker Compose setup with health checks, unified depends_on conditions, and one-command startup; document changes in roadmap. 2026-04-02 18:52:48 +02:00
stefan 2715b75535 Mark A-1 as complete: Add detailed QA Test-Strategie for Compose Desktop App, including test pyramid, tooling, conventions, and CI/CD integration; link roadmap and strategy documents. 2026-04-02 18:46:40 +02:00
stefan 4ae701d969 Document and implement CSN-C-NEU mandatory division rules: Add TURNIER_KLASSEN.md detailing forced divisions by license categories; establish uniform labels and keys (e.g., LZF_ONLY and R1_PLUS); draft optional youth division rules. Extend validation in Validierungsregeln.md. Implement FEI-ID resolver and mapping endpoint in Masterdata service. 2026-04-02 18:37:10 +02:00
stefan bbe5b1a357 Finalize domain model: define event → tournament → class → division hierarchy, multi-tournament account structures, and event-wide cashbox aggregation logic. Add division types and update glossary tasks. 2026-04-02 17:53:49 +02:00
stefan 898d249d41 Document Tenant-Resolution strategy: finalize ADR-0021 with schema-per-tenant approach, add detailed implementation guidelines, and update Architect Roadmap. 2026-04-02 16:38:36 +02:00
1210 changed files with 61970 additions and 21222 deletions
+41
View File
@@ -0,0 +1,41 @@
## 🚀 Identität & Arbeitsmodus (Chamäleon-Modus)
Du bist ein hochqualifizierter KI-Assistent für das Softwareprojekt "Meldestelle" von Stefan.
Ich weise dir in meinen Prompts Aufgaben zu. Nimm sofort die entsprechende Rolle an, beginne deine Antwort zwingend mit dem passenden Badge und passe dein Vokabular an:
* 🏗️ **[Lead Architect]:** System-Design, Gradle-Build-Logik, Modulstruktur.
* 📜 **[Rulebook Expert]:** Validiert Business-Rules gegen das ÖTO/FEI Regelwerk.
* 👷 **[Backend Developer]:** Kotlin & Spring Boot Experte.
* 🎨 **[Frontend Expert]:** KMP & Compose Desktop Spezialist.
* 🐧 **[DevOps Engineer]:** Infrastruktur (Docker, CI/CD, Proxmox).
**Arbeitsanweisung:** Bearbeite pro Antwort immer nur EINE fachliche Aufgabe.
## 🏗️ Projekt-Strategie (Reality-Reset)
1. **Desktop-First & Offline-First:** Das Primärziel ist eine autarke Compose Desktop App (KMP). Sie muss auf Turnieren ohne Internet funktionieren (lokale Persistenz).
2. **Optionales Backend:** Ein Spring Boot Stack (PostgreSQL, Valkey, Keycloak) wird nur für Multi-Tenant-Verwaltung, Registrierung und P2P-Sync genutzt.
3. **Domain-Driven Design (DDD):** Die absolute Business-Hierarchie lautet: Veranstaltung -> Turnier -> Bewerb/Abteilung.
4. **Der System-Akteur:** Der primäre "Actor" in allen Use-Cases ist *nicht* der Veranstalter, sondern zwingend die Person, die die Meldestelle betreut (Actor = Meldestelle).
## 🛠️ Der verbindliche Tech-Stack
Generiere Code ausschließlich für diese exakten Versionen und Paradigmen:
* **Frontend (KMP):** Kotlin 2.3.21, Compose Multiplatform 1.10.3, Ktor Client 3.4.1, SQLDelight.
* **Backend:** Spring Boot 3.5.9 (JDK 25), Ktor Server (wo spezifiziert), Exposed 1.1.1.
* **Infrastruktur:** Gitea (CI/CD), Docker, Pangolin Tunnel. (KEIN GitHub, KEIN Cloudflare).
## 👁️ Anti-Halluzinations-Protokoll
Du bist an strikte, evidenzbasierte Entwicklung gebunden:
1. **Kein "Erledigt" ohne Beweis:** Ein Task ist erst abgeschlossen, wenn Test-Logs oder ein Build vorliegen.
2. **Verifikation ausstehend:** Generierter, ungetesteter Code muss diesen Vermerk zwingend tragen.
3. **Fakten-Check:** Wenn du den Code nicht im Kontext hast (z.B. eine spezifische Gradle-Datei), fordere sie aktiv vom User an, anstatt blind zu raten.
## 🛡️ DSGVO & Lokale Ausführung (Nolik-Spezifika)
* Dein Name ist "Nolik". Du bist ein lokal gehosteter, datenschutzkonformer Senior-Architekt auf dem Server "Simka" (Proxmox VM 101).
* **Datensouveränität:** Du bist der Hüter der lokalen Daten. Generiere niemals Code, der Telemetrie, Tracking oder Logging an externe Cloud-Anbieter sendet.
+11
View File
@@ -0,0 +1,11 @@
## 🚀 Identität & Arbeitsmodus (Chamäleon-Modus)
Du bist ein hochqualifizierter KI-Assistent für das Softwareprojekt "Meldestelle" von Stefan.
Ich weise dir in meinen Prompts Aufgaben zu. Nimm sofort die entsprechende Rolle an, beginne deine Antwort zwingend mit dem passenden Badge und passe dein Vokabular an:
* 🏗️ **[Lead Architect]:** System-Design, Gradle-Build-Logik, Modulstruktur.
* 📜 **[Rulebook Expert]:** Validiert Business-Rules gegen das ÖTO/FEI Regelwerk.
* 👷 **[Backend Developer]:** Kotlin & Spring Boot Experte.
* 🎨 **[Frontend Expert]:** KMP & Compose Desktop Spezialist.
* 🐧 **[DevOps Engineer]:** Infrastruktur (Docker, CI/CD, Proxmox).
**Arbeitsanweisung:** Bearbeite pro Antwort immer nur EINE fachliche Aufgabe.
+3
View File
@@ -0,0 +1,3 @@
## ⚙️ Provider-Spezifika (Google Gemini / Web-Meta-Modus)
* Du agierst als "Gemini" über die Web-Oberfläche. Deine primäre Aufgabe ist die strategische Meta-Ebene, Architektur-Analyse, Review von CI/CD-Pipelines und das Sparring bei komplexen Refactoring-Plänen.
* **Antwort-Stil:** Antworte prägnant, strukturiert und nutze das bereitgestellte Formatierungstoolkit (Markdown, klare Hierarchien, Code-Blöcke). Vermeide unnötige Floskeln und komm direkt auf den technischen Punkt.
+4
View File
@@ -0,0 +1,4 @@
## ⚙️ Provider-Spezifika (JetBrains Junie / IDE-Modus)
* Dein Name ist "Junie". Du arbeitest als hochintegrierter KI-Assistent direkt innerhalb von IntelliJ IDEA.
* **Kontext-Fokus:** Nutze die lokalen Projektdateien, Indizes und das Git-Log intensiv. Wenn Refactorings oder Code-Generierungen anstehen, achte penibel darauf, dass bestehende Datei-Imports (Kotlin-Packages) nicht zerschossen werden.
* **Generierungs-Gate:** Halte dich strikt an die im Projekt hinterlegten Formatierungsregeln für Detekt und Ktlint.
+3
View File
@@ -0,0 +1,3 @@
## 🛡️ DSGVO & Lokale Ausführung (Nolik-Spezifika)
* Dein Name ist "Nolik". Du bist ein lokal gehosteter, datenschutzkonformer Senior-Architekt auf dem Server "Simka" (Proxmox VM 101).
* **Datensouveränität:** Du bist der Hüter der lokalen Daten. Generiere niemals Code, der Telemetrie, Tracking oder Logging an externe Cloud-Anbieter sendet.
+5
View File
@@ -0,0 +1,5 @@
## 🏗️ Projekt-Strategie (Reality-Reset)
1. **Desktop-First & Offline-First:** Das Primärziel ist eine autarke Compose Desktop App (KMP). Sie muss auf Turnieren ohne Internet funktionieren (lokale Persistenz).
2. **Optionales Backend:** Ein Spring Boot Stack (PostgreSQL, Valkey, Keycloak) wird nur für Multi-Tenant-Verwaltung, Registrierung und P2P-Sync genutzt.
3. **Domain-Driven Design (DDD):** Die absolute Business-Hierarchie lautet: Veranstaltung -> Turnier -> Bewerb/Abteilung.
4. **Der System-Akteur:** Der primäre "Actor" in allen Use-Cases ist *nicht* der Veranstalter, sondern zwingend die Person, die die Meldestelle betreut (Actor = Meldestelle).
+5
View File
@@ -0,0 +1,5 @@
## 🛠️ Der verbindliche Tech-Stack
Generiere Code ausschließlich für diese exakten Versionen und Paradigmen:
* **Frontend (KMP):** Kotlin 2.3.21, Compose Multiplatform 1.10.3, Ktor Client 3.4.1, SQLDelight.
* **Backend:** Spring Boot 3.5.9 (JDK 25), Ktor Server (wo spezifiziert), Exposed 1.1.1.
* **Infrastruktur:** Gitea (CI/CD), Docker, Pangolin Tunnel. (KEIN GitHub, KEIN Cloudflare).
+5
View File
@@ -0,0 +1,5 @@
## 👁️ Anti-Halluzinations-Protokoll
Du bist an strikte, evidenzbasierte Entwicklung gebunden:
1. **Kein "Erledigt" ohne Beweis:** Ein Task ist erst abgeschlossen, wenn Test-Logs oder ein Build vorliegen.
2. **Verifikation ausstehend:** Generierter, ungetesteter Code muss diesen Vermerk zwingend tragen.
3. **Fakten-Check:** Wenn du den Code nicht im Kontext hast (z.B. eine spezifische Gradle-Datei), fordere sie aktiv vom User an, anstatt blind zu raten.
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
# Nutze Junies robuste Pfad-Ermittlung
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
REPO_ROOT="$(resolve_repo_root)"
cd "$REPO_ROOT"
AI_DIR=".ai"
DIST_DIR="$AI_DIR/dist"
mkdir -p "$DIST_DIR"
echo "[INFO] Generiere System-Prompts aus den Core-Rules..."
for PROVIDER_DIR in "$AI_DIR/providers/"*; do
if [ -d "$PROVIDER_DIR" ]; then
PROVIDER_NAME=$(basename "$PROVIDER_DIR")
OUTPUT_FILE="$DIST_DIR/${PROVIDER_NAME}-system-prompt.md"
echo "-> Baue Prompt für: $PROVIDER_NAME"
# 1. Basis-Identität schreiben
cat "$AI_DIR/prompts/system/base.md" > "$OUTPUT_FILE"
echo -e "\n\n" >> "$OUTPUT_FILE"
# 2. Alle globalen Regeln anhängen
for RULE_FILE in "$AI_DIR/rules/"*.md; do
if [ -f "$RULE_FILE" ]; then
cat "$RULE_FILE" >> "$OUTPUT_FILE"
echo -e "\n\n" >> "$OUTPUT_FILE"
fi
done
# 3. Provider-Spezifika anhängen
if [ -f "$PROVIDER_DIR/overlay.md" ]; then
cat "$PROVIDER_DIR/overlay.md" >> "$OUTPUT_FILE"
fi
echo "[OK] $OUTPUT_FILE erfolgreich erstellt."
fi
done
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/lib/common.sh"
REPO_ROOT="$(resolve_repo_root)"
cd "$REPO_ROOT"
# check-docs-drift.sh
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
# - Kein Guidelines-System mehr.
# - Single Source of Truth: `docs/`
err=0
has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; }
miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; }
# Harte Altlast-Pfade dürfen nicht mehr vorkommen
if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden"
err=1
fi
# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein)
has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway"
has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway"
miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor"
has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway"
miss docs/01_Architecture/c4/02-container-de.puml "Ktor"
exit $err
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
# Common helpers for AI guardrail scripts
# Robustly resolve the repository root directory.
# Strategy: prefer Git; fallback to marker search upwards; last resort: current dir.
resolve_repo_root() {
local start
start="${1:-$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)}"
if command -v git >/dev/null 2>&1; then
if git -C "$start" rev-parse --show-toplevel >/dev/null 2>&1; then
git -C "$start" rev-parse --show-toplevel
return 0
fi
fi
local dir
dir="$(cd "$start" && pwd)"
while [ "$dir" != "/" ]; do
if [ -f "$dir/gradlew" ] || [ -f "$dir/settings.gradle.kts" ] || [ -d "$dir/.git" ]; then
echo "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
pwd
}
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/lib/common.sh"
REPO_ROOT="$(resolve_repo_root)"
cd "$REPO_ROOT"
mkdir -p build/diagrams
shopt -s nullglob
for f in docs/architecture/c4/*.puml; do
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
done
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/lib/common.sh"
REPO_ROOT="$(resolve_repo_root)"
cd "$REPO_ROOT"
QUICK_MODE=false
while [[ $# -gt 0 ]]; do
case $1 in
--quick)
QUICK_MODE=true
shift
;;
--help|-h)
cat << 'EOF'
Docs Link-Validierung
USAGE:
./.ai/scripts/validate-links.sh [--quick]
BESCHREIBUNG:
Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade.
Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...).
OPTIONEN:
--quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert).
EOF
exit 0
;;
*)
echo "[ERROR] Unbekannter Parameter: $1" >&2
exit 2
;;
esac
done
python3 - <<'PY'
import re
import sys
from pathlib import Path
from urllib.parse import unquote
root = Path.cwd()
docs_dir = root / "docs"
if not docs_dir.is_dir():
print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr)
sys.exit(2)
# Veraltete Pfad-Prüfungen wurden entfernt; Fokus auf Link-Integrität.
FORBIDDEN_SUBSTRINGS = []
md_files = sorted(docs_dir.rglob("*.md"))
link_pattern = re.compile(r"\]\(([^)]+)\)")
errors = 0
def is_external(target: str) -> bool:
t = target.lower()
return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:")
def strip_fragment_and_query(target: str) -> str:
target = target.split("#", 1)[0]
target = target.split("?", 1)[0]
return target
for f in md_files:
text = f.read_text(encoding="utf-8", errors="replace")
for forbidden in FORBIDDEN_SUBSTRINGS:
if forbidden in text:
print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}")
errors += 1
for match in link_pattern.finditer(text):
target = match.group(1).strip()
if not target:
continue
if is_external(target):
continue
if target.startswith("#"):
continue
if target.startswith("<") and target.endswith(">"):
target = target[1:-1]
target = unquote(strip_fragment_and_query(target))
if target.startswith("/"):
continue
if ":" in target.split("/", 1)[0]:
# z.B. "vscode:..."
continue
resolved = (f.parent / target).resolve()
try:
resolved.relative_to(root.resolve())
except ValueError:
print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}")
errors += 1
continue
if resolved.is_dir():
if not (resolved / "README.md").is_file():
print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}")
errors += 1
continue
if not resolved.exists():
print(f"[ERROR] Broken link: {f} -> {target}")
errors += 1
if errors:
print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler")
sys.exit(1)
print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft")
PY
@@ -0,0 +1,4 @@
---
apply: always
---
+1 -1
View File
@@ -193,7 +193,7 @@ secrets/
# ===================================================================
TODO*.md
NOTES*.md
**/.junie/
.junie/
# ===================================================================
# Keep essential files (override exclusions)
+263
View File
@@ -0,0 +1,263 @@
# ==========================================
# Meldestelle Docker Compose Environment
# Single Source of Truth (SSoT)
# ==========================================
# WARNING: This file contains secrets (passwords).
# Do NOT commit this file to version control if it contains production secrets.
# --- PROJECT ---
PROJECT_NAME=meldestelle
# --- BACKUP ---
BACKUP_DIR=/home/stefan/backups/meldestelle
BACKUP_RETENTION_DAYS=7
# Docker build versions (optional overrides)
DOCKER_VERSION=1.0.0-SNAPSHOT
DOCKER_REGISTRY=git.mo-code.at/mocode-software/meldestelle
DOCKER_BUILD_DATE=2026-03-16T12:00:00Z
DOCKER_GRADLE_VERSION=9.3.1
DOCKER_JAVA_VERSION=25
DOCKER_NODE_VERSION=24.12.0
DOCKER_NGINX_VERSION=1.28.0-alpine
DOCKER_CADDY_VERSION=2.11-alpine
# JVM Power Flags (Lokal leer lassen, da Intel/AMD Architektur)
JVM_OPTS_ARM64=
# --- POSTGRES ---
POSTGRES_IMAGE=postgres:16-alpine
POSTGRES_SHARED_BUFFERS=256MB
POSTGRES_EFFECTIVE_CACHE_SIZE=768MB
POSTGRES_USER=pg-user
POSTGRES_PASSWORD=pg-password
POSTGRES_DB=pg-meldestelle-db
POSTGRES_PORT=5432:5432
POSTGRES_DB_URL=jdbc:postgresql://postgres:5432/pg-meldestelle-db
# --- VALKEY (formerly Redis) ---
VALKEY_IMAGE=valkey/valkey:9-alpine
VALKEY_PASSWORD=valkey-password
VALKEY_PORT=6379:6379
VALKEY_SERVER_HOSTNAME=valkey
VALKEY_SERVER_PORT=6379
VALKEY_SERVER_CONNECT_TIMEOUT=5s
VALKEY_POLICY=allkeys-lru
VALKEY_MAX_MEMORY=256MB
SPRING_DATA_VALKEY_HOST=localhost
SPRING_DATA_VALKEY_PORT=6379
SPRING_DATA_VALKEY_PASSWORD=valkey-password
# --- KEYCLOAK ---
KEYCLOAK_IMAGE_TAG=latest
KC_HEAP_MIN=512M
KC_HEAP_MAX=1024M
# Lokale Entwicklung: start-dev (kein Pre-Build nötig, kein --optimized)
# Server/Produktion: start --optimized --import-realm (nutzt das pre-built Registry-Image)
KC_COMMAND=start-dev --import-realm
# System-Admin (Master Console)
KC_BOOTSTRAP_ADMIN_USERNAME=kc-admin
KC_BOOTSTRAP_ADMIN_PASSWORD=kc-password
# Fach-Admin User Passwort (wird im Realm Import genutzt)
# Hinweis: Wenn du das hier änderst, müsstest du auch die JSON anpassen
# oder dort eine Variable nutzen.
KC_DB=postgres
KC_DB_SCHEMA=keycloak
KC_DB_URL=jdbc:postgresql://postgres:5432/pg-meldestelle-db
KC_DB_USERNAME=pg-user
KC_DB_PASSWORD=meldestelle
# Lokal: localhost | Server: echte IP oder Domain (z.B. 10.0.0.50 oder auth.meldestelle.at)
# WICHTIG: Nur den Hostnamen angeben, OHNE Port (Keycloak 26.x hostname v2)
KC_HOSTNAME=localhost
# false = Zugriff über beliebige Hostnamen erlaubt (nötig ohne TLS / für HTTP-Betrieb)
KC_HOSTNAME_STRICT=false
KC_HOSTNAME_STRICT_HTTPS=false
KC_PORT=8180:8080
KC_MANAGEMENT_PORT=9000:9000
KC_HTTP_ENABLE=true
KC_API_GATEWAY_CLIENT_SECRET=K5RqonwVOaxPKaXVH4mbthSRbjRh5tOK
# KC_POSTMAN_CLIENT_SECRET=postman-secret-123
# KC_BOOTSTRAP_ADMIN_PASSWORD=Admin#1234
KC_FRONTEND_URL=http://localhost:8180
KC_PROXY_HEADERS=xforwarded
# --- KEYCLOAK TOKEN VALIDATION ---
# Public Issuer URI (must match the token issuer from browser/postman)
# Lokal: http://localhost:8180 | Produktion: http://10.0.0.50:8180
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://localhost:8180/realms/meldestelle
# Internal JWK Set URI (for service-to-service communication within Docker)
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
# --- CONSUL ---
CONSUL_IMAGE=hashicorp/consul:1.22.1
CONSUL_PORT=8500:8500
CONSUL_UDP_PORT=8600:8600/udp
CONSUL_HOST=consul
CONSUL_HTTP_PORT=8500
SPRING_CLOUD_CONSUL_HOST=consul
SPRING_CLOUD_CONSUL_PORT=8500
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME=api-gateway
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS=true
# --- Zipkin ---
ZIPKIN_IMAGE=openzipkin/zipkin:3
ZIPKIN_MIN_HEAP=256M
ZIPKIN_MAX_HEAP=512M
ZIPKIN_PORT=9411:9411
ZIPKIN_ENDPOINT=http://zipkin:9411/api/v2/spans
ZIPKIN_SAMPLING_PROBABILITY=1.0
# --- Mailpit ---
MAILPIT_IMAGE=axllent/mailpit:v1.29
MAILPIT_WEB_PORT=8025:8025
MAILPIT_SMTP_PORT=1025:1025
# --- PGADMIN ---
PGADMIN_IMAGE=dpage/pgadmin4:8
PGADMIN_EMAIL=meldestelle@mo-code.at
PGADMIN_PASSWORD=pgadmin
PGADMIN_PORT=8888:80
# --- POSTGRES-EXPORTER ---
POSTGRES_EXPORTER_IMAGE=prometheuscommunity/postgres-exporter:v0.18.0
# --- ALERTMANAGER ---
ALERTMANAGER_IMAGE=prom/alertmanager:v0.29.0
ALERTMANAGER_PORT=9093:9093
# --- PROMETHEUS ---
PROMETHEUS_IMAGE=prom/prometheus:v3.7.3
PROMETHEUS_PORT=9090:9090
# --- GRAFANA ---
GF_IMAGE=grafana/grafana:12.3
GF_ADMIN_USER=gf-admin
GF_ADMIN_PASSWORD=gf-password
GF_PORT=3000:3000
# --- API-GATEWAY ---
GATEWAY_PORT=8081:8081
GATEWAY_DEBUG_PORT=5005:5005
GATEWAY_SERVER_PORT=8081
GATEWAY_SPRING_PROFILES_ACTIVE=docker
GATEWAY_DEBUG=true
GATEWAY_SERVICE_NAME=api-gateway
GATEWAY_CONSUL_PREFER_IP=true
# --- PING-SERVICE ---
PING_SPRING_PROFILES_ACTIVE=docker
PING_PORT=8082:8082
PING_DEBUG_PORT=5006:5006
PING_SERVER_PORT=8082
PING_DEBUG=true
PING_SERVICE_NAME=ping-service
PING_CONSUL_PREFER_IP=true
# --- MAIL-SERVICE ---
MAIL_PORT=8083:8083
MAIL_DEBUG_PORT=5014:5014
MAIL_SERVER_PORT=8083
MAIL_SERVICE_URL=http://10.0.0.50:8092
MAIL_SPRING_PROFILES_ACTIVE=docker
MAIL_DEBUG=true
MAIL_SERVICE_NAME=mail-service
MAIL_CONSUL_PREFER_IP=true
MAIL_SMTP_HOST=smtp.world4you.com
MAIL_SMTP_PORT=587
MAIL_SMTP_USER=online-nennen@mo-code.at
MAIL_SMTP_PASSWORD=Mogi#2reiten
MAIL_SMTP_AUTH=true
MAIL_SMTP_STARTTLS=true
SPRING_MAIL_HOST=smtp.world4you.com
SPRING_MAIL_PORT=587
SPRING_MAIL_USERNAME=online-nennen@mo-code.at
SPRING_MAIL_PASSWORD=Mogi#2reiten
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=false
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=false
SPRING_CLOUD_CONSUL_DISCOVERY_ENABLED=false
SPRING_CLOUD_CONSUL_ENABLED=false
MAIL_POLLING_ENABLED=false
# --- MASTERDATA-SERVICE ---
MASTERDATA_PORT=8086:8086
MASTERDATA_DEBUG_PORT=5007:5007
MASTERDATA_SERVER_PORT=8086
MASTERDATA_KTOR_PORT=8091
MASTERDATA_SPRING_PROFILES_ACTIVE=docker
MASTERDATA_DEBUG=true
MASTERDATA_SERVICE_NAME=masterdata-service
MASTERDATA_CONSUL_PREFER_IP=true
MASTERDATA_SERVICE_HOSTNAME=masterdata-service
# --- EVENTS-SERVICE ---
EVENTS_PORT=8085:8085
EVENTS_DEBUG_PORT=5008:5008
EVENTS_SERVER_PORT=8085
EVENTS_SPRING_PROFILES_ACTIVE=docker
EVENTS_DEBUG=true
EVENTS_SERVICE_NAME=events-service
EVENTS_CONSUL_PREFER_IP=true
# --- ZNS-IMPORT-SERVICE ---
ZNS_IMPORT_PORT=8095:8095
ZNS_IMPORT_DEBUG_PORT=5009:5009
ZNS_IMPORT_SERVER_PORT=8095
ZNS_IMPORT_SPRING_PROFILES_ACTIVE=docker
ZNS_IMPORT_DEBUG=true
ZNS_IMPORT_SERVICE_NAME=zns-import-service
ZNS_IMPORT_CONSUL_PREFER_IP=true
# --- RESULTS-SERVICE ---
RESULTS_PORT=8088:8088
RESULTS_DEBUG_PORT=5010:5010
RESULTS_SERVER_PORT=8088
RESULTS_SPRING_PROFILES_ACTIVE=docker
RESULTS_DEBUG=true
RESULTS_SERVICE_NAME=results-service
RESULTS_CONSUL_PREFER_IP=true
# --- BILLING-SERVICE ---
BILLING_PORT=8087:8087
BILLING_DEBUG_PORT=5012:5012
BILLING_SERVER_PORT=8087
BILLING_SPRING_PROFILES_ACTIVE=docker
BILLING_DEBUG=true
BILLING_SERVICE_NAME=billing-service
BILLING_CONSUL_PREFER_IP=true
# --- SCHEDULING-SERVICE ---
SCHEDULING_PORT=8084:8084
SCHEDULING_DEBUG_PORT=5013:5013
SCHEDULING_SERVER_PORT=8084
SCHEDULING_SPRING_PROFILES_ACTIVE=docker
SCHEDULING_DEBUG=true
SCHEDULING_SERVICE_NAME=scheduling-service
SCHEDULING_CONSUL_PREFER_IP=true
# --- SERIES-SERVICE ---
SERIES_PORT=8089:8089
SERIES_DEBUG_PORT=5011:5011
SERIES_SERVER_PORT=8089
SERIES_SPRING_PROFILES_ACTIVE=docker
SERIES_DEBUG=true
SERIES_SERVICE_NAME=series-service
SERIES_CONSUL_PREFER_IP=true
# --- WEB-APP ---
CADDY_VERSION=2.11-alpine
WEB_APP_PORT=8080:80
WEB_BUILD_PROFILE=dev
# Lokal: http://localhost:8081 | Produktion: http://10.0.0.50:8081
WEB_APP_API_URL=http://localhost:8081
WEB_APP_KEYCLOAK_URL=http://auth.mo-code.at
# --- DESKTOP-APP ---
DESKTOP_APP_VNC_PORT=5901:5901
DESKTOP_APP_NOVNC_PORT=6080:6080
+87
View File
@@ -120,6 +120,13 @@ MAILPIT_IMAGE=axllent/mailpit:v1.29
MAILPIT_WEB_PORT=8025:8025
MAILPIT_SMTP_PORT=1025:1025
# --- SPRING MAIL CONFIG (Lokal / Mailpit) ---
# Für lokale Entwicklung mit Mailpit (Docker Compose)
SPRING_MAIL_HOST=mailpit
SPRING_MAIL_PORT=1025
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=false
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=false
# --- PGADMIN ---
PGADMIN_IMAGE=dpage/pgadmin4:8
PGADMIN_EMAIL=meldestelle@mo-code.at
@@ -149,6 +156,8 @@ GATEWAY_DEBUG_PORT=5005:5005
GATEWAY_SERVER_PORT=8081
GATEWAY_SPRING_PROFILES_ACTIVE=docker
GATEWAY_DEBUG=true
GATEWAY_SERVICE_NAME=api-gateway
GATEWAY_CONSUL_PREFER_IP=true
# --- PING-SERVICE ---
PING_SPRING_PROFILES_ACTIVE=docker
@@ -159,6 +168,84 @@ PING_DEBUG=true
PING_SERVICE_NAME=ping-service
PING_CONSUL_PREFER_IP=true
# --- MAIL-SERVICE ---
MAIL_PORT=8083:8083
MAIL_DEBUG_PORT=5014:5014
MAIL_SERVER_PORT=8083
MAIL_SPRING_PROFILES_ACTIVE=docker
MAIL_DEBUG=true
MAIL_SERVICE_NAME=mail-service
MAIL_CONSUL_PREFER_IP=true
MAIL_SMTP_HOST=smtp.world4you.com
MAIL_SMTP_PORT=587
MAIL_SMTP_USER=online-nennen@mo-code.at
MAIL_SMTP_PASSWORD=<DEIN_WORLD4YOU_PASSWORT>
MAIL_SMTP_AUTH=true
MAIL_SMTP_STARTTLS=true
# --- MASTERDATA-SERVICE ---
MASTERDATA_PORT=8086:8086
MASTERDATA_DEBUG_PORT=5007:5007
MASTERDATA_SERVER_PORT=8086
MASTERDATA_SPRING_PROFILES_ACTIVE=docker
MASTERDATA_DEBUG=true
MASTERDATA_SERVICE_NAME=masterdata-service
MASTERDATA_CONSUL_PREFER_IP=true
# --- EVENTS-SERVICE ---
EVENTS_PORT=8085:8085
EVENTS_DEBUG_PORT=5008:5008
EVENTS_SERVER_PORT=8085
EVENTS_SPRING_PROFILES_ACTIVE=docker
EVENTS_DEBUG=true
EVENTS_SERVICE_NAME=events-service
EVENTS_CONSUL_PREFER_IP=true
# --- ZNS-IMPORT-SERVICE ---
ZNS_IMPORT_PORT=8095:8095
ZNS_IMPORT_DEBUG_PORT=5009:5009
ZNS_IMPORT_SERVER_PORT=8095
ZNS_IMPORT_SPRING_PROFILES_ACTIVE=docker
ZNS_IMPORT_DEBUG=true
ZNS_IMPORT_SERVICE_NAME=zns-import-service
ZNS_IMPORT_CONSUL_PREFER_IP=true
# --- RESULTS-SERVICE ---
RESULTS_PORT=8088:8088
RESULTS_DEBUG_PORT=5010:5010
RESULTS_SERVER_PORT=8088
RESULTS_SPRING_PROFILES_ACTIVE=docker
RESULTS_DEBUG=true
RESULTS_SERVICE_NAME=results-service
RESULTS_CONSUL_PREFER_IP=true
# --- BILLING-SERVICE ---
BILLING_PORT=8087:8087
BILLING_DEBUG_PORT=5012:5012
BILLING_SERVER_PORT=8087
BILLING_SPRING_PROFILES_ACTIVE=docker
BILLING_DEBUG=true
BILLING_SERVICE_NAME=billing-service
BILLING_CONSUL_PREFER_IP=true
# --- SCHEDULING-SERVICE ---
SCHEDULING_PORT=8084:8084
SCHEDULING_DEBUG_PORT=5013:5013
SCHEDULING_SERVER_PORT=8084
SCHEDULING_SPRING_PROFILES_ACTIVE=docker
SCHEDULING_DEBUG=true
SCHEDULING_SERVICE_NAME=scheduling-service
SCHEDULING_CONSUL_PREFER_IP=true
# --- SERIES-SERVICE ---
SERIES_PORT=8089:8089
SERIES_DEBUG_PORT=5011:5011
SERIES_SERVER_PORT=8089
SERIES_SPRING_PROFILES_ACTIVE=docker
SERIES_DEBUG=true
SERIES_SERVICE_NAME=series-service
SERIES_CONSUL_PREFER_IP=true
# --- WEB-APP ---
WEB_APP_PORT=4000:4000
# URL für API-Zugriffe vom Browser (Public URL via Pangolin)
+74
View File
@@ -0,0 +1,74 @@
name: Desktop CI — Headless Tests & Build
on:
# Nur ausführen, wenn explizit das Desktop-Shell-Modul geändert wurde
push:
branches: [ main, master ]
paths:
- 'frontend/shells/meldestelle-desktop/**'
- '.gitea/workflows/desktop-tests.yml'
pull_request:
branches: [ main, master ]
paths:
- 'frontend/shells/meldestelle-desktop/**'
# Manuell startbar, falls benötigt
workflow_dispatch:
jobs:
desktop-tests:
# Komplett deaktivierbar über Repo-Variable: Settings → Variables → DESKTOP_CI_ENABLED=true
# Zusätzlich: Für PlanBBuilds überspringen, wenn Commit-Message [planb] enthält
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
name: Compose Desktop — Tests (headless) & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 21 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Show Gradle version
run: ./gradlew --version
- name: Run Desktop tests headless (xvfb)
env:
_JAVA_OPTIONS: -Djava.awt.headless=true
run: |
sudo apt-get update -y
sudo apt-get install -y xvfb xauth
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
./gradlew :frontend:shells:meldestelle-desktop:jvmTest --stacktrace --no-daemon
- name: Build Desktop shell (Release)
env:
_JAVA_OPTIONS: -Djava.awt.headless=true
run: |
./gradlew :frontend:shells:meldestelle-desktop:build --stacktrace --no-daemon
- name: Upload build artifacts (Desktop shell)
uses: actions/upload-artifact@v4
with:
name: desktop-shell-build
path: |
frontend/shells/meldestelle-desktop/build/libs/**/*.jar
frontend/shells/meldestelle-desktop/build/compose*/**
if-no-files-found: warn
+34 -46
View File
@@ -33,18 +33,11 @@ jobs:
max-parallel: 1
matrix:
include:
- service: keycloak
# Plan-B fokussiert: Nur Mail-Service + Web-App bauen/pushen (beschleunigt CI deutlich)
- service: mail-service
context: .
dockerfile: config/docker/keycloak/Dockerfile
image: keycloak
- service: api-gateway
context: .
dockerfile: backend/infrastructure/gateway/Dockerfile
image: api-gateway
- service: ping-service
context: .
dockerfile: backend/services/ping/Dockerfile
image: ping-service
dockerfile: backend/services/mail/Dockerfile
image: mail-service
- service: web-app
context: .
dockerfile: config/docker/caddy/web-app/Dockerfile
@@ -61,43 +54,42 @@ jobs:
distribution: "temurin"
cache: gradle
- name: Setup Gradle Cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# Verhindert mysteriöse Build-Fehler durch korrupte Node/Kotlin-Caches (nur web-app relevant)
- name: Cleanup stale build caches
if: matrix.service == 'web-app'
run: |
rm -rf frontend/shells/meldestelle-portal/build/js/node_modules/.cache || true
rm -rf frontend/shells/meldestelle-portal/build/js/.yarn/cache || true
rm -rf ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable || true
- name: Build Frontend (Kotlin JS)
# --- SCHRITT 1: Build mit radikalem Clean (gegen die März-Leichen) ---
- name: Build Frontend (Wasm JS)
if: matrix.service == 'web-app'
run: |
chmod +x gradlew
./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution \
# Löscht alte Build-Stände komplett
./gradlew :frontend:shells:meldestelle-web:clean
./gradlew :frontend:shells:meldestelle-web:wasmJsBrowserDistribution \
-Pproduction=true \
--max-workers=4 \
-Dkotlin.daemon.jvm.options="-Xmx4g"
# Pangolin-Bypass: Credentials direkt in config.json schreiben.
# Kein "docker login" → kein Daemon-Ping → kein HTTPS-Fehler.
# BuildKit liest ~/.docker/config.json und verwendet diese Credentials beim Push.
# - name: Registry-Credentials konfigurieren (kein Daemon-Kontakt)
# run: |
# mkdir -p ~/.docker
# AUTH=$(echo -n "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" | base64 -w 0)
# printf '{"auths":{"%s":{"auth":"%s"}}}\n' "${{ env.REGISTRY_INTERNAL }}" "${AUTH}" > ~/.docker/config.json
# echo "✓ Credentials für ${{ env.REGISTRY_INTERNAL }} gespeichert"
# --- SCHRITT 2: Staging ohne rsync (Fix für dein Log-Fehler) ---
- name: Stage Web Assets for Docker build
if: matrix.service == 'web-app'
run: |
set -e
DIST_DIR="frontend/shells/meldestelle-web/build/dist/wasmJs/productionExecutable"
TARGET_DIR="config/docker/caddy/web-app/_site"
if [ ! -d "$DIST_DIR" ]; then
echo "❌ Fehler: Build-Verzeichnis nicht gefunden!"
exit 1
fi
# Ersetzt rsync durch sicheres Löschen & Kopieren
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR"
cp -r "$DIST_DIR"/. "$TARGET_DIR/"
# Kopiere Turnier-Ausschreibungen (PDFs) für Plan-B
cp docs/Neumarkt2026/*.pdf "$TARGET_DIR/" || true
echo "✓ Assets für Docker vorbereitet (Stand: $(date))"
# --- SCHRITT 3: Login & BuildX ---
# NEU (sauber, nach daemon.json-Fix):
- name: Login to Gitea Registry
uses: docker/login-action@v3
@@ -122,7 +114,7 @@ jobs:
with:
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=latest
type=sha,format=long
- name: Build and push Docker image
@@ -137,9 +129,5 @@ jobs:
provenance: false
sbom: false
build-args: |
DOCKER_BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
BUILD_DATE=${{ github.event.head_commit.timestamp || 'unknown' }}
VERSION=${{ github.sha }}
GRADLE_VERSION=${{ env.GRADLE_VERSION }}
JAVA_VERSION=${{ env.JAVA_VERSION }}
KEYCLOAK_IMAGE_TAG=${{ env.KEYCLOAK_IMAGE_TAG }}
JVM_OPTS_APPEND=${{ env.JVM_OPTS_ARM64 }}
+2
View File
@@ -4,6 +4,8 @@ on:
branches: [ "**" ]
jobs:
no-hardcoded-versions:
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+189
View File
@@ -0,0 +1,189 @@
name: Release — Semantic Versioning & Desktop Packaging
# ---------------------------------------------------------------
# Trigger: Manuell ODER automatisch wenn version.properties
# auf main/master geändert wird.
# ---------------------------------------------------------------
on:
push:
branches: [ main, master ]
paths:
- 'version.properties'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry-Run (kein Tag, kein Upload)'
required: false
default: 'false'
jobs:
# =============================================================
# JOB 1: Version lesen & Git-Tag setzen
# =============================================================
tag-release:
name: 🏷️ Git-Tag setzen
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.read-version.outputs.version }}
tag: ${{ steps.read-version.outputs.tag }}
already_tagged: ${{ steps.check-tag.outputs.already_tagged }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Version aus version.properties lesen
id: read-version
run: |
source <(grep -v '^#' version.properties | grep -v '^$' | sed 's/^/export /')
QUALIFIER="${VERSION_QUALIFIER:-}"
if [ -z "$QUALIFIER" ]; then
VERSION="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}"
else
VERSION="${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}-${QUALIFIER}"
fi
TAG="v${VERSION}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "📦 Version: ${VERSION} | Tag: ${TAG}"
- name: Prüfen ob Tag bereits existiert
id: check-tag
run: |
TAG="${{ steps.read-version.outputs.tag }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "already_tagged=true" >> $GITHUB_OUTPUT
echo "⚠️ Tag $TAG existiert bereits — überspringe Tagging"
else
echo "already_tagged=false" >> $GITHUB_OUTPUT
echo "✅ Tag $TAG ist neu"
fi
- name: Git-Tag erstellen & pushen
if: steps.check-tag.outputs.already_tagged == 'false' && gitea.event.inputs.dry_run != 'true'
run: |
TAG="${{ steps.read-version.outputs.tag }}"
VERSION="${{ steps.read-version.outputs.version }}"
git config user.name "Gitea CI"
git config user.email "ci@mo-code.at"
git tag -a "$TAG" -m "Release $VERSION"
git push origin "$TAG"
echo "🚀 Tag $TAG gepusht"
# =============================================================
# JOB 2: Desktop-Packaging (.deb — Linux)
# =============================================================
package-linux:
name: 📦 Linux .deb Packaging
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein PlanB Commit
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
runs-on: ubuntu-latest
needs: tag-release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 25 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Linux .deb bauen
env:
_JAVA_OPTIONS: -Djava.awt.headless=true
run: |
./gradlew :frontend:shells:meldestelle-desktop:packageDeb \
--stacktrace --no-daemon
- name: .deb Artefakt hochladen
uses: actions/upload-artifact@v4
with:
name: meldestelle-${{ needs.tag-release.outputs.version }}-linux-deb
path: frontend/shells/meldestelle-desktop/build/compose/binaries/main/deb/*.deb
if-no-files-found: warn
# =============================================================
# JOB 3: Desktop-Packaging (.msi — Windows)
# =============================================================
package-windows:
name: 📦 Windows .msi Packaging
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein PlanB Commit
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
runs-on: windows-latest
needs: tag-release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 25 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- name: Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Windows .msi bauen
env:
_JAVA_OPTIONS: -Djava.awt.headless=true
run: |
./gradlew :frontend:shells:meldestelle-desktop:packageMsi `
--stacktrace --no-daemon
- name: .msi Artefakt hochladen
uses: actions/upload-artifact@v4
with:
name: meldestelle-${{ needs.tag-release.outputs.version }}-windows-msi
path: frontend/shells/meldestelle-desktop/build/compose/binaries/main/msi/*.msi
if-no-files-found: warn
# =============================================================
# JOB 4: Release-Summary
# =============================================================
release-summary:
name: 📋 Release-Summary
runs-on: ubuntu-latest
needs: [ tag-release, package-linux, package-windows ]
if: always()
steps:
- name: Summary ausgeben
run: |
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITEA_STEP_SUMMARY
echo "" >> $GITEA_STEP_SUMMARY
echo "| Artefakt | Status |" >> $GITEA_STEP_SUMMARY
echo "|----------|--------|" >> $GITEA_STEP_SUMMARY
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITEA_STEP_SUMMARY
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITEA_STEP_SUMMARY
echo "" >> $GITEA_STEP_SUMMARY
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITEA_STEP_SUMMARY
+47 -60
View File
@@ -1,74 +1,61 @@
# --- General ---
.gradle/
**/build/
**/out/
.kotlin/
kotlin-js-store/
# 🐧 [DevOps Engineer] Optimierte .gitignore für Meldestelle (KMP / Gradle / Docker)
# --- Environments ---
#.env
config/env/.env.local
.env.development.local
.env.test.local
.env.production.local
.env.local
# --- AI ---
.ai/dist/
# --- IDEs ---
# IntelliJ
# --- IDE & Editor ---
.idea/
*.iml
*.ipr
*.iws
*.ipr
out/
.vscode/
.history/
.shelf/
# VS Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/snippets
# --- Gradle ---
.gradle/
build/
!**/src/**/build/
gradle-app.setting
!gradle-wrapper.jar
.gradletasknamecache
bin/
# Fleet
.fleet/
!.fleet/receipt.json
# --- Kotlin / KMP ---
.kotlin/
kotlin-js-store/
.jetbrains/
# --- Dependencies & Build ---
# --- Android (falls relevant) ---
*.ap_
*.apk
*.dex
local.properties
# --- Node / JS (Compose Web / KMP JS) ---
node_modules/
**/node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm/
# --- OS Files ---
# --- Docker & Infrastructure ---
.docker/
*.log
logs/
.env
!.env.example
.data/
postgres-data/
# --- OS Specific ---
.DS_Store
Thumbs.db
*.swp
*~
.nfs*
desktop.ini
# --- Logs ---
_backup/logs/
**/*.log
*.log.gz
# --- Languages & Runtimes ---
# Java/Kotlin
*.class
.attach_pid*
# Python
.venv/
venv/
ENV/
*.pyc
__pycache__/
# --- Quality & Documentation ---
build/diagrams/
.eslintcache
.stylelintcache
.phpunit.result.cache
.dataSources/
dataSources.local.xml
/_backup/
.env
# --- Project Specific ---
docs/temp/
docs/Bin/
docs/_archive/
+4 -40
View File
@@ -1,43 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# check-docs-drift.sh
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
# - Kein Guidelines-System mehr.
# - Single Source of Truth: `docs/`
err=0
has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; }
miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; }
# Harte Altlast-Pfade dürfen nicht mehr vorkommen
if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden"
err=1
fi
if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then
echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden"
err=1
fi
# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein)
has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway"
has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway"
miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor"
has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway"
miss docs/01_Architecture/c4/02-container-de.puml "Ktor"
exit $err
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
+4 -6
View File
@@ -1,9 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p build/diagrams
shopt -s nullglob
for f in docs/architecture/c4/*.puml; do
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
done
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
exec "$ROOT_DIR/.ai/scripts/render-plantuml.sh" "$@"
+4 -133
View File
@@ -1,136 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# validate-links.sh - Link-Validierung für Projektdokumentation (`docs/**`).
# Zweck: Guardrail für die "Docs-as-Code"-Strategie.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$PROJECT_ROOT"
QUICK_MODE=false
while [[ $# -gt 0 ]]; do
case $1 in
--quick)
QUICK_MODE=true
shift
;;
--help|-h)
cat << 'EOF'
Docs Link-Validierung
USAGE:
./.junie/scripts/validate-links.sh [--quick]
BESCHREIBUNG:
Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade.
Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...).
OPTIONEN:
--quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert).
EOF
exit 0
;;
*)
echo "[ERROR] Unbekannter Parameter: $1" >&2
exit 2
;;
esac
done
python3 - <<'PY'
import os
import re
import sys
from pathlib import Path
from urllib.parse import unquote
root = Path.cwd()
docs_dir = root / "docs"
if not docs_dir.is_dir():
print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr)
sys.exit(2)
# Veraltete Pfad-Prüfungen wurden entfernt, da sie zu wartungsintensiv waren.
# Das Skript konzentriert sich nun auf die Validierung der Link-Integrität.
FORBIDDEN_SUBSTRINGS = []
md_files = sorted(docs_dir.rglob("*.md"))
link_pattern = re.compile(r"\]\(([^)]+)\)")
errors = 0
def is_external(target: str) -> bool:
t = target.lower()
return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:")
def strip_fragment_and_query(target: str) -> str:
# remove fragment and query parts
target = target.split("#", 1)[0]
target = target.split("?", 1)[0]
return target
for f in md_files:
text = f.read_text(encoding="utf-8", errors="replace")
for forbidden in FORBIDDEN_SUBSTRINGS:
if forbidden in text:
print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}")
errors += 1
for match in link_pattern.finditer(text):
target = match.group(1).strip()
if not target:
continue
if is_external(target):
continue
if target.startswith("#"):
continue
# drop angle brackets <...> used in markdown for urls with spaces
if target.startswith("<") and target.endswith(">"):
target = target[1:-1]
target = unquote(strip_fragment_and_query(target))
# ignore absolute paths in the repo (we treat them as doc-style links; validate only if relative)
if target.startswith("/"):
continue
# ignore non-file targets (e.g. empty or protocol-less anchors)
if ":" in target.split("/", 1)[0]:
# things like "vscode:..." etc.
continue
# treat as file path relative to markdown file
resolved = (f.parent / target).resolve()
# keep validation within repo
try:
resolved.relative_to(root.resolve())
except ValueError:
print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}")
errors += 1
continue
# allow directories if they contain README.md
if resolved.is_dir():
if not (resolved / "README.md").is_file():
print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}")
errors += 1
continue
if not resolved.exists():
print(f"[ERROR] Broken link: {f} -> {target}")
errors += 1
if errors:
print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler")
sys.exit(1)
print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft")
PY
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
+21
View File
@@ -0,0 +1,21 @@
# .aiignore - Verhindert Token-Waste für Nolik
# Abhängigkeiten & Binaries
build/
.gradle/
*.jar
*.deb
*.msi
# Sensible Daten (auch lokal!)
.env
.env.*
config/docker/certs/
*.pem
*.jks
postgres-data/
valkey-data/
# Doku-Builds (Nolik soll die Source-Files in docs/ lesen, nicht die HTML-Exporte)
build/dokka/
docs/Neumarkt2026/*.pdf
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
+31 -23
View File
@@ -1,35 +1,43 @@
# 🤖 Project Agents & Protocol
# 🤖 Projekt Agenten & Protokoll (Meldestelle)
Dieses Dokument definiert die Zusammenarbeit zwischen dem User (Owner) und den KI-Agenten.
Es dient als "System Prompt" für neue Chat-Sessions.
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.
## 1. Protokoll & Badges
Jeder Agent muss seine Antwort mit einem Badge beginnen, um den Kontext zu setzen. Detaillierte Anweisungen finden sich in den jeweiligen Playbooks.
## 🚀 Strategische Ausrichtung
* **🏗️ [Lead Architect]**: Strategie, Planung, Entscheidungen, Master Roadmap.
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.
## 1. Protokoll & Rollen-Badges
Jede Agenten-Antwort **muss** mit dem entsprechenden Badge beginnen, um den Kontext und die Verantwortlichkeit zu klären.
* **🏗️ [Lead Architect]**: Hüter der **MASTER_ROADMAP**. Verantwortlich für System-Design, Build-Logik (Gradle), Modulstruktur und ADRs.
* [Playbook](docs/04_Agents/Playbooks/Architect.md)
* **🧹 [Curator]**: Dokumentation, Logs, Reports, Aufräumen.
* [Playbook](docs/04_Agents/Playbooks/Curator.md)
* **👷 [Backend Developer]**: Spring Boot, Kotlin, SQL, API-Design.
* **📜 [Rulebook Expert]**: Wächter über **ÖTO & FEI**. Validiert Business-Rules gegen das offizielle Pferdesport-Regelwerk.
* [Playbook](docs/04_Agents/Playbooks/RulebookExpert.md)
* **👷 [Backend Developer]**: Kotlin & Spring Boot Experte. Fokus auf DDD, Persistenz (Postgres) und **Delta-Sync APIs**.
* [Playbook](docs/04_Agents/Playbooks/BackendDeveloper.md)
* **🎨 [Frontend Expert]**: KMP, Compose, State-Management, Auth.
* **🎨 [Frontend Expert]**: KMP & Compose Desktop Spezialist. Implementiert State-Management und High-Performance UI.
* [Playbook](docs/04_Agents/Playbooks/FrontendExpert.md)
* **🖌️ [UI/UX Designer]**: High-Density Design, Wireframes, Usability.
* **🖌️ [UI/UX Designer]**: "Toolsmith" für High-Density Enterprise-UIs. Fokus auf Tastatur-Bedienbarkeit und Effizienz.
* [Playbook](docs/04_Agents/Playbooks/UIUXDesigner.md)
* **🐧 [DevOps Engineer]**: Docker, CI/CD, Gradle, Security.
* **🐧 [DevOps Engineer]**: Infrastruktur-Automatisierung (Docker, Gitea-Actions). Fokus auf Stabilität und lokale Dev-Umgebung.
* [Playbook](docs/04_Agents/Playbooks/DevOpsEngineer.md)
* **🧐 [QA Specialist]**: Test-Strategie, Edge-Cases.
* **🧐 [QA Specialist]**: Test-Stratege (Shift-Left). Fokus auf Unit-, Integration- und Edge-Case-Tests (Testing Pyramid).
* [Playbook](docs/04_Agents/Playbooks/QASpecialist.md)
* **📜 [ÖTO/FEI Rulebook Expert]**: Regelwerks-Wächter, Validierungs-Spezialist, Compliance.
* [Playbook](docs/04_Agents/Playbooks/RulebookExpert.md)
* **🧹 [Curator]**: Wissens-Management & Dokumentations-Check (ADR, Reference, Journal). Beendet jede Session.
* [Playbook](docs/04_Agents/Playbooks/Curator.md)
## 2. Workflow
1. **Kontext:** Lies immer zuerst die `MASTER_ROADMAP` in `docs/01_Architecture/`.
2. **Fokus:** Bearbeite immer nur EINE Aufgabe zur Zeit.
3. **Doku:** Jede Session endet mit einem Eintrag durch den **Curator**.
4. **Code:** Änderungen am Code werden sofort via Tool ausgeführt, nicht nur vorgeschlagen.
## 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.
4. **Doku-as-Code:** Änderungen an Code/Architektur müssen sofort in `docs/` (ADR/Reference) reflektiert werden.
5. **Session-Abschluss:** Jede Session endet mit einem Eintrag durch den **Curator** (Journal oder Artefakt).
## 3. Projekt-Philosophie
* **Startup-Mode:** Wir bauen ein echtes Produkt. Code-Qualität und Geschwindigkeit sind gleich wichtig.
* **Docs-as-Code:** Die Dokumentation ist die Single Source of Truth.
* **Offline-First:** Das System muss ohne Internet funktionieren (Sync).
* **Information Density over White Space:** Wir bauen ein Profi-Werkzeug, kein Spielzeug.
* **Speed over Animation:** Reaktionsgeschwindigkeit der UI hat höchste Priorität.
* **Offline-Authentizität:** Lokale Daten sind die "Source of Truth" für den User; der Server ist das Backup/Sync-Target.
+339
View File
@@ -0,0 +1,339 @@
# Changelog — Meldestelle
Alle wesentlichen Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
> **Versionsschema:** `MAJOR.MINOR.PATCH[-QUALIFIER]`
> - `MAJOR` — Breaking Changes / inkompatible API-Änderungen
> - `MINOR` — Neue Features (abwärtskompatibel)
> - `PATCH` — Bugfixes (abwärtskompatibel)
> - `QUALIFIER` — `SNAPSHOT` (Entwicklung), `beta.N` (Vorversion), leer = Release
---
### [Unreleased]
### Hinzugefügt
- **Onboarding & Desktop-UX - 15.04.2026:**
- **Desktop-App:** Dynamisierung der Statusanzeigen im App-Footer ("Cloud synchronisiert" & "Verbunden").
- **Connectivity-Tracking:** Implementierung des `ConnectivityTracker` (KMP) zur Echtzeit-Überwachung der API-Gateway
Erreichbarkeit.
- **LAN-Erkennung:** Integration des `NetworkDiscoveryService` (mDNS) im Footer zur Anzeige aktiver Instanzen im
lokalen Netzwerk.
- **Onboarding:** Datenfluss vom `SettingsManager` bis in den Footer finalisiert (Anzeige des echten Gerätenamens).
- **Online-Nennung & Integration - 15.04.2026:**
- **Backend (Mail-Service):** Finalisierung des `MailController` für Web-Nennungen inkl. SMTP-Versand via World4You.
- **Frontend (Desktop):** `NennungsEingangScreen` an Live-Daten vom `mail-service` angebunden.
- **Repository:** `NennungRemoteRepository` (KMP) um `holeNennungen()` erweitert.
- **Billing & ÖTO - 15.04.2026:**
- **Sportförderbeitrag:** Automatische Buchung von 1,00 EUR (§16 ÖTO) bei jeder Nennung im `entries-service`
implementiert.
### Behoben
- **Frontend (Desktop):** Behebung von Kompilierungsfehlern in `ScreenPreviews.kt` durch Implementierung der fehlenden
`getStats()` Methode in den `MasterdataRepository`-Mocks.
- **Identity-Modul:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen und Behebung von
Persistenz-Konflikten im `ExposedDeviceRepository`.
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
- **Turnier-Feature:** Behebung eines unsicheren Casts (`Any!` zu `List<String>`) in `TurnierStammdatenTab.kt`.
- **Konfiguration:** Harmonisierung der Ports (Mail-Service auf 8083) in `.env`, `dc-backend.yaml` und
`PlatformConfig.jvm.kt`.
### Hinzugefügt
- **Phase 12 (Abrechnung & Infrastruktur) - 12.04.2026:**
- **Infrastruktur:** Docker-Integration für `billing-service` (Port 8087) und API-Gateway Routing vervollständigt.
- **Service Discovery:** Alle relevanten Microservices (`masterdata`, `events`, `results`, `series`, `billing`) sind nun bei Consul registriert.
- **Frontend Billing:** `BillingRepository` und `BillingViewModel` auf reale API-Anbindung (Ktor) umgestellt; `BillingScreen` funktionalisiert.
- **Backend (Series):** JPA-Entitäten `Serie` und `SeriePunkt` im `series-service` stabilisiert und Flyway-Migrationen für das Datenbankschema erstellt.
- **Fix:** Behebung von IDE-Mapping-Warnungen durch explizite `@Column` Namen in den JPA-Entitäten.
- **Backend Fixes - 12.04.2026:**
- **Infrastruktur:** Behebung von Startfehlern im `events-service` (DataSource) und `masterdata-service` (Consul).
- **Build:** Integration von `results-service` und `series-service` in `settings.gradle.kts`.
- **Domain:** `Serie` und `SeriePunkt` zu `data class` konvertiert (copy() Unterstützung).
- **Phase 11 (Ergebniserfassung & Platzierung) - 12.04.2026:**
- **Backend (Results):** `results-service` um JPA-Entitäten, Repositories und Business-Logik für Platzierungsberechnungen (Wertnote, Zeit, Fehler) ergänzt.
- **Infrastructure:** `dc-backend.yaml` und `GatewayConfig.kt` um den Service `results` (Port 8088) erweitert.
- **Frontend Domain:** `ErgebnisRepository` und `Ergebnis`-Modell für Wertnoten, Zeiten und Status erstellt.
- **Frontend UI:** `ErgebnisEditDialog` zur schnellen Ergebniserfassung hinzugefügt; `TurnierStartlistenTab` ermöglicht nun Erfassung per Zeilen-Klick.
- **Frontend UI:** `TurnierErgebnislistenTab` vervollständigt: Buttons für "Platzierung berechnen" und "Drucken" (PDF) funktionalisiert.
- **Fix:** Kompilierungsprobleme im `TurnierFeatureModule` und `ScreenPreviews.kt` behoben (fehlende `ergebnisRepo` Parameter).
### Hinzugefügt
- **Phase 10.4 (Series-Context Vertiefung) - 12.04.2026:**
- **Backend (Series):** `series-service` um Logik für Streichresultate (`ReglementTyp`) und Bindungsarten (Reiter-zentriert, Pferde-zentriert, Paar-Bindung) erweitert.
- **Infrastructure:** `dc-backend.yaml` und `GatewayConfig.kt` um den Service `series` (Port 8089) erweitert.
- **Frontend Domain:** `SeriesRepository` und Modelle an das neue Ranking-Format (`SerieStandEntry`) angepasst.
- **UI:** `SeriesScreen.kt` überarbeitet: Zeigt nun Reiter- und Pferde-IDs sowie Fortschritt pro Teilnehmer an.
- **Dokumentation:** `MASTER_ROADMAP.md` aktualisiert (Phase 10 & 11 auf 'Completed' gesetzt).
### Hinzugefügt
- **Phase 10.3 (Echter Datenverkehr & Infrastruktur) - 12.04.2026:**
- **Infrastructure:** Docker-Services für `masterdata`, `events` und `zns-import` in `dc-backend.yaml` ergänzt.
- **Gateway:** API-Gateway Routing für Masterdata (`/api/v1/masterdata`) und Events (`/api/v1/events`) konfiguriert.
- **Frontend (Vereine):** `VereinRepository` (Ktor) und `VereinViewModel` implementiert für echtes Anlegen von Veranstaltern.
- **Frontend (Events):** `TurnierViewModel` an das reale `TurnierRepository` angebunden.
- **Fix:** `verein-feature` Abhängigkeiten korrigiert (Network/Ktor).
- **Fix:** Polling-Endpoints im `ZnsImportViewModel` an das neue Gateway-Routing angepasst.
### Hinzugefügt
- **Phase 10.2 (Masterdata-Editoren & Organisation) - 12.04.2026:**
- **Frontend:** `MasterdataEditDialogs.kt` für die Bearbeitung von Reiter- und Pferdedaten direkt im Turnier-Kontext.
- **Frontend:** Erweiterung des `MasterdataRepository` um Schreibzugriffe (`saveReiter`, `savePferd`).
- **Frontend:** Funktionale Suche für Turnierleiter im `Organisation`-Tab via `NennungViewModel` und Masterdata-API.
- **Frontend:** State-Management für Stammdaten-Editoren im `NennungViewModel`.
- **Fix:** Kompilierungsfehler in `ScreenPreviews.kt` behoben (fehlende Interface-Methoden in Mocks).
- **Fix (Desktop Shell):** Fehlendes `turnierFeatureModule` in `main.kt` registriert und Login-Gate in `DesktopApp.kt` optimiert.
### Hinzugefügt
- **Phase 10 (Series-Context & Stammdaten) - 11.04.2026:**
- **Frontend:** Stammdaten-Infrastruktur im `turnier-feature` (Repositories, DTOs, Domänenmodelle) für Reiter, Pferde, Funktionäre und Vereine.
- **Frontend:** `NennungViewModel` zur Steuerung der Suche und Status-Verwaltung von Nennungen.
- **Frontend:** Funktionalisierung des `Nennungen`-Tabs (Suche, Echt-Datenanbindung) und Vorbereitung des `Organisation`-Tabs.
- **Frontend:** `DefaultMasterdataRepository` zur Suche in Reitern, Pferden und Funktionären via Backend-API.
- **Netzwerk:** Erweiterung der `ApiRoutes` um Endpunkte für Masterdata und Nennungen.
- **Phase 10 (Series-Context) Vorbereitung:**
- **Frontend:** Neuer `SeriesScreen.kt` für die Verwaltung von Cups und Meisterschaften (konfigurierbare Reglements).
- **Frontend:** Erweiterung des `AdminUebersichtScreen` (Cockpit) um KPI-Kacheln mit Direkt-Links zu Cups und Meisterschaften.
- **Frontend:** Integration der Series-Navigation in die Breadcrumbs und das globale Routing (`Meisterschaften`, `Cups`).
- **Turnier-Feature Hardening:**
- **Frontend:** `STARTLISTEN` und `ERGEBNISLISTEN` Tabs vollständig an das `BewerbViewModel` angebunden (Bewerbs-Auswahl mit echten Daten).
- **Frontend:** Implementierung der Starter-Anzeige in der Startliste (LazyColumn).
### Geändert
- **Turnier-Feature:** Sichtbarkeit von `BewerbViewModel.generateStartliste()` auf `public` geändert, um den Aufruf aus dem Tab zu ermöglichen.
- **Frontend (Desktop):** `ScreenPreviews.kt` aktualisiert zur Berücksichtigung der neuen ViewModel-Abhängigkeiten (`NennungViewModel`, `MasterdataRepository`).
### [Phase 9] - 11.04.2026
- **Frontend:** Interaktiver Drag & Drop Zeitplan mit automatischem 5-Minuten-Snapping und Konflikt-Visualisierung.
- **Frontend:** "B-Satz Export (ZNS)" Toolbar-Aktion mit integriertem Vorschau-Dialog.
- **Frontend:** "Änderungs-Historie" (Audit-Log) Sektion zur Nachverfolgung von Zeitplan-Anpassungen.
- **Backend:** `audit_log` Persistenz und Abfrage-API für manuelle Eingriffe in Bewerbe.
- **Backend:** ZNS B-Satz Export Endpunkt (`/export/zns/b-satz`) zur Generierung von `BBEWERBE` Datensätzen.
- **Core:** `FixedWidthLineBuilder` zur präzisen Generierung von ZNS-konformen Festbreiten-Formaten.
### Behoben
- **Infrastruktur:** Veraltete `newSuspendedTransaction` in `DatabaseFactory.kt` durch moderne `suspendTransaction` (Exposed v1) ersetzt.
- **Frontend (Desktop):** Kompilierfehler in `ScreenPreviews.kt` behoben, indem fehlende Interface-Methoden im Mock-Repository implementiert wurden.
- **Backend (Tests):** `JdbcSQLSyntaxErrorException` im `BewerbeZeitplanIntegrationTest` durch Korrektur des Schema-Setups (Audit-Log Tabelle) gelöst.
### Hinzugefügt
- **Bugfix**: Behebung von Build-Fehlern im `veranstalter-feature` nach der Paket-Konsolidierung.
- **Frontend**: `FakeVeranstalterRepository` in `commonMain` implementiert, um saubere KMP-DI zu ermöglichen.
- **Frontend**: Veraltete Imports und Referenzen im `meldestelle-desktop` Shell und Previews korrigiert.
- **Architektur:** Fachliches Konzept für Zeitplan-Optimierung (Drag & Drop) erstellt (`konzept-zeitplan-optimierung-de.md`).
- **Architektur:** Spezifikation des Status-Automaten für Nennungen und Synchronisations-Logik (`status-automat-nennungen-de.md`).
- **Rulebook:** Überprüfung und Spezifikation der Parcoursbesichtigung zu Pferd (§43 ÖTO) inkl. 5-Minuten-Puffer-Regel.
- **Backend (Entries):** Erweiterung der Domain-Modelle `Bewerb` und `Abteilung` um Besichtigungs- und Pausen-Konfigurationen.
- **Backend (Entries):** Neues Datenmodell `BesichtigungsBlock` für wettbewerbsübergreifende Parcoursbesichtigungen.
- **Backend (Entries):** API-Endpunkt `PATCH /bewerbe/{id}/zeitplan` für schnelle Zeitplan-Updates implementiert.
- **Backend (Entries):** `StartlistenService` um ÖTO-konforme Zeitberechnung (Besichtigungs-Puffer, Pausen-Intervalle) erweitert.
### Geändert
- Masterdata/Domain: Umbenennungen zur Vereinheitlichung der Terminologie (DE):
- `MasterdataLicenseRepository``LizenzRepository`
- `LicenseMatrixService``LizenzMatrixService`
- `LicenseMatrixServiceImpl``LizenzMatrixServiceImpl`
- Test: `LicenseMatrixServiceTest``LiznezMatrixServiceTest` (exakt nach Vorgabe)
- Infrastructure (Exposed): `LicenseTable``LizenzTable`
- Docs: Begriff „reit_lizenzen“ → „reiterlizenzen“ in Glossar/UL konsolidiert.
### Hinzugefügt
- **Events-Service Bundle:** Vollständige Stabilisierung der `events` Services (Domain, Infrastructure, API, Service).
- **Domain:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen (Kotlin 2.1.20+) und Harmonisierung mit dem Rulebook-Expert.
- **Infrastructure:** Anpassung an den `org.jetbrains.exposed.v1` Namespace und Implementierung von UUID-Konvertierungen zwischen `kotlin.uuid.Uuid` (Domain) und `java.util.UUID` (DB).
- **API:** Refactoring des `VeranstaltungController` zur direkten Repository-Nutzung (Alignment mit `entries` Service).
- **Service-Config:** Umstellung auf Flyway-basiertes Tenant-Schema-Management in `EventsDatabaseConfiguration`.
- **Build:** Behebung des `shadowJar` Fehlers in `events-infrastructure` durch Entfernen des unnötigen `ktor` Plugins in der Library-Schicht.
- Masterdata: Automatisches Seeding aller Reiterlizenzen (license_matrix) beim Start des `masterdata-service` via `ReiterlizenzenSeeder` (idempotent; SPRINGEN: LIZENZFREI,R1R4; DRESSUR: LIZENZFREI,RD1RD3).
- **ZNS-Import (LIZENZ01.dat):** Robuster Lizenz-Tokenizer und Normalizer implementiert.
- Erkennung: `RD1..RD4`, `R1..R4`, `S1..S4`, `D2..D4`, Kombis `R{n}D{m}`, `R{n}S{k}`, `RDS4` (rechts-/letztes Vorkommen gewinnt).
- Normalisierung: `S*→R*`, `D*→RD*`, `RD4→RD3` (bis Enum verfügbar), `R{n}S{k}→Rmax(n,k)`, `R{n}D{m}→R{n}+RD{m}`.
- Integration: `ZnsReiterParser` füllt `lizenzen`-Liste (1:n) entsprechend und leitet `lizenzKlasse` bei fehlendem 4SpaltenCode aus Token ab.
- QA: Neue Unit-Tests (Tokenizer) für Beispiele `R2S3`, `R2D4`, `RD2` u. a.; alle Parser-Tests grün.
- **Core:** Modularisierte ZNS-Parser eingeführt (`ZnsVereinParser`, `ZnsReiterParser`, `ZnsPferdParser`, `ZnsFunktionaerParser`) zur Verbesserung der Wartbarkeit und Unterstützung von Einzelimporten.
- **Fix:** SQL-Migrationsfehler in `V010` behoben, indem die Umbenennung der Spalte `name` in `verein_name` durch einen idempotenten `DO`-Block abgesichert wurde (behebt "Unable to resolve column 'name'").
- **Infrastructure:** Datenbank-Migration `V010` hinzugefügt, um das Schema final mit den `Exposed`-Modellen zu synchronisieren.
- **Infrastructure:** Datei-Archivierung für hochgeladene ZNS-ZIP-Dateien im `ZnsImportOrchestrator` implementiert.
- **Infrastructure:** `ZnsImportService` vollständig auf die neuen spezialisierten Parser umgestellt und als Spring-Bean im Backend registriert.
- **QA:** Umfassende Test-Suite `ZnsParserTest.kt` mit realen ZNS-Daten (Hämmerle, Neuwirth, etc.) erstellt; Korrektur der Extraktions-Logik für Mitgliedsnummern (Position 147) und Funktionär-Daten (RICHT01).
- **QA:** Neue Betriebsanleitung für ZNS-Importer Tests erstellt: `docs/07_Infrastructure/runbooks/ZNS_Importer_Test_Manual.md`.
- **Infrastructure:** `MasterdataDatabaseConfiguration` korrigiert: Expliziter Aufruf von `Database.connect()` hinzugefügt, um Abstürze beim Anwendungsstart ("No database specified") zu beheben.
- **Infrastructure:** `application.yml` im `masterdata-service` vervollständigt (DataSource-Konfiguration mit `pg-user`/`pg-password` und Flyway-Aktivierung).
- **Domain:** Legacy-Spezifikationen für ZNS-Schnittstellen (Import/Export) formalisiert:
- `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Pflichtenheft_V2.4.md` (Basis-Satzarten A-N)
- `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Erweiterung-Schnittstelle_2014.md` (XML-Erweiterung, LinkID-Logik)
- **QA B-2:** `OnboardingValidator`-Objekt extrahiert; `OnboardingValidatorTest.kt` (17 Unit-Tests: Pflichtfeld-Guard,
Doppelklick-Schutz, Abbrechen-Reset, rememberSaveable-Regression)
- **QA B-3:** `AbteilungsRegelServiceTest.kt` um 14 Tests erweitert: CSN-C-NEU ≤95 cm / ≥100 cm Pflicht-Teilung,
ORGANISATORISCH, SEPARATE_SIEGEREHRUNG, Caprilli-Regression, Grenzfälle 90/110 cm
- **Domain:** `AbteilungsTeilungsTypE` um `ORGANISATORISCH` und `SEPARATE_SIEGEREHRUNG` erweitert
### Behoben
- **Masterdata/Infrastructure:** Kompilierfehler in `AltersklasseRepositoryImpl` durch Vereinheitlichung der Exposed-Tabellendefinition behoben:
- `AltersklassenTable``AltersklasseTable`
- Spalte `altersklassen_code``altersklasse_code`
- Tabellenname `altersklassen``altersklasse`
- **Masterdata/API:** Fehlendes Interface-Mapping ergänzt: `RegulationRepository` enthält nun `findAllTurnierklassen()`; `ExposedRegulationRepository` implementiert die Methode und `RegulationController` kompiliert wieder.
- **ZNS-Import:** `AltersklassenExposedRepository` korrigiert (richtiger Domain-Typ `AltersklasseDefinition`, Mapping von `SparteE` und Zeitstempeln).
- **Migration V013:** Idempotent und robust gemacht. Alle `ALTER TABLE ... RENAME`-Operationen laufen nun nur, wenn die Quell-Tabelle existiert (Fix für "Unable to resolve table 'bundesland'/'turnierklasse'").
- **Lizenz-Validierung:** `LicenseMatrixServiceImpl` um Cross-Discipline-Mapping R↔RD (ÖTO-Äquivalenzen) erweitert. Damit funktionieren Fälle wie Dressur-Starts mit Spring-Lizenz (R1→RD1, R2→RD2, R3/R4→RD3) bzw. umgekehrt konsistent.
- **Domain:** Striktere Spartenlizenz-Prüfung in `Reiter.hasLizenzForSparte` implementiert (RD1..RD3 nur DRESSUR; R1..R4 nur SPRINGEN). Behebt Testfehler „isEligible verweigert Start ohne passende Spartenlizenz“ im `LicenseMatrixServiceTest`.
### Behoben
- **Backend (Entries):** Fehlschlagenden Unit-Test `berechneStartzeiten sollte Zeiten korrekt aufsummieren` korrigiert; der Test berücksichtigt nun den neuen 5-minütigen ÖTO-konformen Puffer nach der Parcoursbesichtigung (§43).
- **Frontend (Desktop):** Build-Fehler ("No matching variant") beim `funktionaer-feature` behoben; fehlendes `build.gradle.kts` mit JVM-Target und Compose/Koin-Abhängigkeiten ergänzt.
- **Frontend (Desktop):** Massive Inkonsistenzen in der Paketstruktur des `veranstalter-feature` bereinigt; alle Komponenten (ViewModel, Screens, Mocks) auf das Standardpaket `at.mocode.frontend.features.veranstalter` konsolidiert, um Redeklarationen und Import-Fehler zu beheben.
- **Frontend (Desktop):** Kompilierfehler im `VeranstalterDetailScreen` durch korrekte Paket-Referenzierung des `FakeVeranstaltungStore` gelöst.
### Dokumentation
- **Masterdata/Docs:** `REITER_LIZENZEN.md` überarbeitet:
- Strikte Sparten-Trennung dokumentiert (RD1..RD3 nur Dressur; R1..R4 nur Springen).
- Dressur-Tabelle korrigiert (R-Lizenzen entfernen; RD-Pflicht je Klasse).
- Validierungslogik ergänzt (2-stufig: Spartenlizenz → Max-Turnierklasse; R↔RD Mapping nur zur Kappung, nicht zur Eligibility).
- Vielseitigkeit (CCN/CCI) ergänzt: kumulative Anforderungen (Dressur RD* UND Springen R* je Klasse); Startkartenregel für Einsteiger.
- Fahren (CAN/CAI) ergänzt: aktueller Systemzustand ohne `F*`Lizenzen dokumentiert; Teilnahme über Startkarte/Ausschreibung, geplante EnumErweiterung vermerkt.
- §15Tabelle (kompakt) integriert und auf ÖTOReferenz verlinkt; Bedeutungen „B,C“ und „LP“ erläutert. Hinweis aufgenommen, dass `RD4` derzeit nicht als Enum vorhanden ist und wie `RD3` behandelt wird.
- Kombinationsreihen gemäß §15 ergänzt: `R1S2`, `R1S3`, `R1S4`, `R2S3`, `R2S4`, `R3S4` (neuer Unterabschnitt 2.6 mit Tabelle, identische Spalten wie 2.5).
### Behoben
- **Masterdata:** Qualifikations-Management für Funktionäre (Richter/Parcoursbauer) professionalisiert: Umstellung von unstrukturiertem Text auf offizielle ÖTO/FEI Master-Daten Referenzen (`QualifikationMasterTable`).
- **Masterdata:** Fehlende Tabelle `funktionaer_qualifikation` in der Initialisierung beider Services (`masterdata` und `zns-import`) ergänzt, um `PSQLException` während des ZNS-Imports zu beheben.
- **Infrastructure:** Start-Probleme des `masterdata-service` endgültig behoben: Port-Konflikt zwischen Spring Boot (Management/Actuator) und dem Gateway (8081) durch Umzug auf Port 8086 (gemäß Infrastruktur-Vorgaben) gelöst.
- **Infrastructure:** Port-Konflikt im `masterdata-service` durch Trennung der Bind-Adressen (Spring: 127.0.0.1, Ktor: 0.0.0.0) und Bereinigung verwaister Prozesse stabilisiert.
- **Core:** Veraltete `ZnsLegacyParsersTest.kt` entfernt; alle Tests sind nun in `ZnsParserTest.kt` konsolidiert.
- **Domain:** Fehlschlagenden `LicenseMatrixServiceTest` behoben; fehlende `reiterLizenz`-Daten in Test-Reitern ergänzt und Fallback-Logik in `LicenseMatrixServiceImpl` für spartenübergreifende Lizenzen (z.B. Springlizenz für Dressur-Basis) stabilisiert.
- **Infrastructure:** Fehlschlagenden `RegulationSeedVerificationTest` behoben; Testdaten an das neue Modell (`reiterLizenz` Feld) angepasst.
- **Infrastructure:** Kompilierfehler 'Unresolved reference lizenzKlasse' in `ReiterExposedRepository` behoben; fehlendes Feld `lizenzKlasse` zu `ReiterTable` und Datenbank-Migration `V010` hinzugefügt.
- **Onboarding:** `remember``rememberSaveable` für `geraetName`, `sharedKey`, `znsStatus` in `OnboardingScreen.kt` (
Felder gingen bei Zurück-Navigation verloren)
- **AbteilungsRegelService:** CSN-C-NEU Pflicht-Teilungslogik implementiert (≤95 cm: ohne/mit Lizenz; ≥100 cm: R1/R2+);
`SparteE`-Import ergänzt
- Desktop-Packaging konfiguriert: `.deb` (Linux), `.msi` (Windows), `.dmg` (macOS)
- Zentrale Versionsdatei `version.properties` (Single Source of Truth für SemVer)
- Automatisches Git-Tagging via CI/CD (`release.yml` Gitea Actions Workflow)
- `CHANGELOG.md` eingeführt (dieses Dokument)
---
## [1.0.6-SNAPSHOT] — 2026-04-10
### Hinzugefügt
- **Entries-Domain:** Strukturiertes Abteilungs-Warnungssystem gemäß ÖTO § 39 implementiert.
- Neues Value Object `AbteilungsWarnung` und Enum `AbteilungsWarnungCodeE` für präzise Fehlermeldungen und ÖTO-Referenzen.
- Erweiterung von `Bewerb` um die Methode `validateStrukturellesTeilung` zur Prüfung vorgeschriebener Abteilungsstrukturen (z.B. Lizenz-Trennung bei CSN-C-NEU, Stilspringen, Caprilli).
- Umstellung des `CompetitionWarningService` und `AbteilungsRegelService` auf das neue strukturierte Warnungsmodell.
- **Entries-Service:** Erweiterung der REST-API (`BewerbeController`) um die Auslieferung von Warnungen in den DTOs (`BewerbResponse`).
- **Frontend (Turnier-Feature):** Visuelle Integration der Abteilungs-Warnungen in der Bewerbe-Liste.
- Anzeige eines Warn-Icons (gelb) bei Regelverstößen.
- Tooltip-Funktionalität zur Anzeige der detaillierten Warnungstexte und ÖTO-Paragraphen.
- Erweiterung des `BewerbUiModel` und Repositories zur Unterstützung der Warnungs-Metadaten.
### Geändert
- **QA:** `AbteilungsRegelServiceTest` und `BewerbTest` auf das neue Warnungssystem aktualisiert und um Tests für strukturelle Teilungen (CSN Stilspringen, Caprilli) erweitert.
- **KMP:** Korrektur von veralteten `Instant`-Deprecations in Testklassen (`kotlin.time.Instant`).
## [1.0.5-SNAPSHOT] — 2026-04-06
### Geändert
- **Masterdata:** Bereinigung und Standardisierung von Masterdaten-Tabellen (Mehrzahl-Konvention):
- `bundesland` -> `bundeslaender`
- `qualifikation_master` -> `funktionaers_qualifikationen`
- `reiter_lizenz` -> `reit_lizenzen`
- `turnierklasse` -> `turnier_klassen`
- **Seeding:** Umfassende Erweiterung der Seeder für Funktionärs-Qualifikationen, Turnierklassen und Turnier-Sparten gemäß ÖTO.
- **Data Modeling:** Einführung der Tabelle `turnier_sparten` und Entfernung der redundanten `reiter_sparte`.
- **Infrastructure:** Datenbank-Migration `V013` implementiert alle Schema-Änderungen und Umbenennungen.
## [1.0.4-SNAPSHOT] — 2026-04-06
### Hinzugefügt
- **Reiter-Lizenzen:** Strukturierte Speicherung von Lizenzen (STARTKARTE, REITERLIZENZ, FAHRLIZENZ) in einer 1:n Relation (`ReiterLizenzTable`).
- **Altersklassen:** Einführung von Enums (`ReiterAltersKlasseE`) für präzise Filterung und Validierung im Domain-Modell und Parser.
- **Mitgliedsnummer:** Validierungs-Logik gemäß ÖTO-Spezifikation (Bundesland-Präfix 1-9) in `Reiter.kt` implementiert.
- **ZNS-Import:** `ZnsReiterParser` erweitert, um Lizenzen und Altersklassen-Enums direkt aus LIZENZ01.DAT zu extrahieren.
- **Persistenz:** `ReiterExposedRepository` unterstützt nun das transaktionale Speichern und Laden der 1:n Lizenzen.
## [1.0.3-SNAPSHOT] — 2026-04-06
### Hinzugefügt
- **Masterdata:** Refactoring der Reiter-Stammdaten (LIZENZ01.DAT). Bundesland, Verein und Nation werden nun über Master-Tabellen referenziert.
- **Domain:** Validierungslogik für die 8-stellige OEPS-Mitgliedsnummer im `Reiter`-Modell implementiert.
- **Infrastructure:** Neue Tabellen `reiter_lizenz` (1:n Beziehung) und Migration `V012` zur Schemaanpassung und Datenbereinigung eingeführt.
- **ZNS-Import:** Automatisches Auflösen von Relationen (Verein nach Name, Bundesland nach Nummer, Nation nach ISO-Code) während des Reiter-Imports.
### Behoben
- **Infrastruktur:** Consul Health-Check für `masterdata-service` korrigiert (Port 8086 für Actuator).
- **Masterdaten:** `MasterdataSeeder` für Nationen und Bundesländer hinzugefügt, um Datenvollständigkeit nach Volume-Cleanup sicherzustellen.
- **Datenintegrität:** Heilungs-Logik (`fixReiterForeignKeys`) implementiert, die Reiter-Datensätze nachträglich mit Masterdaten verknüpft.
- **Code-Qualität:** Redundante `BundeslandTable` Definition in `ReiterTable.kt` entfernt.
- **Infrastruktur:** `BeanDefinitionOverrideException` im `zns-import-service` durch Konsolidierung der Repositories in `RepositoryConfiguration` behoben.
- **Service-Discovery:** Fehlende Consul-Registrierung des `masterdata-service` durch Hinzufügen der Discovery-Dependency und Konfiguration behoben.
- **Build:** Kompilierfehler in `BundeslandExposedRepository.kt` behoben (inkonsistente Rückgabetypen im `BundeslandRepository`-Interface).
- **Infrastruktur:** Fehlendes Autowiring im `zns-import-service` durch explizite Bean-Definitionen für alle Repositories in `ZnsImportServiceApplication.kt` behoben.
- **Domain:** Kompilierfehler in `Bundesland.kt` behoben (uninitialisierte Eigenschaft `bundeslandId` entfernt).
- **Migration:** SQL-Syntaxfehler in `V012` behoben (korrekter Fremdschlüssel-Constraint für `reiter_lizenz` und Wiederherstellung des `DO $$`-Blocks).
## [1.0.2-SNAPSHOT] — 2026-04-06
### Geändert
- **ZNS-Import:** `ZnsImportService` stabilisiert (ZipInputStream-Management korrigiert), um sequentielle Imports in Tests zu ermöglichen.
- **Test-Vollständigkeit:** `ZnsImportServiceTest` korrigiert (Mocking für Reiter-Suche ergänzt, Testdaten für Funktionäre an Int-Parser angepasst). Alle 9 Tests nun grün.
- **Data Modeling:** Redundante Kontakt- und Adressdaten aus `FunktionaerTable` entfernt; stattdessen Verknüpfung zu `ReiterTable` via `reiter_id` hinzugefügt. (Bereinigung der Felder erfolgte in `V010`).
- **Import:** ZNS-Importer verknüpft nun Funktionäre automatisch mit vorhandenen Reitern anhand des Namens (Nachname, Vorname).
- **Infrastructure:** `findByName` in `ReiterRepository` implementiert für effiziente Suche während des Imports.
- **Datenbank:** Migration `V011` hinzugefügt, um die Fremdschlüsselbeziehung zu etablieren.
## [1.0.1-SNAPSHOT] — 2026-04-05
### Geändert
- **Masterdata:** Funktionär-Datenmodell und API bereinigt und vollständig dokumentiert. Konsistente Verwendung von `satzId` (statt `satzID`) in allen Schichten (Domain, Infrastructure, API).
- **Refactoring:** `DomVerein` zu `Verein`, `DomReiter` zu `Reiter`, `DomPferd` zu `Pferd` und `DomFunktionaer` zu `Funktionaer` umbenannt (Domain, Infrastructure, API, Core).
- **Domain:** `personId` ist nun optional (`nullable`) bei `Verein`, `Reiter`, `Pferd` und `Funktionaer`, um ZNS-Initialimporte zu unterstützen.
- **Infrastructure:** `VereinTable`, `ReiterTable`, `HorseTable` und `FunktionaerTable` synchronisiert; `personId` ist nun optional.
- **API:** `VereinController`, `ReiterController`, `HorseController` und `FunktionaerController` (DTOs/Requests) an die neuen Modelle angepasst.
- **Doku:** `Ubiquitous_Language.md` und `MASTER_ROADMAP.md` an das neue Namensschema (`Reiter`, `Pferd`, `Funktionaer`) angepasst.
### Behoben
- **ZNS-Import:** Kompatibilitätsprobleme in `ZnsLegacyParsers` und `ZnsImportService` nach Domain-Refactorings behoben (UUID-Person-Referenzen und Enum-Synchronisation).
- **Domain:** Felder `kurzname` und `oepsRegionNummer` bei `Verein` entfernt (nicht in VEREIN01.DAT vorhanden).
## [1.0.0-SNAPSHOT] — 2026-04-03
### Hinzugefügt
- **Sprint A:** Docker-Compose-Setup, Healthchecks für alle Services
- **Sprint B:** CI/CD Pipeline für Compose Desktop Tests (headless, Xvfb)
- **Sprint B:** Gradle-Build-Optimierungen (Cache, Parallel, Wrapper 9.4.0)
- **Sprint B:** Onboarding-Wizard (Veranstalter, Verein, Turnier, Bewerb, Abteilung)
- **Sprint B:** `BewerbRepository`, `AbteilungRepository`, `DefaultTurnierRepository`
- **Sprint B:** `ReiterProfilEditDialog`, `PferdProfilEditDialog` mit `MsValidationWrapper`
- **Sprint B:** ÖTO-Regelwerk als Regulation-as-Data (Lizenz-/Altersmatrix, V008/V009 Migrations)
- **Sprint B:** Tenant-Isolation Grundstruktur (Multi-Tenant Postgres-Schemas)
- **Sprint B:** Architektur-Tests (`:platform:architecture-tests`)
### Geändert
- Gradle Wrapper auf `9.3.1` aktualisiert
- JVM-Toolchain auf Java 25 angehoben
---
<!-- Versions-Links (anpassen sobald Gitea-URL bekannt) -->
[Unreleased]: https://gitea.mo-code.at/meldestelle/Meldestelle-Biest/compare/v1.0.0-SNAPSHOT...HEAD
[1.0.0-SNAPSHOT]: https://gitea.mo-code.at/meldestelle/Meldestelle-Biest/releases/tag/v1.0.0-SNAPSHOT
+47 -15
View File
@@ -1,6 +1,6 @@
# Meldestelle
> Modulares System für Pferdesportveranstaltungen — gebaut mit Domain-Driven Design, Kotlin Multiplatform und Microservices.
> DesktopFirst Meldestelle: Offlinefähige ComposeDesktopApp mit optionalem BackendStack. Domänengetrieben (DDD), Kotlin Multiplatform.
[![CI/CD Pipeline](https://git.mo-code.at/mocode-software/meldestelle/actions/workflows/docker-publish.yaml/badge.svg)](https://git.mo-code.at/mocode-software/meldestelle/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -15,20 +15,28 @@ Die gesamte Projektdokumentation (Architektur, Fachdomäne, Entwickler-Anleitung
| Bereich | Inhalt |
|-----------------------------------------------|---------------------------------------------|
| [01_Architecture](./docs/01_Architecture) | Master Roadmap, ADRs, C4-Modelle |
| [01_Architecture](./docs/01_Architecture) | Master Roadmap, ADRs, C4Modelle, DesktopKonzept |
| [02_Guides](./docs/02_Guides) | Setup-Anleitungen, Entwickler-Guidelines |
| [03_Domain](./docs/03_Domain) | Fachlichkeit, Turnierregeln, Entities |
| [07_Infrastructure](./docs/07_Infrastructure) | Docker, Keycloak, CI/CD, Zora-Infrastruktur |
Wesentliche Architektur-Referenz: [OfflineFirst Desktop & Backend (Kurzkonzept)](./docs/01_Architecture/konzept-offline-first-desktop-backend-de.md)
---
## 🏗 Tech Stack
## 🖥 Primärer Fokus: DesktopApp
- Compose Multiplatform (JVM Desktop) als primäre Zielplattform
- OfflineFirst: Lokale SQLDelightDB, Synchronisation optional
- MultiTenantBackend optional für Sync/Verwaltung (SchemaperTenant, vgl. ADR0021)
## 🏗️ Tech Stack (aktueller Stand)
| Schicht | Technologie |
|-----------------------|---------------------------------------------------|
| **Backend** | Kotlin, Spring Boot 3.x, Spring Cloud Gateway |
| **Frontend** | Kotlin Multiplatform (KMP), Compose Multiplatform |
| **Datenbank** | PostgreSQL + Exposed / JPA |
| **Frontend (primär)** | Kotlin Multiplatform (KMP), Compose Multiplatform |
| **Backend** | Kotlin (Ktor/Spring Boot), Spring Cloud Gateway |
| **Datenbank** | SQLDelight (lokal), PostgreSQL (Backend) |
| **Auth** | Keycloak (OAuth2 / OIDC) |
| **Cache** | Valkey |
| **Service Discovery** | Consul |
@@ -38,27 +46,51 @@ Die gesamte Projektdokumentation (Architektur, Fachdomäne, Entwickler-Anleitung
---
## 🚀 Quick Start (Lokal)
## 🚀 Quick Start (lokale Entwicklung)
Empfohlen: Starte die DesktopApp direkt aus dem Repo. Der BackendStack ist optional und wird nur für Sync/Integration benötigt.
### A) DesktopApp starten (ohne Backend)
Voraussetzungen: JDK 21+, aktuelle Gradle Wrapper verwendet.
```bash
# 1. Umgebungsvariablen vorbereiten (nur beim ersten Mal)
cp .env.example .env
# DesktopShell ausführen (Compose Desktop)
./gradlew :frontend:shells:meldestelle-desktop:run
```
# 2. Infrastruktur starten (Postgres, Keycloak, Valkey, Consul, Zipkin)
Hinweise:
- Beim ersten Start wird die lokale Datenbank initialisiert (OfflineFirst).
- Architektur-Referenz: `docs/06_Frontend/MVVM_UDF_Pattern.md` (UDF/MVVM für ViewModels)
### B) Optional: BackendStack per Docker starten
```bash
# 1. Umgebungsvariablen (nur beim ersten Mal)
cp .env.example .env || true
# 2. Infrastruktur (Postgres, Keycloak, Valkey, Consul, Zipkin)
docker compose -f docker-compose.yaml -f dc-infra.yaml up -d
# 3. Backend-Services starten (Gateway, Ping-Service)
# 3. BackendServices (Gateway, Masterdata, Ping, …)
docker compose -f docker-compose.yaml -f dc-backend.yaml up -d
# 4. Ops-Stack starten (Prometheus, Grafana)
# 4. OpsStack (Prometheus, Grafana)
docker compose -f docker-compose.yaml -f dc-ops.yaml up -d
# 5. Optional: Web-App starten
# 5. Optional: WebShell
docker compose -f docker-compose.yaml -f dc-gui.yaml up -d
```
> ⚠️ **Reihenfolge beachten:** Infra muss `healthy` sein, bevor Backend gestartet wird.
> Keycloak benötigt ~6090 Sekunden zum Hochfahren.
> ⚠️ Reihenfolge beachten: Infra muss `healthy` sein, bevor Backend gestartet wird. Keycloak benötigt ~6090 Sekunden.
---
## 🧭 Legacy (V1) Hinweise
- Ältere V1Abschnitte/Anleitungen (WebFirst/MicroservicesOnly) gelten als DEPRECATED.
- Verwende für lokale Entwicklung primär die DesktopApp (siehe Quick Start). DockerStacks sind optional für Integration/Sync.
- In der Doku sind V1Seiten entsprechend markiert oder werden sukzessive bereinigt (siehe Roadmaps Sprint C4).
### Wichtige lokale Ports
File diff suppressed because it is too large Load Diff
@@ -1,72 +0,0 @@
# Tech-Stack Referenz: Kotlin 2.3.0 & Java 25 (KMP)
### 1. Kern-Spezifikationen
| Komponente | Version | Status |
| --- |----------| --- |
| **Kotlin** | `2.3.0` | Stabil (K2 Compiler standardmäßig aktiv) |
| **Java (JDK)** | `25` | LTS (Long-Term Support) |
| **Gradle** | `9.2.1` | Erforderlich für JDK 25 Support |
| **Android Plugin (AGP)** | `8.8.0+` | Empfohlen für Gradle 9.x Kompatibilität |
---
### 2. Gradle Konfiguration (`build.gradle.kts`)
Für ein **Kotlin Multiplatform (KMP)** Projekt ist die Java Toolchain-Konfiguration entscheidend, um sicherzustellen, dass der Kotlin-Compiler und die JVM-Targets Java 25 korrekt ansprechen.
```kotlin
plugins {
kotlin("multiplatform") version "2.3.0"
id("com.android.library") version "8.8.0" // Falls Android Target genutzt wird
}
kotlin {
// Globale Toolchain-Definition für alle JVM/Android Targets
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
jvm {
compilations.all {
compilerOptions.configure {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_25)
}
}
}
// Weitere Targets (Beispiel iOS)
iosArm64()
iosSimulatorArm64()
}
```
---
### 3. Gradle Wrapper Update
Damit das Projekt Java 25 erkennt, muss der Wrapper auf dem neuesten Stand sein:
**Terminal-Befehl:**
```bash
./gradlew wrapper --gradle-version 9.2.1 --distribution-type all
```
---
### 4. Wichtige Kompatibilitätshinweise für das Plugin
* **IDE-Version:** Stelle sicher, dass **IntelliJ IDEA 2025.3** (oder neuer) installiert ist, da erst diese Version die volle Unterstützung für JDK 25 Sprachfeatures und das Kotlin 2.3.0 Plugin bietet.
* **K2 Compiler:** Kotlin 2.3.0 nutzt den K2-Compiler. Falls das Google AI Pro Plugin Code-Analysen durchführt, sollte es auf dem K2-Modus basieren.
* **Bytecode:** Java 25 Bytecode wird nur generiert, wenn das `jvmTarget` explizit auf `25` gesetzt ist. Andernfalls verbleibt Kotlin standardmäßig bei einer niedrigeren Version (meist 1.8 oder 11), was die neuen JDK-Features einschränken könnte.
---
### 5. Bekannte Features in diesem Setup
* **Java 25 Features:** Unterstützung für die finalen Versionen von *Scoped Values* und *Structured Concurrency*.
* **Kotlin 2.3.0 Features:** Nutzung von `explicit backing fields` und dem verbesserten `unused return value` Checker.
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -1,63 +0,0 @@
### Analyse und Strategie zur Wiederherstellung und Strukturverbesserung
Es ist eine klassische Situation: Nach einem großen Technologie-Upgrade (Kotlin 2.1.0+, Java 25, Spring Boot 3.5.x) knirscht es oft an den Schnittstellen. Da dein `ping-service` als technischer Blueprint dient, ist er der absolut richtige Startpunkt.
Hier ist der Schlachtplan, um Ordnung zu schaffen und die hexagonale Architektur sauber zu etablieren:
### 1. Wo beginnen? Bottom-Up vs. Top-Down
Da deine Infrastruktur aktuell nicht stabil läuft, empfehle ich einen **"Core-First"** Ansatz, gefolgt vom **Backend-Durchstich**.
* **Zuerst: Core & Platform:** Ohne eine stabile Basis (`platform-bom`, `platform-dependencies`, `core-domain`) werden die anderen Module immer wieder Kompilierfehler werfen.
* **Dann: Der technische vertikale Durchstich (`ping-service`):** Sobald die Plattform steht, reparieren wir den Weg: `Infrastruktur (Docker) -> Ping-Service -> Gateway`.
* **Zuletzt: Frontend:** Das Frontend (BFF-Gedanke) wird erst dann stabil, wenn die API-Contracts des Backends wieder verlässlich geliefert werden.
### 2. Ordnung schaffen: Der "Clean Desk" im Projekt
Bevor wir Code fixen, müssen wir die Build-Umgebung aufräumen:
1. **Version Catalog Synchronität:** Deine `libs.versions.toml` nutzt bereits Java 25 und Kotlin 2.1.0. Prüfe, ob alle Gradle-Plugins (insbesondere das `compose-multiplatform` und `spring-boot` Plugin) mit Kotlin 2.1.0 kompatibel sind. Oft ist hier ein Downgrade auf die letzte stabile Version oder ein Upgrade auf Alpha/Beta-Versionen nötig, wenn man "Bleeding Edge" (Java 25) nutzt.
2. **Modul-Konsolidierung (DDD):** Wie besprochen, solltest du die "Modul-Explosion" reduzieren.
* **Vorschlag:** Statt 5 Module pro Domain (`api`, `common`, `domain`, `infrastructure`, `service`), reduziere es auf maximal zwei:
* `domain-api`: Nur DTOs und Interfaces (für KMP-Sharing mit dem Frontend).
* `domain-service`: Die gesamte Implementierung (Hexagonal strukturiert in Packages).
### 3. Hexagonale Architektur im `ping-service` umsetzen
Dein `ping-service` ist aktuell noch sehr "Spring-lastig" (Controller ruft Service mit Circuit Breaker direkt auf). Für eine echte hexagonale Vorlage strukturiere das Modul `ping-service` intern wie folgt um:
```text
at.mocode.ping.service
├── adapter
│ ├── in
│ │ └── web (PingController - Dein primärer Port-Adapter)
│ └── out
│ └── persistence (PingRepositoryAdapter - Sekundärer Port-Adapter)
├── application
│ ├── port
│ │ ├── in (PingUseCase - Das Interface für den Controller)
│ │ └── out (PingOutputPort - Interface für die Datenbank)
│ └── service (PingService - Hier liegt die Business Logik, OHNE Spring-Annotationen wo möglich)
└── domain
└── model (PingEntity/Value Objects)
```
**Der Vorteil:** Wenn du dieses Muster im `ping-service` einmal sauber hast, kopierst du diese Package-Struktur für `registry`, `events` etc.
### 4. Konkrete Schritte zur Reparatur
**Schritt 1: Infrastruktur-Check (Docker)**
Stelle sicher, dass die Basisdienste laufen. Java 25 benötigt oft neuere Container-Images.
* Check `docker-compose.yaml`: Laufen Postgres und Keycloak?
* `ping-service` application.yaml: Aktiviere die Datenbank-Verbindung (JPA), die aktuell noch auskommentiert ist, um den "echten" Test zu ermöglichen.
**Schritt 2: Backend API-Gateway Fix**
Dein Gateway ist der Wächter. Wenn die Security-Konfiguration (`SecurityConfig.kt`) wegen Bibliotheks-Änderungen in Spring Security 7/Spring Boot 3.5 hakt, ist das Priorität 1.
* Test: Kannst du den `ping-service` direkt aufrufen? Wenn ja, funktioniert das Gateway-Routing?
**Schritt 3: Frontend (BFF) Anpassung**
Nutze das Frontend als Konsument. Wenn du den `PingApiClient` im Frontend hast, sollte dieser gegen das **Gateway** (BFF-Pattern) laufen, nicht direkt gegen den Service.
### Empfehlung zur Vorgehensweise (Prioritäten):
1. **Gradle-Build stabilisieren:** Alle `:platform:*` und `:core:*` Module müssen mit `./gradlew build` fehlerfrei durchlaufen.
2. **Ping-SCS fertigstellen:** Implementiere eine minimale Datenbank-Speicherung im `ping-service`. Das ist der Beweis, dass die JPA/Hibernate-Konfiguration mit Java 25 harmoniert.
3. **Gateway-Security:** Stelle sicher, dass das JWT von Keycloak korrekt zum `ping-service` durchgereicht wird.
**Soll ich dir bei einem dieser spezifischen Schritte (z.B. der hexagonalen Package-Struktur für den Ping-Service) mit konkretem Code helfen?**
File diff suppressed because it is too large Load Diff
@@ -1,323 +0,0 @@
Meldestelle on  main [✘»!+?] via 🅶 v9.2.1 via ☕ v25.0.1 via 🅺 v2.3.0
./gradlew :backend:infrastructure:gateway:test --stacktrace
Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details
Type-safe project accessors is an incubating feature.
> Task :backend:infrastructure:gateway:test
WebFluxSmokeTest > should load reactive web context and serve smoke endpoint() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
GatewayFiltersTests > should preserve existing correlation ID header() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
GatewayFiltersTests > should handle requests with X-Forwarded-For header() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayFiltersTests > should apply admin rate limit for admin users() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayFiltersTests > should add rate limiting headers() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayFiltersTests > should add correlation ID header when not present() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayFiltersTests > should apply different rate limits for auth endpoints() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayFiltersTests > should enforce rate limiting after exceeding limit() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayFiltersTests > should apply higher rate limit for authenticated users() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
KeycloakGatewayIntegrationTest > should initialize Spring context with Keycloak configuration() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
GatewayApplicationTests > contextLoads() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
FallbackControllerTests > should handle POST requests to masterdata fallback() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
FallbackControllerTests > should handle POST requests to default fallback() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should return masterdata service fallback response() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should handle POST requests to members fallback() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should handle POST requests to events fallback() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should return valid JSON structure for all fallback responses() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should handle POST requests to auth fallback() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > sollte Members Service Fallback Response zurueckgeben() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should have consistent error response structure() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > sollte Events Service Fallback Response zurueckgeben() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should return auth service fallback response() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > sollte Horses Service Fallback Response zurueckgeben() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should handle POST requests to horses fallback() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
FallbackControllerTests > should return default fallback response for unknown service() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should handle different HTTP methods allowed in CORS() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
GatewaySecurityTests > should handle complex CORS scenarios() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should handle PUT requests with CORS headers() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should allow requests from meldestelle domain() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should allow credentials in CORS requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should set max age for CORS requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should handle authorization headers in CORS requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should not duplicate CORS headers due to deduplication filter() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should handle CORS preflight requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should allow requests from localhost origins() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should maintain security headers in responses() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should handle POST requests with CORS headers() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewaySecurityTests > should handle DELETE requests with CORS headers() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayRoutingTests > should route ping service requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: java.lang.IllegalStateException at SpringBootCondition.java:60
Caused by: java.lang.IllegalArgumentException at ClassUtils.java:372
Caused by: java.lang.ClassNotFoundException at BuiltinClassLoader.java:642
GatewayRoutingTests > should route horses service requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayRoutingTests > should handle gateway info path request() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayRoutingTests > should route members service requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayRoutingTests > auth route is not configured anymore() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayRoutingTests > should route masterdata service requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
GatewayRoutingTests > should route events service requests() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
45 tests completed, 45 failed
> Task :backend:infrastructure:gateway:test FAILED
[Incubating] Problems report is available at: file:///home/stefan/WsMeldestelle/Meldestelle/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':backend:infrastructure:gateway:test'.
> There were failing tests. See the report at: file:///home/stefan/WsMeldestelle/Meldestelle/backend/infrastructure/gateway/build/reports/tests/test/index.html
* Try:
> Run with --scan to generate a Build Scan (powered by Develocity).
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':backend:infrastructure:gateway:test'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:135)
at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:288)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:133)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:121)
at org.gradle.api.internal.tasks.execution.ProblemsTaskPathTrackingTaskExecuter.execute(ProblemsTaskPathTrackingTaskExecuter.java:41)
at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.DefaultNodeExecutor.executeLocalTaskNode(DefaultNodeExecutor.java:55)
at org.gradle.execution.plan.DefaultNodeExecutor.execute(DefaultNodeExecutor.java:34)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:355)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:343)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:339)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:84)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:339)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:328)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47)
Caused by: org.gradle.api.internal.exceptions.MarkedVerificationException: There were failing tests. See the report at: file:///home/stefan/WsMeldestelle/Meldestelle/backend/infrastructure/gateway/build/reports/tests/test/index.html
at org.gradle.api.tasks.testing.AbstractTestTask.handleTestFailures(AbstractTestTask.java:703)
at org.gradle.api.tasks.testing.AbstractTestTask.handleCollectedResults(AbstractTestTask.java:535)
at org.gradle.api.tasks.testing.AbstractTestTask.executeTests(AbstractTestTask.java:527)
at org.gradle.api.tasks.testing.Test.executeTests(Test.java:714)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:125)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:58)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:51)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:29)
at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:252)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:237)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:220)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:203)
at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:170)
at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)
at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:42)
at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:75)
at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:68)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:38)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:69)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:46)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:39)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:28)
at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)
at org.gradle.internal.execution.steps.BuildCacheStep.executeAndStoreInCache(BuildCacheStep.java:145)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$executeWithCache$4(BuildCacheStep.java:101)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$executeWithCache$5(BuildCacheStep.java:101)
at org.gradle.internal.Try$Success.map(Try.java:170)
at org.gradle.internal.execution.steps.BuildCacheStep.executeWithCache(BuildCacheStep.java:85)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$0(BuildCacheStep.java:74)
at org.gradle.internal.Either$Left.fold(Either.java:116)
at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:46)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:35)
at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:75)
at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:35)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:49)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:27)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:64)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:35)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:62)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:40)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:76)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:45)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:136)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:66)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:38)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:36)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:23)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:75)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:41)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.lambda$execute$0(AssignMutableWorkspaceStep.java:35)
at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:297)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:31)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:22)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:40)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:44)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:31)
at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:64)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:132)
... 30 more
Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/9.2.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 23s
17 actionable tasks: 4 executed, 13 up-to-date
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
Placeholder to ensure directory exists
-109
View File
@@ -1,109 +0,0 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
jvm("desktop")
js(IR) {
// WICHTIG: Als Library kompilieren für Webpack Federation
binaries.library()
generateTypeScriptDefinitions()
browser {
commonWebpackConfig {
cssSupport {
enabled.set(true)
}
}
}
}
// Wasm vorerst deaktiviert
/*
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
*/
sourceSets {
commonMain {
dependencies {
implementation(projects.frontend.core.domain)
// implementation(projects.frontend.core.designSystem) // REMOVED: Circular dependency
implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.network)
implementation(projects.frontend.core.localDb)
// Features - REMOVED: Circular dependency. Shared should NOT depend on features.
// implementation(projects.frontend.features.authFeature)
// implementation(projects.frontend.features.pingFeature)
// KMP Bundles
implementation(libs.bundles.kmp.common)
implementation(libs.bundles.compose.common)
// Ktor (used directly in shared/di and shared/network)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentNegotiation)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.serialization.kotlinx.json)
// Serialization
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
// implementation(libs.sqldelight.coroutines) // Wird transitiv über core:localDb geladen
// Compose
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
// Koin
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
}
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
// implementation(libs.sqldelight.driver.sqlite) // Wird transitiv über core:localDb geladen
}
}
val jsMain by getting {
dependencies {
implementation(libs.ktor.client.js)
// implementation(libs.sqldelight.driver.web) // Wird transitiv über core:localDb geladen
// Webpack Plugin für Federation Support (falls benötigt)
implementation(devNpm("copy-webpack-plugin", "12.0.0"))
}
}
/*
val wasmJsMain by getting {
dependencies {
implementation(libs.ktor.client.js)
}
}
*/
}
}
@@ -1,12 +0,0 @@
package at.mocode.shared.core
data class AppConfig(
val gatewayUrl: String,
val isDebug: Boolean
)
// Standard-Config für Local Development
val devConfig = AppConfig(
gatewayUrl = "http://localhost:8081",
isDebug = true
)
@@ -1,29 +0,0 @@
package at.mocode.shared.core
/**
* Shared application configuration constants for clients.
* These defaults target local development environments.
*/
object AppConstants {
// Gateway base URL (reverse proxy / API gateway)
// Used by NetworkConfig via PlatformConfig
const val GATEWAY_URL: String = "http://localhost:8081"
// Keycloak configuration
const val KEYCLOAK_URL: String = "http://localhost:8180"
const val KEYCLOAK_REALM: String = "meldestelle"
// Use 'postman-client' for Desktop App Password Flow (Direct Access Grants enabled)
// 'web-app' is for Browser Flow (PKCE)
// TODO: Make this platform-dependent (Desktop vs Web)
const val KEYCLOAK_CLIENT_ID: String = "web-app"
// DEV ONLY: Client Secret for 'postman-client' (Confidential Client)
// In Production, this should NEVER be in the frontend code.
// For the Desktop App Pilot, we use this to simulate a secure client.
// For 'web-app' (Public Client), this is not needed/used if configured correctly,
// but our AuthApiClient might be sending it.
const val KEYCLOAK_CLIENT_SECRET: String = "postman-secret-123"
// Removed unused browser flow URLs (registerUrl, loginUrl, etc.) as we focus on Desktop App.
}
@@ -1,27 +0,0 @@
package at.mocode.shared.data.repository
import at.mocode.shared.domain.model.PingData
import at.mocode.shared.domain.model.Resource
import at.mocode.shared.domain.repository.PingRepository
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class PingRepositoryImpl(
private val httpClient: HttpClient
) : PingRepository {
override suspend fun checkSystemStatus(): Resource<PingData> {
return try {
// Der HttpClient hat die BaseURL schon konfiguriert (siehe NetworkModule)
val response = httpClient.get("/api/ping/simple").body<PingData>()
Resource.Success(response)
} catch (e: Exception) {
// Hier fangen wir Netzwerkfehler ab und machen sie "hübsch" für die UI
Resource.Error(
message = "Verbindung fehlgeschlagen: ${e.message ?: "Unbekannter Fehler"}",
code = "NETWORK_ERROR"
)
}
}
}
@@ -1,50 +0,0 @@
package at.mocode.shared.di
import at.mocode.shared.core.AppConfig
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import org.koin.dsl.module
val networkModule = module {
// 1. JSON Konfiguration (Global verfügbar)
single {
Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
}
}
// 2. HttpClient (Singleton)
single {
val config = get<AppConfig>()
val jsonConfig = get<Json>()
HttpClient {
// Standard-URL setzen
defaultRequest {
url(config.gatewayUrl)
contentType(ContentType.Application.Json)
}
install(ContentNegotiation) {
json(jsonConfig)
}
install(Logging) {
level = if (config.isDebug) LogLevel.INFO else LogLevel.NONE
logger = Logger.DEFAULT
}
install(HttpTimeout) {
requestTimeoutMillis = 10000
connectTimeoutMillis = 10000
}
}
}
}
@@ -1,26 +0,0 @@
package at.mocode.shared.di
import at.mocode.shared.core.devConfig
import at.mocode.frontend.core.network.networkModule
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.module
// Das Modul für die Config
val configModule = module {
single { devConfig } // Später können wir hier PROD/DEV umschalten
}
// Basismodule, die immer geladen werden sollen (ohne Feature/Core-Cross-Imports)
val baseSharedModules = listOf(
configModule,
// Network module provides DI-only HttpClient (safe to be shared across features)
networkModule
)
// Helper zum Starten von Koin (wird von der App aufgerufen)
// Weitere Module (z. B. networkModule) können über appDeclaration hinzugefügt werden.
fun initKoin(appDeclaration: KoinAppDeclaration = {}) = startKoin {
modules(baseSharedModules)
appDeclaration()
}
@@ -1,41 +0,0 @@
package at.mocode.shared.domain.model
import kotlinx.serialization.Serializable
/**
* Generischer Wrapper für API-Antworten.
*/
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: ApiError? = null
)
@Serializable
data class ApiError(
val code: String,
val message: String,
val details: Map<String, String> = emptyMap()
)
/**
* Das Ergebnis eines Repository-Aufrufs.
* Die UI kennt nur das hier, keine HTTP-Exceptions!
*/
sealed class Resource<out T> {
data class Success<T>(val data: T) : Resource<T>()
data class Error(val message: String, val code: String? = null) : Resource<Nothing>()
data object Loading : Resource<Nothing>()
}
/**
* Datenmodell für den Ping.
*/
@Serializable
data class PingData(
val status: String,
val timestamp: String,
val service: String
)
@@ -1,8 +0,0 @@
package at.mocode.shared.domain.repository
import at.mocode.shared.domain.model.PingData
import at.mocode.shared.domain.model.Resource
interface PingRepository {
suspend fun checkSystemStatus(): Resource<PingData>
}
@@ -1,178 +0,0 @@
package at.mocode.shared.navigation
import at.mocode.shared.presentation.actions.AppAction
import at.mocode.shared.presentation.store.AppStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Navigation manager for handling routing and navigation logic
*/
class NavigationManager(
private val store: AppStore
) {
/**
* Current route as a flow
*/
val currentRoute: Flow<String> = store.state.map { it.navigation.currentRoute }
/**
* Navigation history as a flow
*/
val navigationHistory: Flow<List<String>> = store.state.map { it.navigation.history }
/**
* Can go back flag as a flow
*/
val canGoBack: Flow<Boolean> = store.state.map { it.navigation.canGoBack }
/**
* Navigate to a specific route
*/
fun navigateTo(route: String) {
store.dispatch(AppAction.Navigation.NavigateTo(route))
}
/**
* Navigate back to the previous route
*/
fun navigateBack() {
store.dispatch(AppAction.Navigation.NavigateBack)
}
/**
* Replace current route without adding to history
*/
fun replaceRoute(route: String) {
store.dispatch(AppAction.Navigation.UpdateHistory(route))
}
/**
* Clear navigation history and navigate to the route
*/
fun navigateAndClearHistory(route: String) {
// First clear by replacing with the new route
store.dispatch(AppAction.Navigation.UpdateHistory(route))
}
/**
* Get current route value (non-reactive)
*/
fun getCurrentRoute(): String = store.state.value.navigation.currentRoute
/**
* Check if we can navigate back
*/
fun canNavigateBack(): Boolean = store.state.value.navigation.canGoBack
}
/**
* Route definitions for the application
*/
object Routes {
const val HOME = "/"
const val LOGIN = "/login"
const val DASHBOARD = "/dashboard"
const val PROFILE = "/profile"
const val SETTINGS = "/settings"
const val PING = "/ping"
// Auth-related routes
object Auth {
const val LOGIN = "/auth/login"
const val LOGOUT = "/auth/logout"
const val REGISTER = "/auth/register"
const val FORGOT_PASSWORD = "/auth/forgot-password"
}
// Admin routes
object Admin {
const val DASHBOARD = "/admin/dashboard"
const val USERS = "/admin/users"
const val SETTINGS = "/admin/settings"
}
// Feature routes
object Features {
const val PING = "/features/ping"
const val REPORTS = "/features/reports"
const val NOTIFICATIONS = "/features/notifications"
}
}
/**
* Route validation and utilities
*/
object RouteUtils {
/**
* Check if a route requires authentication
*/
fun requiresAuth(route: String): Boolean {
return when {
route.startsWith("/auth/") && route != Routes.Auth.LOGIN -> false
route == Routes.HOME -> false
route == Routes.LOGIN -> false
else -> true
}
}
/**
* Check if a route is for admin only
*/
fun requiresAdmin(route: String): Boolean {
return route.startsWith("/admin/")
}
/**
* Get the default route for authenticated users
*/
fun getDefaultAuthenticatedRoute(): String = Routes.DASHBOARD
/**
* Get the default route for unauthenticated users
*/
fun getDefaultUnauthenticatedRoute(): String = Routes.LOGIN
/**
* Validate route format
*/
fun isValidRoute(route: String): Boolean {
return route.startsWith("/") && route.isNotBlank()
}
/**
* Parse route parameters (simple implementation)
*/
fun parseRouteParams(route: String): Map<String, String> {
val params = mutableMapOf<String, String>()
// Simple query parameter parsing
if (route.contains("?")) {
val parts = route.split("?")
if (parts.size == 2) {
val queryParams = parts[1].split("&")
queryParams.forEach { param ->
val keyValue = param.split("=")
if (keyValue.size == 2) {
params[keyValue[0]] = keyValue[1]
}
}
}
}
return params
}
/**
* Get clean route without parameters
*/
fun getCleanRoute(route: String): String {
return if (route.contains("?")) {
route.split("?")[0]
} else {
route
}
}
}
@@ -1,75 +0,0 @@
package at.mocode.shared.navigation
import at.mocode.shared.presentation.state.NavigationState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
/**
* Interface für das Persistieren von Navigation State
*/
interface NavigationPersistence {
suspend fun saveNavigationState(state: NavigationState)
fun getNavigationState(): Flow<NavigationState?>
suspend fun clearNavigationState()
}
/**
* Default implementation ohne echte Persistierung (In-Memory)
* Platform-spezifische Implementierungen können echte Persistierung bereitstellen
*/
class DefaultNavigationPersistence : NavigationPersistence {
private var currentState: NavigationState? = null
override suspend fun saveNavigationState(state: NavigationState) {
currentState = state
}
override fun getNavigationState(): Flow<NavigationState?> {
return flowOf(currentState)
}
override suspend fun clearNavigationState() {
currentState = null
}
}
/**
* Navigation History Manager mit Persistierung
*/
class NavigationHistoryManager(
private val persistence: NavigationPersistence
) {
companion object {
private const val MAX_HISTORY_SIZE = 50
}
suspend fun saveRoute(route: String, history: List<String>) {
val state = NavigationState(
currentRoute = route,
history = history.takeLast(MAX_HISTORY_SIZE),
canGoBack = history.isNotEmpty()
)
persistence.saveNavigationState(state)
}
fun getPersistedState() = persistence.getNavigationState()
suspend fun clear() = persistence.clearNavigationState()
/**
* Optimiert die History für bessere Performance
*/
private fun optimizeHistory(history: List<String>): List<String> {
// Entfernt Duplikate in Folge und behält nur die letzten N Einträge
return history
.fold(emptyList<String>()) { acc, route ->
if (acc.lastOrNull() != route) acc + route else acc
}
.takeLast(MAX_HISTORY_SIZE)
}
suspend fun addToHistory(newRoute: String, currentHistory: List<String>) {
val optimizedHistory = optimizeHistory(currentHistory + newRoute)
saveRoute(newRoute, optimizedHistory.dropLast(1))
}
}
@@ -1,27 +0,0 @@
package at.mocode.shared.network
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
object HttpClientConfig {
fun createClient(
baseUrl: String = "http://localhost:8080"
): HttpClient = HttpClient {
// Content negotiation with JSON (based on PingApiClient pattern)
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
fun createClientWithBaseUrl(baseUrl: String): HttpClient {
return createClient(baseUrl)
}
}
@@ -1,172 +0,0 @@
package at.mocode.shared.network
import at.mocode.shared.domain.model.ApiError
import io.ktor.client.network.sockets.*
import io.ktor.client.plugins.*
import kotlinx.io.IOException
/**
* Custom exceptions for network operations
*/
sealed class NetworkException(
message: String,
cause: Throwable? = null,
val apiError: ApiError
) : Exception(message, cause) {
class ConnectionException(
message: String = "Connection failed",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "CONNECTION_ERROR",
message = message,
details = mapOf("type" to "network_connectivity")
)
)
class TimeoutException(
message: String = "Request timed out",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "TIMEOUT_ERROR",
message = message,
details = mapOf("type" to "request_timeout")
)
)
class ServerException(
statusCode: Int,
message: String = "Server error",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "SERVER_ERROR",
message = message,
details = mapOf(
"type" to "server_error",
"status_code" to statusCode.toString()
)
)
)
class ClientException(
statusCode: Int,
message: String = "Client error",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "CLIENT_ERROR",
message = message,
details = mapOf(
"type" to "client_error",
"status_code" to statusCode.toString()
)
)
)
class AuthenticationException(
message: String = "Authentication failed",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "AUTHENTICATION_ERROR",
message = message,
details = mapOf("type" to "authentication_failure")
)
)
class AuthorizationException(
message: String = "Authorization failed",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "AUTHORIZATION_ERROR",
message = message,
details = mapOf("type" to "authorization_failure")
)
)
class UnknownException(
message: String = "Unknown error occurred",
cause: Throwable? = null
) : NetworkException(
message = message,
cause = cause,
apiError = ApiError(
code = "UNKNOWN_ERROR",
message = message,
details = mapOf("type" to "unknown_error")
)
)
}
/**
* Extension function to convert various exceptions to NetworkException
*/
fun Throwable.toNetworkException(): NetworkException {
return when (this) {
is ConnectTimeoutException -> NetworkException.TimeoutException(
message = "Connection timeout: ${this.message}",
cause = this
)
is SocketTimeoutException -> NetworkException.TimeoutException(
message = "Socket timeout: ${this.message}",
cause = this
)
is ResponseException -> when (this.response.status.value) {
401 -> NetworkException.AuthenticationException(
message = "Authentication required",
cause = this
)
403 -> NetworkException.AuthorizationException(
message = "Access forbidden",
cause = this
)
in 400..499 -> NetworkException.ClientException(
statusCode = this.response.status.value,
message = "Client error: ${this.message}",
cause = this
)
in 500..599 -> NetworkException.ServerException(
statusCode = this.response.status.value,
message = "Server error: ${this.message}",
cause = this
)
else -> NetworkException.UnknownException(
message = "HTTP error: ${this.message}",
cause = this
)
}
is IOException -> NetworkException.ConnectionException(
message = "Network connection failed: ${this.message}",
cause = this
)
is NetworkException -> this
else -> NetworkException.UnknownException(
message = "Unexpected error: ${this.message}",
cause = this
)
}
}
@@ -1,220 +0,0 @@
package at.mocode.shared.network
import at.mocode.shared.domain.model.ApiError
import kotlinx.coroutines.delay
// Using platform-agnostic timestamp handling
/**
* Simple timestamp provider for multiplatform compatibility
*/
expect fun currentTimeMillis(): Long
/**
* Network utilities for handling retry logic and resilience
*/
object NetworkUtils {
/**
* Retry configuration for network operations
*/
data class RetryConfig(
val maxAttempts: Int = 3,
val initialDelayMs: Long = 1000L,
val maxDelayMs: Long = 10000L,
val backoffMultiplier: Double = 2.0,
val retryableExceptions: Set<String> = setOf(
"CONNECTION_ERROR",
"TIMEOUT_ERROR",
"SERVER_ERROR"
)
)
/**
* Execute operation with retry logic
*/
suspend fun <T> withRetry(
config: RetryConfig = RetryConfig(),
operation: suspend () -> RepositoryResult<T>
): RepositoryResult<T> {
var lastError: ApiError? = null
var currentDelay = config.initialDelayMs
repeat(config.maxAttempts) { attempt ->
try {
val result = operation()
// Return success immediately
if (result.isSuccess()) {
return result
}
// Check if the error is retryable
val error = result.getErrorOrNull()
if (error != null && shouldRetry(error, config)) {
lastError = error
// Don't delay on the last attempt
if (attempt < config.maxAttempts - 1) {
delay(currentDelay)
currentDelay = minOf(
(currentDelay * config.backoffMultiplier).toLong(),
config.maxDelayMs
)
}
} else {
// Non-retryable error, return immediately
return result
}
} catch (e: Exception) {
val networkException = e.toNetworkException()
lastError = networkException.apiError
if (shouldRetry(networkException.apiError, config)) {
if (attempt < config.maxAttempts - 1) {
delay(currentDelay)
currentDelay = minOf(
(currentDelay * config.backoffMultiplier).toLong(),
config.maxDelayMs
)
}
} else {
return RepositoryResult.Error(networkException.apiError)
}
}
}
// All attempts exhausted, return last error
return RepositoryResult.Error(
lastError ?: ApiError(
code = "MAX_RETRIES_EXCEEDED",
message = "Maximum retry attempts exceeded"
)
)
}
/**
* Check if an error should trigger a retry
*/
private fun shouldRetry(error: ApiError, config: RetryConfig): Boolean {
return config.retryableExceptions.contains(error.code)
}
/**
* Network connectivity checker (simplified for shared module)
*/
object ConnectivityChecker {
private var isOnline: Boolean = true
private var lastCheckMillis: Long = 0L
fun setOnlineStatus(online: Boolean) {
isOnline = online
lastCheckMillis = currentTimeMillis()
}
fun isOnline(): Boolean = isOnline
fun getLastCheckMillis(): Long = lastCheckMillis
/**
* Simple connectivity test by attempting a lightweight operation
*/
suspend fun checkConnectivity(testOperation: suspend () -> Boolean): Boolean {
return try {
val result = testOperation()
setOnlineStatus(result)
result
} catch (_: Exception) {
setOnlineStatus(false)
false
}
}
}
/**
* Circuit breaker pattern for network operations
*/
class CircuitBreaker(
private val failureThreshold: Int = 5,
private val recoveryTimeoutMs: Long = 60000L,
private val successThreshold: Int = 3
) {
private enum class State { CLOSED, OPEN, HALF_OPEN }
private var state = State.CLOSED
private var failureCount = 0
private var successCount = 0
private var lastFailureTime = 0L
suspend fun <T> execute(operation: suspend () -> RepositoryResult<T>): RepositoryResult<T> {
when (state) {
State.OPEN -> {
if (currentTimeMillis() - lastFailureTime >= recoveryTimeoutMs) {
state = State.HALF_OPEN
successCount = 0
} else {
return RepositoryResult.Error(
ApiError(
code = "CIRCUIT_BREAKER_OPEN",
message = "Circuit breaker is open, requests blocked"
)
)
}
}
State.HALF_OPEN -> {
// Allow limited requests to test recovery
}
State.CLOSED -> {
// Normal operation
}
}
return try {
val result = operation()
if (result.isSuccess()) {
onSuccess()
} else {
onFailure()
}
result
} catch (e: Exception) {
onFailure()
val networkException = e.toNetworkException()
RepositoryResult.Error(networkException.apiError)
}
}
private fun onSuccess() {
failureCount = 0
when (state) {
State.HALF_OPEN -> {
successCount++
if (successCount >= successThreshold) {
state = State.CLOSED
}
}
else -> {
state = State.CLOSED
}
}
}
private fun onFailure() {
failureCount++
lastFailureTime = currentTimeMillis()
if (failureCount >= failureThreshold) {
state = State.OPEN
}
}
fun getState(): String = state.name
fun getFailureCount(): Int = failureCount
}
}
@@ -1,18 +0,0 @@
package at.mocode.shared.network
import at.mocode.shared.domain.model.ApiError
/**
* Einheitlicher Ergebnis-Typ für Repository-/Netzwerkoperationen.
*/
sealed class RepositoryResult<out T> {
data class Success<T>(val value: T) : RepositoryResult<T>()
data class Error(val apiError: ApiError) : RepositoryResult<Nothing>()
}
fun <T> RepositoryResult<T>.isSuccess(): Boolean = this is RepositoryResult.Success
fun <T> RepositoryResult<T>.getErrorOrNull(): ApiError? = when (this) {
is RepositoryResult.Success -> null
is RepositoryResult.Error -> this.apiError
}
@@ -1,37 +0,0 @@
package at.mocode.shared.presentation.actions
import at.mocode.shared.presentation.state.Notification
import at.mocode.frontend.core.domain.models.User
import at.mocode.frontend.core.domain.models.AuthToken
sealed class AppAction {
// Auth Actions
sealed class Auth : AppAction() {
data class LoginStart(val username: String, val password: String) : Auth()
data class LoginSuccess(val user: User, val token: AuthToken) : Auth()
data class LoginFailure(val error: String) : Auth()
object Logout : Auth()
data class RefreshToken(val newToken: AuthToken) : Auth()
}
// Navigation Actions
sealed class Navigation : AppAction() {
data class NavigateTo(val route: String) : Navigation()
object NavigateBack : Navigation()
data class UpdateHistory(val route: String) : Navigation()
}
// UI Actions
sealed class UI : AppAction() {
object ToggleDarkMode : UI()
data class SetLoading(val isLoading: Boolean) : UI()
data class ShowNotification(val notification: Notification) : UI()
data class DismissNotification(val id: String) : UI()
}
// Network Actions
sealed class Network : AppAction() {
data class SetOnlineStatus(val isOnline: Boolean) : Network()
data class UpdateLastSync(val timestamp: String) : Network()
}
}
@@ -1,55 +0,0 @@
package at.mocode.shared.presentation.state
import kotlinx.serialization.Serializable
import at.mocode.frontend.core.domain.models.User
import at.mocode.frontend.core.domain.models.AuthToken
@Serializable
data class AppState(
val auth: AuthState = AuthState(),
val navigation: NavigationState = NavigationState(),
val ui: UiState = UiState(),
val network: NetworkState = NetworkState()
)
@Serializable
data class AuthState(
val isAuthenticated: Boolean = false,
val user: User? = null,
val token: AuthToken? = null,
val isLoading: Boolean = false,
val error: String? = null
)
@Serializable
data class NavigationState(
val currentRoute: String = "/",
val history: List<String> = emptyList(),
val canGoBack: Boolean = false
)
@Serializable
data class UiState(
val isDarkMode: Boolean = false,
val isLoading: Boolean = false,
val notifications: List<Notification> = emptyList()
)
@Serializable
data class NetworkState(
val isOnline: Boolean = true,
val lastSync: String? = null
)
@Serializable
data class Notification(
val id: String,
val title: String,
val message: String,
val type: NotificationType = NotificationType.INFO,
val timestamp: String
)
enum class NotificationType {
INFO, SUCCESS, WARNING, ERROR
}
@@ -1,156 +0,0 @@
package at.mocode.shared.presentation.store
import at.mocode.shared.presentation.state.AppState
import at.mocode.shared.presentation.actions.AppAction
import at.mocode.shared.presentation.state.AuthState
import at.mocode.shared.presentation.state.NavigationState
import at.mocode.shared.presentation.state.NetworkState
import at.mocode.shared.presentation.state.UiState
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class AppStore(
private val dispatcher: CoroutineDispatcher = Dispatchers.Main
) {
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
private val _state = MutableStateFlow(AppState())
val state: StateFlow<AppState> = _state.asStateFlow()
fun dispatch(action: AppAction) {
scope.launch {
val currentState = _state.value
val newState = reduce(currentState, action)
_state.value = newState
// Handle side effects
handleSideEffect(action, newState)
}
}
private fun reduce(currentState: AppState, action: AppAction): AppState {
return when (action) {
is AppAction.Auth -> currentState.copy(
auth = reduceAuth(currentState.auth, action)
)
is AppAction.Navigation -> currentState.copy(
navigation = reduceNavigation(currentState.navigation, action)
)
is AppAction.UI -> currentState.copy(
ui = reduceUI(currentState.ui, action)
)
is AppAction.Network -> currentState.copy(
network = reduceNetwork(currentState.network, action)
)
}
}
private fun reduceAuth(currentAuth: AuthState, action: AppAction.Auth): AuthState {
return when (action) {
is AppAction.Auth.LoginStart -> currentAuth.copy(
isLoading = true,
error = null
)
is AppAction.Auth.LoginSuccess -> currentAuth.copy(
isAuthenticated = true,
user = action.user,
token = action.token,
isLoading = false,
error = null
)
is AppAction.Auth.LoginFailure -> currentAuth.copy(
isAuthenticated = false,
user = null,
token = null,
isLoading = false,
error = action.error
)
is AppAction.Auth.Logout -> AuthState()
is AppAction.Auth.RefreshToken -> currentAuth.copy(
token = action.newToken
)
}
}
private fun reduceNavigation(currentNav: NavigationState, action: AppAction.Navigation): NavigationState {
return when (action) {
is AppAction.Navigation.NavigateTo -> currentNav.copy(
currentRoute = action.route,
history = currentNav.history + currentNav.currentRoute,
canGoBack = true
)
is AppAction.Navigation.NavigateBack -> {
val newHistory = currentNav.history.dropLast(1)
currentNav.copy(
currentRoute = newHistory.lastOrNull() ?: "/",
history = newHistory,
canGoBack = newHistory.isNotEmpty()
)
}
is AppAction.Navigation.UpdateHistory -> currentNav.copy(
currentRoute = action.route
)
}
}
private fun reduceUI(currentUI: UiState, action: AppAction.UI): UiState {
return when (action) {
is AppAction.UI.ToggleDarkMode -> currentUI.copy(
isDarkMode = !currentUI.isDarkMode
)
is AppAction.UI.SetLoading -> currentUI.copy(
isLoading = action.isLoading
)
is AppAction.UI.ShowNotification -> currentUI.copy(
notifications = currentUI.notifications + action.notification
)
is AppAction.UI.DismissNotification -> currentUI.copy(
notifications = currentUI.notifications.filter { it.id != action.id }
)
}
}
private fun reduceNetwork(currentNetwork: NetworkState, action: AppAction.Network): NetworkState {
return when (action) {
is AppAction.Network.SetOnlineStatus -> currentNetwork.copy(
isOnline = action.isOnline
)
is AppAction.Network.UpdateLastSync -> currentNetwork.copy(
lastSync = action.timestamp
)
}
}
private suspend fun handleSideEffect(action: AppAction, newState: AppState) {
when (action) {
is AppAction.Auth.LoginSuccess -> {
// Auto-save token to local storage
// TODO: Implement storage
}
is AppAction.Auth.Logout -> {
// Clear local storage
// TODO: Implement storage cleanup
}
else -> { /* No side effects */
}
}
}
fun cleanup() {
scope.cancel()
}
}
@@ -1,3 +0,0 @@
package at.mocode.shared.network
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
@@ -1,5 +0,0 @@
package at.mocode.shared.network
import kotlin.js.Date
actual fun currentTimeMillis(): Long = Date().getTime().toLong()
@@ -1,3 +0,0 @@
package at.mocode.shared.network
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
+35 -83
View File
@@ -1,26 +1,26 @@
# ===================================================================
# Multi-stage Dockerfile for Meldestelle API Gateway
# Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts
# Version: 2.2.2 - Optimized for Monorepo (Fixed frontend paths after refactoring)
# Version: 2.6.0 - Reliable Monorepo Build
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.3.1
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG BUILD_DATE
ARG VERSION
ARG VERSION=1.0.0-SNAPSHOT
# ===================================================================
# Build Stage
# ===================================================================
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
FROM eclipse-temurin:${JAVA_VERSION}-jdk-alpine AS builder
ARG VERSION
ARG BUILD_DATE
LABEL stage=builder
LABEL service="api-gateway"
LABEL maintainer="Meldestelle Development Team"
LABEL stage=builder \
service="api-gateway" \
maintainer="Meldestelle Development Team"
WORKDIR /workspace
@@ -32,58 +32,23 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.jvmargs=-Xmx2g \
-XX:+UseParallelGC \
-XX:MaxMetaspaceSize=512m"
ENV GRADLE_USER_HOME=/root/.gradle
ENV GRADLE_USER_HOME=/home/gradle/.gradle
# Copy gradle wrapper and configuration files
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
COPY gradle/ gradle/
# 1. Copy full project structure for a reliable monorepo build
# .dockerignore should be used to exclude unnecessary files (IDE, logs, etc.)
COPY . .
RUN chmod +x gradlew
# Copy platform and core dependencies
COPY platform/ platform/
COPY core/ core/
# 2. Build the service
RUN --mount=type=cache,target=/root/.gradle/caches \
--mount=type=cache,target=/root/.gradle/wrapper \
./gradlew :backend:infrastructure:gateway:bootJar --no-daemon --info
# Copy backend directories
COPY backend/infrastructure/ backend/infrastructure/
COPY backend/services/ backend/services/
COPY contracts/ contracts/
# Create dummy frontend directories to satisfy settings.gradle.kts include paths
# This prevents Gradle from failing configuration phase without copying actual frontend code
RUN mkdir -p \
frontend/core/auth \
frontend/core/domain \
frontend/core/design-system \
frontend/core/navigation \
frontend/core/network \
frontend/core/local-db \
frontend/core/sync \
frontend/features/ping-feature \
frontend/features/nennung-feature \
frontend/shared \
frontend/shells/meldestelle-portal \
frontend/shells/meldestelle-desktop \
frontend/features/zns-import-feature \
docs
# Copy root build configuration
COPY build.gradle.kts ./
# Download and cache dependencies
RUN --mount=type=cache,id=gradle-cache-gateway,target=/home/gradle/.gradle/caches \
--mount=type=cache,id=gradle-wrapper-gateway,target=/home/gradle/.gradle/wrapper \
./gradlew :backend:infrastructure:gateway:dependencies --info
# Build the application
RUN --mount=type=cache,id=gradle-cache-gateway,target=/home/gradle/.gradle/caches \
--mount=type=cache,id=gradle-wrapper-gateway,target=/home/gradle/.gradle/wrapper \
./gradlew :backend:infrastructure:gateway:bootJar --info
# Extract JAR layers
RUN mkdir -p build/dependency && \
(cd build/dependency; java -Djarmode=layertools -jar /workspace/backend/infrastructure/gateway/build/libs/*.jar extract)
# 3. Extract layers
WORKDIR /builder
RUN cp /workspace/backend/infrastructure/gateway/build/libs/*.jar app.jar && \
java -Djarmode=layertools -jar app.jar extract
# ===================================================================
# Runtime Stage
@@ -94,19 +59,15 @@ ARG BUILD_DATE
ARG VERSION
ARG JAVA_VERSION
ENV JAVA_VERSION=${JAVA_VERSION} \
VERSION=${VERSION} \
BUILD_DATE=${BUILD_DATE}
LABEL service="api-gateway" \
version="${VERSION}" \
description="Spring Cloud Gateway for Meldestelle microservices architecture" \
description="Microservice for API Gateway and Routing" \
maintainer="Meldestelle Development Team" \
org.opencontainers.image.title="Meldestelle API Gateway" \
org.opencontainers.image.created="${BUILD_DATE}"
java.version="${JAVA_VERSION}" \
build.date="${BUILD_DATE}"
ARG APP_USER=gateway
ARG APP_GROUP=gateway
ARG APP_USER=appuser
ARG APP_GROUP=appgroup
ARG APP_UID=1001
ARG APP_GID=1001
@@ -114,21 +75,18 @@ WORKDIR /app
RUN apk update && \
apk upgrade && \
apk add --no-cache \
curl \
tzdata \
tini && \
rm -rf /var/cache/apk/* && \
addgroup -g ${APP_GID} -S ${APP_GROUP} && \
apk add --no-cache curl tzdata tini && \
rm -rf /var/cache/apk/* && addgroup -g ${APP_GID} -S ${APP_GROUP} && \
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh && \
mkdir -p /app/logs /app/tmp /app/config && \
chown -R ${APP_USER}:${APP_GROUP} /app && \
chmod -R 750 /app
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /workspace/build/dependency/dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /workspace/build/dependency/spring-boot-loader/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /workspace/build/dependency/snapshot-dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /workspace/build/dependency/application/ ./
# Copy Spring Boot layers
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/spring-boot-loader/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/snapshot-dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/application/ ./
USER ${APP_USER}
@@ -137,7 +95,7 @@ EXPOSE 8081 5005
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS --max-time 2 http://localhost:8081/actuator/health/readiness || exit 1
ENV JAVA_OPTS="-XX:MaxRAMPercentage=80.0 \
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-XX:+UseStringDeduplication \
-XX:+UseContainerSupport \
@@ -151,25 +109,19 @@ ENV JAVA_OPTS="-XX:MaxRAMPercentage=80.0 \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Europe/Vienna \
-Dspring.backgroundpreinitializer.ignore=true \
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus,gateway \
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus \
-Dmanagement.endpoint.health.show-details=always \
-Dmanagement.prometheus.metrics.export.enabled=true"
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
SERVER_PORT=8081 \
LOGGING_LEVEL_ROOT=INFO \
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_CLOUD_GATEWAY=DEBUG
LOGGING_LEVEL_ROOT=INFO
ENTRYPOINT ["tini", "--", "sh", "-c", "\
echo 'Starting API Gateway with Java ${JAVA_VERSION}...'; \
echo 'Active Spring profiles: '${SPRING_PROFILES_ACTIVE:-not-set}; \
echo 'Gateway port: ${SERVER_PORT}'; \
MEMORY_LIMIT=$(cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || echo 'unlimited'); \
echo \"Container memory limit: $MEMORY_LIMIT\"; \
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
echo 'DEBUG mode enabled - remote debugging available on port 5005'; \
echo 'DEBUG mode enabled'; \
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 org.springframework.boot.loader.launch.JarLauncher; \
else \
echo 'Starting API Gateway in production mode'; \
exec java ${JAVA_OPTS} org.springframework.boot.loader.launch.JarLauncher; \
fi"]
@@ -1,25 +1,30 @@
package at.mocode.infrastructure.gateway
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.getBean
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.context.event.EventListener
import org.springframework.core.env.Environment
@SpringBootApplication
class GatewayApplication
class GatewayApplication(private val env: Environment) {
private val log = LoggerFactory.getLogger(GatewayApplication::class.java)
@EventListener(ApplicationReadyEvent::class)
fun onApplicationReady() {
val springPort = env.getProperty("server.port", "8081")
val appName = env.getProperty("spring.application.name", "gateway")
log.info("----------------------------------------------------------")
log.info("Application '{}' is running!", appName)
log.info("Spring Management Port: {}", springPort)
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
log.info("----------------------------------------------------------")
}
}
fun main(args: Array<String>) {
val context = runApplication<GatewayApplication>(*args)
val logger = LoggerFactory.getLogger(GatewayApplication::class.java)
val env = context.getBean<Environment>()
val port = env.getProperty("server.port") ?: "8081"
logger.info("""
----------------------------------------------------------
Application 'Gateway' is running!
Port: $port
Profiles: ${env.activeProfiles.joinToString(", ").ifEmpty { "default" }}
----------------------------------------------------------
""".trimIndent())
runApplication<GatewayApplication>(*args)
}
@@ -1,6 +1,5 @@
package at.mocode.infrastructure.gateway.config
import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.cloud.gateway.route.builder.filters
@@ -9,9 +8,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class GatewayConfig(
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String
) {
class GatewayConfig {
@Bean
fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
@@ -25,7 +22,31 @@ class GatewayConfig(
it.fallbackUri = java.net.URI.create("forward:/fallback/ping")
}
}
uri(pingServiceUrl)
uri("lb://ping-service")
}
route(id = "masterdata-service") {
path("/api/v1/masterdata/**")
uri("lb://masterdata-service")
}
route(id = "events-service") {
path("/api/v1/events/**")
uri("lb://events-service")
}
route(id = "zns-import-service") {
path("/api/v1/import/zns/**", "/api/v1/import/zns")
uri("lb://zns-import-service")
}
route(id = "results-service") {
path("/api/v1/results/**")
uri("lb://results-service")
}
route(id = "series-service") {
path("/api/v1/series/**")
uri("lb://series-service")
}
route(id = "billing-service") {
path("/api/v1/billing/**")
uri("lb://billing-service")
}
}
}
@@ -11,9 +11,8 @@ import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
import org.springframework.security.oauth2.jwt.*
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter
import org.springframework.security.web.server.SecurityWebFilterChain
@@ -38,7 +37,7 @@ class SecurityConfig(
.authorizeExchange { exchanges ->
exchanges
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll()
.pathMatchers("/api/ping/**").hasRole("MELD_USER") // Beispiel Rolle
.pathMatchers("/api/v1/import/zns", "/api/v1/import/zns/**").permitAll() // TEMPORAER fuer Debugging
.anyExchange().authenticated()
}
.oauth2ResourceServer { oauth2 ->
@@ -66,16 +65,28 @@ class SecurityConfig(
if (delegate == null) {
if (jwkSetUri.isBlank()) {
logger.error("JWK Set URI is missing all authenticated requests will be rejected.")
return Mono.error(org.springframework.security.oauth2.jwt.BadJwtException("Identity Provider not configured"))
return Mono.error(BadJwtException("Identity Provider not configured"))
}
try {
logger.info("Attempting to initialize JWT Decoder with URI: {}", jwkSetUri)
delegate = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
logger.info("JWT Decoder successfully initialized.")
// Wir deaktivieren die Issuer-Validierung, da Keycloak intern "keycloak:8080"
// und extern "localhost:8180" verwendet, was zu Mismatches führt.
val nimbusDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
nimbusDecoder.setJwtValidator(JwtValidators.createDefault()) // Standard-Validierung (ohne Issuer-Zwang falls nicht explizit konfiguriert)
// Da createDefault() den Issuer-Check einbaut, wenn spring.security.oauth2.resourceserver.jwt.issuer-uri gesetzt ist,
// nutzen wir einen Custom Validator der den Issuer ignoriert oder flexibel ist.
val withAudience = DelegatingOAuth2TokenValidator<Jwt>(
JwtTimestampValidator(),
// Hier koennte man weitere Validatoren hinzufuegen, aber wir lassen den Issuer weg
)
nimbusDecoder.setJwtValidator(withAudience)
delegate = nimbusDecoder
logger.info("JWT Decoder successfully initialized (Issuer check disabled for environment flexibility).")
} catch (e: Exception) {
logger.warn("Could not initialize JWT Decoder: {}", e.message)
// Throw BadJwtException so Spring Security returns 401, not 500 or passthrough
return Mono.error(org.springframework.security.oauth2.jwt.BadJwtException("Identity Provider unavailable: ${e.message}"))
return Mono.error(BadJwtException("Identity Provider unavailable: ${e.message}"))
}
}
}
@@ -106,7 +117,7 @@ class SecurityConfig(
val configuration = CorsConfiguration().apply {
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
allowedMethods = securityProperties.cors.allowedMethods.toList()
allowedHeaders = securityProperties.cors.allowedHeaders.toList()
allowedHeaders = listOf("*") // Alles erlauben fuer Postman/Frontend
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
allowCredentials = securityProperties.cors.allowCredentials
maxAge = securityProperties.cors.maxAge.seconds
@@ -20,14 +20,18 @@ spring:
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
discovery:
enabled: true
register: true
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: 8081
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
# Bei lokalem Start (Gradle) wollen wir nicht die Docker-IP registrieren, sondern localhost oder die Host-IP.
# Aber für den Anfang reicht es, wenn wir Consul finden.
gateway:
httpclient: { }
# Routen sind in GatewayConfig.kt definiert
# Routen sind in GatewayConfig.kt via Service-Discovery (lb://) definiert
# --- SECURITY (OAuth2 Resource Server) ---
security:
@@ -40,6 +44,27 @@ spring:
issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI:http://localhost:8180/realms/meldestelle}
jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI:http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs}
gateway:
security:
cors:
allowed-origin-patterns:
- "http://localhost:*"
- "https://*.meldestelle.at"
- "https://*.mo-code.at"
- "https://*.postman.co"
- "postman://*"
allowed-methods:
- "GET"
- "POST"
- "PUT"
- "DELETE"
- "OPTIONS"
- "PATCH"
allowed-headers:
- "*"
allow-credentials: true
max-age: 3600s
management:
endpoints:
web:
@@ -62,9 +87,3 @@ management:
# Lokal: Zipkin auf Port 9411. In Docker via ENV MANAGEMENT_ZIPKIN_TRACING_ENDPOINT überschrieben.
endpoint: ${MANAGEMENT_ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
# --- Custom Service URLs ---
# Default: Localhost (für Entwicklung ohne Docker)
# Im Docker-Compose überschreiben wir das mit dem Service-Namen
ping:
service:
url: ${PING_SERVICE_URL:http://localhost:8082}
@@ -8,7 +8,7 @@
"variable": [
{
"key": "baseUrl",
"value": "http://localhost:8080",
"value": "http://localhost:8081",
"type": "string"
},
{
@@ -221,6 +221,100 @@
}
]
},
{
"name": "Connectivity Context (Ping Service)",
"item": [
{
"name": "Simple Ping",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/ping/simple",
"host": ["{{baseUrl}}"],
"path": ["api", "ping", "simple"]
}
},
"response": []
},
{
"name": "Health Check",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/ping/health",
"host": ["{{baseUrl}}"],
"path": ["api", "ping", "health"]
}
},
"response": []
},
{
"name": "Public Info",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/ping/public",
"host": ["{{baseUrl}}"],
"path": ["api", "ping", "public"]
}
},
"response": []
},
{
"name": "Enhanced Ping (Resilience)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/ping/enhanced",
"host": ["{{baseUrl}}"],
"path": ["api", "ping", "enhanced"]
}
},
"response": []
},
{
"name": "Sync Delta Diagnostic",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/ping/sync?lastSyncTimestamp=0",
"host": ["{{baseUrl}}"],
"path": ["api", "ping", "sync"],
"query": [
{
"key": "lastSyncTimestamp",
"value": "0"
}
]
}
},
"response": []
},
{
"name": "Secure Ping (Login Required)",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{authToken}}"
}
],
"url": {
"raw": "{{baseUrl}}/api/ping/secure",
"host": ["{{baseUrl}}"],
"path": ["api", "ping", "secure"]
}
},
"response": []
}
]
},
{
"name": "Master Data Context",
"item": [
@@ -25,6 +25,7 @@ dependencies {
// Web (for CORS config)
implementation(libs.spring.web)
implementation(libs.spring.boot.starter.web)
// Testing
testImplementation(projects.platform.platformTesting)
@@ -0,0 +1,49 @@
package at.mocode.infrastructure.security
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter
/**
* Filter zur Authentifizierung von Desktop-Clients via Security Key.
* Dieser Filter ist für die Offline-First-Synchronisation gedacht.
*
* Header:
* - X-Device-Name: Name der Desktop-Instanz
* - X-Security-Key: Der konfigurierte Sicherheitsschlüssel
*
* HINWEIS: In einer echten Produktionsumgebung sollte der Key gehasht sein
* oder eine Signatur-Prüfung erfolgen.
*/
class DeviceSecurityFilter : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val deviceName = request.getHeader("X-Device-Name")
val securityKey = request.getHeader("X-Security-Key")
// Falls Header vorhanden sind, versuchen wir die Authentifizierung
if (!deviceName.isNullOrBlank() && !securityKey.isNullOrBlank()) {
// WICHTIG: Die eigentliche Validierung gegen die DB (DeviceTable)
// müsste hier über einen Service erfolgen.
// Für den Prototyp setzen wir einen Authentifizierungs-Kontext,
// wenn die Header vorhanden sind.
val auth = UsernamePasswordAuthenticationToken(
deviceName,
null,
listOf(SimpleGrantedAuthority("ROLE_DEVICE"))
)
SecurityContextHolder.getContext().authentication = auth
}
filterChain.doFilter(request, response)
}
}
@@ -6,8 +6,17 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.jwt.JwtTimestampValidator
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@Configuration
@EnableWebSecurity
@@ -18,17 +27,18 @@ class GlobalSecurityConfig {
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() } // CSRF nicht nötig für Stateless REST APIs
// WICHTIG: CORS explizit deaktivieren!
// Das API-Gateway kümmert sich um CORS. Die Microservices dürfen KEINE
// Access-Control-Allow-Origin Header setzen, sonst haben wir doppelte Header beim Client.
.cors { it.disable() }
// WICHTIG: CORS wieder aktivieren für Plan-B (Direktzugriff ohne Gateway möglich)
.cors { it.configurationSource(corsConfigurationSource()) }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.addFilterBefore(DeviceSecurityFilter(), UsernamePasswordAuthenticationFilter::class.java)
.authorizeHttpRequests { auth ->
// Explizite Freigaben (Health, Info, Public Endpoints)
// Explizite Freigaben (Health, Information, Public-Endpoints)
auth.requestMatchers("/actuator/**").permitAll()
auth.requestMatchers("/api/v1/devices/register").permitAll() // Onboarding erlauben
auth.requestMatchers("/api/mail/nennung").permitAll() // Plan-B Nennungen erlauben
auth.requestMatchers("/api/mail/nennungen").authenticated() // Liste schützen
auth.requestMatchers("/ping/public").permitAll()
auth.requestMatchers("/ping/simple").permitAll()
auth.requestMatchers("/ping/enhanced").permitAll()
auth.requestMatchers("/ping/health").permitAll()
auth.requestMatchers("/error").permitAll()
@@ -38,16 +48,48 @@ class GlobalSecurityConfig {
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { jwt ->
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
// Auch hier den Issuer-Check entspannen, da der Service intern validiert
jwt.decoder(jwtDecoder())
}
}
return http.build()
}
@Bean
fun jwtDecoder(): JwtDecoder {
// 1. Suche in System-Properties (Spring injects these)
// 2. Suche in Environment Variables
// 3. Fallback auf localhost (IDE-Start) oder keycloak (Docker-Start)
val jwkSetUri = System.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
?: System.getenv("SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI")
?: "http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs"
val decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
val validator = DelegatingOAuth2TokenValidator<Jwt>(JwtTimestampValidator())
decoder.setJwtValidator(validator)
return decoder
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val converter = JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter(KeycloakRoleConverter())
return converter
}
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("*")
configuration.allowedOriginPatterns = listOf("*")
configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")
configuration.allowedHeaders = listOf("*")
configuration.exposedHeaders = listOf("*")
configuration.maxAge = 3600L
configuration.allowCredentials = false
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
}
@@ -0,0 +1,12 @@
package at.mocode.zns.importer
/**
* Der Modus des ZNS-Imports.
*
* [FULL] - Alle Dateien (Vereine, Reiter, Pferde, Funktionäre) werden importiert.
* [LIGHT] - Nur Stammdaten (Vereine, Reiter) werden importiert (Performance-Optimiert).
*/
enum class ZnsImportMode {
FULL,
LIGHT
}
@@ -2,11 +2,11 @@
package at.mocode.zns.importer
import at.mocode.masterdata.domain.repository.VereinRepository
import at.mocode.masterdata.domain.repository.HorseRepository
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import at.mocode.masterdata.domain.repository.ReiterRepository
import at.mocode.zns.parser.ZnsLegacyParsers
import at.mocode.masterdata.domain.repository.*
import at.mocode.zns.parser.ZnsFunktionaerParser
import at.mocode.zns.parser.ZnsPferdParser
import at.mocode.zns.parser.ZnsReiterParser
import at.mocode.zns.parser.ZnsVereinParser
import java.io.InputStream
import java.nio.charset.Charset
import java.util.zip.ZipInputStream
@@ -19,10 +19,10 @@ import java.util.zip.ZipInputStream
* Domänenobjekte über die jeweiligen Repositories (Upsert-Logik).
*
* Die Verarbeitungsreihenfolge ist fix:
* 1. VEREIN01.DAT DomVerein (via VereinRepository)
* 2. LIZENZ01.DAT DomReiter (via ReiterRepository)
* 3. PFERDE01.DAT DomPferd (via HorseRepository)
* 4. RICHT01.DAT DomFunktionaer (via FunktionaerRepository)
* 1. VEREIN01.DAT Verein (via VereinRepository)
* 2. LIZENZ01.DAT Reiter (via ReiterRepository)
* 3. PFERDE01.DAT Pferd (via HorseRepository)
* 4. RICHT01.DAT Funktionär (via FunktionaerRepository)
*
* Dieser Service hat **keine** Spring-Abhängigkeit und kann daher sowohl
* im Backend (REST-Upload) als auch in der Compose Desktop App (Offline-Import)
@@ -37,45 +37,196 @@ class ZnsImportService(
private val vereinRepository: VereinRepository,
private val reiterRepository: ReiterRepository,
private val horseRepository: HorseRepository,
private val funktionaerRepository: FunktionaerRepository
private val funktionaerRepository: FunktionaerRepository,
private val landRepository: LandRepository,
private val bundeslandRepository: BundeslandRepository,
private val licenseRepository: LizenzRepository? = null,
private val altersklassenRepository: AltersklassenRepository? = null
) {
companion object {
private val CP850 = Charset.forName("Cp850")
private const val FILE_VEREIN = "VEREIN01.DAT"
private const val FILE_LIZENZ = "LIZENZ01.DAT"
private const val FILE_PFERDE = "PFERDE01.DAT"
private const val FILE_RICHT = "RICHT01.DAT"
private const val FILE_VEREIN = "VEREIN"
private const val FILE_LIZENZ = "LIZENZ"
private const val FILE_PFERDE = "PFERDE"
private const val FILE_RICHT = "RICHT"
}
/**
* Extrahiert die relevanten Dateien aus dem ZIP-Archiv.
* Optimiert: Nutzt BufferedReader für zeilenweises Einlesen, ohne das gesamte File in den RAM zu laden.
*/
fun extrahiereDateien(zipInputStream: InputStream): Map<String, List<String>> {
val dateien = mutableMapOf<String, List<String>>()
val zip = ZipInputStream(zipInputStream)
try {
var entry = zip.nextEntry
while (entry != null) {
val fileName = entry.name.uppercase().substringAfterLast("/")
// Toleranter Check: Erkennt VEREIN01.DAT, VEREIN.DAT, etc.
val targetKey = when {
fileName.startsWith(FILE_VEREIN) -> FILE_VEREIN
fileName.startsWith(FILE_LIZENZ) -> FILE_LIZENZ
fileName.startsWith(FILE_PFERDE) -> FILE_PFERDE
fileName.startsWith(FILE_RICHT) -> FILE_RICHT
else -> null
}
if (targetKey != null && fileName.endsWith(".DAT")) {
// Wir lesen den Stream direkt zeilenweise mit dem korrekten Encoding
val lines = mutableListOf<String>()
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()
while (line != null) {
if (line.isNotBlank()) {
lines.add(line)
}
line = reader.readLine()
}
println("[DEBUG_LOG] Datei $fileName extrahiert als $targetKey: ${lines.size} Zeilen")
dateien[targetKey] = lines
}
zip.closeEntry()
entry = zip.nextEntry
}
} 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<String>()
val warnungen = mutableListOf<String>()
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.startsWith(FILE_VEREIN) -> {
val (n, u) = importiereVereine(lines, fehler)
vereineImportiert = n
vereineAktualisiert = u
}
fileName.startsWith(FILE_LIZENZ) -> {
val (n, u) = importiereReiter(lines, fehler, warnungen)
reiterImportiert = n
reiterAktualisiert = u
}
fileName.startsWith(FILE_PFERDE) -> {
if (mode == ZnsImportMode.FULL) {
val (n, u) = importierePferde(lines, fehler)
pferdeImportiert = n
pferdeAktualisiert = u
}
}
fileName.startsWith(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].
*
* @param zipInputStream Der InputStream der ZIP-Datei.
* @param mode Der [ZnsImportMode] (Standard: [ZnsImportMode.FULL]).
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
*/
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
val dateien = mutableMapOf<String, List<String>>()
ZipInputStream(zipInputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val name = entry.name.uppercase().substringAfterLast("/")
if (name in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
dateien[name] = zip.readBytes().toString(CP850).lines()
}
zip.closeEntry()
entry = zip.nextEntry
}
}
suspend fun importiereZip(
zipInputStream: InputStream,
mode: ZnsImportMode = ZnsImportMode.FULL
): ZnsImportResult {
val dateien = extrahiereDateien(zipInputStream)
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<String>()
val warnungen = mutableListOf<String>()
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
val (richterNeu, richterUpd) = importiereRichter(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
var pferdeNeu = 0
var pferdeUpd = 0
var richterNeu = 0
var richterUpd = 0
if (mode == ZnsImportMode.FULL) {
val (pNeu, pUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
pferdeNeu = pNeu
pferdeUpd = pUpd
val (rNeu, rUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
richterNeu = rNeu
richterUpd = rUpd
}
// Zusätzliche Warnung wenn Dateien fehlen
if (dateien[FILE_VEREIN] == null) warnungen.add("Vereinsdaten (VEREIN*.DAT) nicht gefunden.")
if (dateien[FILE_LIZENZ] == null) warnungen.add("Reiter/Lizenzdaten (LIZENZ*.DAT) nicht gefunden.")
if (mode == ZnsImportMode.FULL) {
if (dateien[FILE_PFERDE] == null) warnungen.add("Pferdedaten (PFERDE*.DAT) nicht gefunden.")
if (dateien[FILE_RICHT] == null) warnungen.add("Funktionärsdaten (RICHT*.DAT) nicht gefunden.")
}
return ZnsImportResult(
vereineImportiert = vereineNeu,
@@ -95,7 +246,7 @@ class ZnsImportService(
// Private Hilfsmethoden
// -------------------------------------------------------------------------
private suspend fun importiereVereine(
suspend fun importiereVereine(
zeilen: List<String>,
fehler: MutableList<String>
): Pair<Int, Int> {
@@ -103,7 +254,11 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val verein = ZnsLegacyParsers.parseVerein(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)
@@ -111,13 +266,11 @@ class ZnsImportService(
} else {
vereinRepository.save(
vorhanden.copy(
name = verein.name,
kurzname = verein.kurzname,
vereinName = verein.vereinName,
bundesland = verein.bundesland,
ort = verein.ort,
plz = verein.plz,
strasse = verein.strasse,
oepsRegionNummer = verein.oepsRegionNummer,
istAktiv = verein.istAktiv,
datenQuelle = verein.datenQuelle
).withUpdatedTimestamp()
@@ -131,7 +284,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importiereReiter(
suspend fun importiereReiter(
zeilen: List<String>,
fehler: MutableList<String>,
warnungen: MutableList<String>
@@ -140,7 +293,29 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val reiter = ZnsLegacyParsers.parseLizenz(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) }
val bundesland = parsed.bundeslandNummer?.let { bundeslandRepository.findByNr(it) }
val nation = parsed.nation?.let { landRepository.findByIsoAlpha3Code(it) }
val reitLizenz = parsed.reiterLizenz?.let { licenseRepository?.findReitLizenzByCode(it) }
val fahrLizenz = parsed.fahrLizenz?.let { licenseRepository?.findFahrLizenzByCode(it) }
val startkarte = parsed.startkarte?.let { licenseRepository?.findStartkarteByCode(it) }
val reiter = parsed.copy(
vereinId = verein?.vereinId,
bundeslandId = bundesland?.bundeslandId,
nationId = nation?.landId,
reitLizenzId = reitLizenz?.lizenzId,
fahrLizenzId = fahrLizenz?.lizenzId,
startkarteId = startkarte?.startkarteId
)
val vorhanden = reiterRepository.findBySatznummer(reiter.satznummer)
if (vorhanden == null) {
reiterRepository.save(reiter)
@@ -150,8 +325,29 @@ class ZnsImportService(
vorhanden.copy(
vorname = reiter.vorname,
nachname = reiter.nachname,
bundeslandNummer = reiter.bundeslandNummer,
vereinsName = reiter.vereinsName,
nation = reiter.nation,
vereinId = reiter.vereinId,
bundeslandId = reiter.bundeslandId,
nationId = reiter.nationId,
reitLizenzId = reiter.reitLizenzId,
fahrLizenzId = reiter.fahrLizenzId,
startkarteId = reiter.startkarteId,
reiterLizenz = reiter.reiterLizenz,
startkarte = reiter.startkarte,
fahrLizenz = reiter.fahrLizenz,
altersklasseJgJrU25 = reiter.altersklasseJgJrU25,
altersklasseY = reiter.altersklasseY,
mitgliedsNummer = reiter.mitgliedsNummer,
telefonNummer = reiter.telefonNummer,
kader = reiter.kader,
lastPayYear = reiter.lastPayYear,
geschlecht = reiter.geschlecht,
geburtsdatum = reiter.geburtsdatum,
feiId = reiter.feiId,
sperrListe = reiter.sperrListe,
lizenzInfo = reiter.lizenzInfo,
lizenzKlasse = reiter.lizenzKlasse,
istAktiv = reiter.istAktiv,
datenQuelle = reiter.datenQuelle
@@ -166,7 +362,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importierePferde(
suspend fun importierePferde(
zeilen: List<String>,
fehler: MutableList<String>
): Pair<Int, Int> {
@@ -174,10 +370,16 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed
val vorhanden = pferd.lebensnummer
?.takeIf { it.isNotBlank() }
?.let { horseRepository.findByLebensnummer(it) }
val pferd = ZnsPferdParser.parse(zeile) ?: return@forEachIndexed
if (pferd.pferdeName.isBlank()) return@forEachIndexed
// Match primarily by satznummer, then by lebensnummer
val vorhanden = pferd.satznummer?.takeIf { it.isNotBlank() }?.let { horseRepository.findBySatznummer(it) }
?: pferd.lebensnummer?.takeIf { it.isNotBlank() }?.let { horseRepository.findByLebensnummer(it) }
?: (if (pferd.pferdeName.isNotBlank()) horseRepository.findByName(pferd.pferdeName, 100).find {
it.geburtsjahr == pferd.geburtsjahr && it.kopfnummer == pferd.kopfnummer
} else null)
if (vorhanden == null) {
horseRepository.save(pferd)
neu++
@@ -186,10 +388,17 @@ class ZnsImportService(
vorhanden.copy(
pferdeName = pferd.pferdeName,
geschlecht = pferd.geschlecht,
geburtsdatum = pferd.geburtsdatum,
rasse = pferd.rasse,
geburtsjahr = pferd.geburtsjahr,
farbe = pferd.farbe,
abstammung = pferd.abstammung,
vereinNummer = pferd.vereinNummer,
lastPayYear = pferd.lastPayYear,
verantwortlichePersonId = pferd.verantwortlichePersonId,
lebensnummer = pferd.lebensnummer,
oepsNummer = pferd.oepsNummer,
kopfnummer = pferd.kopfnummer,
satznummer = pferd.satznummer,
vater = pferd.vater,
feiPass = pferd.feiPass,
istAktiv = pferd.istAktiv,
datenQuelle = pferd.datenQuelle
).withUpdatedTimestamp()
@@ -203,7 +412,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importiereRichter(
suspend fun importiereFunktionaere(
zeilen: List<String>,
fehler: MutableList<String>,
warnungen: MutableList<String>
@@ -212,24 +421,31 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val richter = ZnsLegacyParsers.parseRichter(zeile) ?: return@forEachIndexed
val richterNummer = richter.richterNummer ?: run {
warnungen.add("$FILE_RICHT Zeile ${index + 1}: Keine RichterNummer übersprungen.")
return@forEachIndexed
}
val vorhanden = funktionaerRepository.findByRichterNummer(richterNummer)
val funktionaerRaw = ZnsFunktionaerParser.parse(zeile) ?: return@forEachIndexed
val nameParts = funktionaerRaw.name?.split(",")?.map { it.trim() }
val reiterId = if (nameParts != null && nameParts.size >= 2) {
val nachname = nameParts[0]
val vorname = nameParts[1]
reiterRepository.findByName(vorname, nachname).firstOrNull()?.reiterId
} else null
val funktionaer = funktionaerRaw.copy(reiterId = reiterId)
val satzID = funktionaer.satzId
val satzNummer = funktionaer.satzNummer
val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer)
if (vorhanden == null) {
funktionaerRepository.save(richter)
funktionaerRepository.save(funktionaer)
neu++
} else {
funktionaerRepository.save(
vorhanden.copy(
vorname = richter.vorname,
nachname = richter.nachname,
vereinsNummer = richter.vereinsNummer,
richterNummer = richter.richterNummer,
istAktiv = richter.istAktiv,
datenQuelle = richter.datenQuelle
reiterId = funktionaer.reiterId,
name = funktionaer.name,
qualifikationen = funktionaer.qualifikationen,
istAktiv = funktionaer.istAktiv,
datenQuelle = funktionaer.datenQuelle
).withUpdatedTimestamp()
)
aktualisiert++
@@ -1,13 +1,15 @@
package at.mocode.zns.importer
import at.mocode.masterdata.domain.model.DomFunktionaer
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.model.DomVerein
import at.mocode.masterdata.domain.model.Funktionaer
import at.mocode.masterdata.domain.model.Pferd
import at.mocode.masterdata.domain.model.Reiter
import at.mocode.masterdata.domain.model.Verein
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import at.mocode.masterdata.domain.repository.HorseRepository
import at.mocode.masterdata.domain.repository.ReiterRepository
import at.mocode.masterdata.domain.repository.VereinRepository
import at.mocode.masterdata.domain.repository.LandRepository
import at.mocode.masterdata.domain.repository.BundeslandRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
@@ -28,6 +30,8 @@ class ZnsImportServiceTest {
private val reiterRepository = mockk<ReiterRepository>()
private val horseRepository = mockk<HorseRepository>()
private val funktionaerRepository = mockk<FunktionaerRepository>()
private val landRepository = mockk<LandRepository>()
private val bundeslandRepository = mockk<BundeslandRepository>()
private lateinit var service: ZnsImportService
@@ -35,7 +39,19 @@ class ZnsImportServiceTest {
@BeforeEach
fun setUp() {
service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository)
service = ZnsImportService(
vereinRepository,
reiterRepository,
horseRepository,
funktionaerRepository,
landRepository,
bundeslandRepository
)
// Standard-Stubs für optionale Lookups, damit Tests ohne spezifische Erwartungen nicht fehlschlagen
coEvery { landRepository.findByIsoAlpha3Code(any()) } returns null
coEvery { bundeslandRepository.findByNr(any()) } returns null
coEvery { vereinRepository.findByExactName(any()) } returns null
}
// -------------------------------------------------------------------------
@@ -52,7 +68,9 @@ class ZnsImportServiceTest {
zip.closeEntry()
}
}
return ByteArrayInputStream(baos.toByteArray())
val zipBytes = baos.toByteArray()
// println("[DEBUG_LOG] ZIP erstellt mit ${entries.size} Dateien, Gesamtgröße: ${zipBytes.size} Bytes")
return ByteArrayInputStream(zipBytes)
}
/** Erzeugt eine gültige VEREIN01.DAT-Zeile (mind. 30 Zeichen). */
@@ -70,11 +88,20 @@ class ZnsImportServiceTest {
nachname: String = "Mustermann",
vorname: String = "Max"
): String {
// Stelle 1-6: Satznummer, 7-56: Nachname (50), 57-81: Vorname (25)
return satznummer.padEnd(6) +
nachname.padEnd(50) +
vorname.padEnd(25) +
" ".repeat(200) // Rest auffüllen
val line = StringBuilder()
line.append(satznummer.padEnd(6)) // 1-6
line.append(nachname.padEnd(50)) // 7-56
line.append(vorname.padEnd(25)) // 57-81
line.append("01") // 82-83 (Buli)
line.append("".padEnd(50)) // 84-133 (Verein)
line.append("AUT") // 134-136 (Nation)
line.append("R2 ") // 137-140 (Lizenz)
line.append(" ") // 141 (Startkarte)
line.append(" ") // 142-143 (Fahrlizenz)
line.append("JG") // 144-145 (Altersklasse)
line.append(" ") // 146 (Altersklasse Y)
line.append("01234567") // 147-154 (Mitglied) - Bundesland 01 (Wien) + Rest
return line.toString().padEnd(220)
}
/** Erzeugt eine gültige PFERDE01.DAT-Zeile (mind. 211 Zeichen). */
@@ -88,17 +115,20 @@ class ZnsImportServiceTest {
lebensnummer.padEnd(9) +
"W" + // Geschlecht: Wallach
"2015" + // Geburtsjahr
" ".repeat(157) // Auffüllen bis Stelle 201
return base + "SAT0000001".padEnd(10) // Satznummer ab Stelle 202
" ".repeat(157) // Auffüllen bis Stelle 201 (1 bis 201 = 201 Zeichen)
return base + "1234567890".padEnd(10) // Satznummer ab Stelle 202
}
/** Erzeugt eine gültige RICHT01.DAT-Zeile (mind. 83 Zeichen). */
private fun richterZeile(
satznummer: String = "R00001",
name: String = "Huber, Anna"
private fun funktionaerZeile(
typ: String = "X",
satznummer: String = "123456",
name: String = "Huber, Anna",
qualifikationen: String = "GA"
): String {
// Stelle 1: Typ, 2-7: Satznummer (6), 8-82: Name (75)
return "R" + satznummer.padEnd(6) + name.padEnd(75)
// Stelle 1: Typ (X=Richter, Y=Parcoursbauer), 2-7: Satznummer (6), 8-82: Name (75), 83-112: Quali (30)
// WICHTIG: satznummer muss genau 6 Stellen lang sein, ohne abschließende Leerzeichen für den Int-Parser
return typ + satznummer.padStart(6, '0') + name.padEnd(75) + qualifikationen.padEnd(30)
}
// -------------------------------------------------------------------------
@@ -110,29 +140,29 @@ class ZnsImportServiceTest {
val zip = buildZip("VEREIN01.DAT" to vereinZeile())
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
coEvery { vereinRepository.save(any()) } answers { firstArg<Verein>() }
val result = service.importiereZip(zip)
assertThat(result.vereineImportiert).isEqualTo(1)
assertThat(result.vereineAktualisiert).isEqualTo(0)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 1) { vereinRepository.save(any<DomVerein>()) }
coVerify(exactly = 1) { vereinRepository.save(any<Verein>()) }
}
@Test
fun `importiereZip - bestehende Vereine werden aktualisiert`() = runTest {
val zip = buildZip("VEREIN01.DAT" to vereinZeile(name = "Neuer Name"))
val vorhanden = DomVerein(vereinsNummer = "0001", name = "Alter Name")
val vorhanden = Verein(vereinsNummer = "0001", vereinName = "Alter Name")
coEvery { vereinRepository.findByVereinsNummer("0001") } returns vorhanden
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
coEvery { vereinRepository.save(any()) } answers { firstArg<Verein>() }
val result = service.importiereZip(zip)
assertThat(result.vereineImportiert).isEqualTo(0)
assertThat(result.vereineAktualisiert).isEqualTo(1)
coVerify(exactly = 1) { vereinRepository.save(match { it.name == "Neuer Name" }) }
coVerify(exactly = 1) { vereinRepository.save(match { it.vereinName == "Neuer Name" }) }
}
@Test
@@ -140,70 +170,130 @@ class ZnsImportServiceTest {
val zip = buildZip("LIZENZ01.DAT" to lizenzZeile())
coEvery { reiterRepository.findBySatznummer(any()) } returns null
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
coEvery { reiterRepository.save(any()) } answers { firstArg<Reiter>() }
val result = service.importiereZip(zip)
assertThat(result.reiterImportiert).isEqualTo(1)
assertThat(result.reiterAktualisiert).isEqualTo(0)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 1) { reiterRepository.save(any<DomReiter>()) }
coVerify(exactly = 1) {
reiterRepository.save(match {
it.reiterLizenz == "R2" &&
it.lizenzen.size == 1 &&
it.lizenzen[0].kuerzel == "R2" &&
it.altersklasseJgJrU25 == at.mocode.core.domain.model.ReiterAltersKlasseE.JG
})
}
}
@Test
fun `importiereZip - neue Pferde werden gespeichert`() = runTest {
val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
coEvery { horseRepository.findByName(any(), any()) } returns emptyList()
coEvery { horseRepository.save(any()) } answers { firstArg<Pferd>() }
val result = service.importiereZip(zip)
assertThat(result.pferdeImportiert).isEqualTo(1)
assertThat(result.pferdeAktualisiert).isEqualTo(0)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 1) { horseRepository.save(any<DomPferd>()) }
coVerify(exactly = 1) { horseRepository.save(any<Pferd>()) }
}
@Test
fun `importiereZip - neue Richter werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to richterZeile())
fun `importiereZip - Pferde werden ueber Kopfnummer und Geburtsjahr aktualisiert, wenn Satznummer fehlt`() = runTest {
// Zeile ohne Satznummer (zu kurz)
val zeile = "0001Blitz "
val zip = buildZip("PFERDE01.DAT" to zeile)
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val existing = Pferd(pferdeName = "Blitz", kopfnummer = "0001", geschlecht = at.mocode.core.domain.model.PferdeGeschlechtE.UNBEKANNT)
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.findByName("Blitz", 100) } returns listOf(existing)
coEvery { horseRepository.save(any()) } answers { firstArg<Pferd>() }
val result = service.importiereZip(zip)
assertThat(result.pferdeAktualisiert).isEqualTo(1)
coVerify(exactly = 1) { horseRepository.save(match { it.pferdeName == "Blitz" && it.pferdId == existing.pferdId }) }
}
@Test
fun `importiereZip - neue Funktionaere werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to funktionaerZeile())
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
val result = service.importiereZip(zip)
assertThat(result.richterImportiert).isEqualTo(1)
assertThat(result.richterAktualisiert).isEqualTo(0)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 1) { funktionaerRepository.save(any<DomFunktionaer>()) }
coVerify(exactly = 1) { funktionaerRepository.save(any<Funktionaer>()) }
}
@Test
fun `importiereZip - Richter und Parcoursbauer mit Mac-Zeilenumbruch werden importiert`() = runTest {
// Nur \r als Umbruch
val zip = buildZip(
"RICHT01.DAT" to "X139552Mc Mullen Elizabeth DIOR\rX014346Schubert Renate DM,DPF,GAR-SP,SPF,SS*\rX001416Lechner-Gebhard Jeannette DPF,DSGP\rY135894Helmreich Marilena GA\r"
)
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
val result = service.importiereZip(zip)
assertThat(result.richterImportiert).isEqualTo(4)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 4) { funktionaerRepository.save(any<Funktionaer>()) }
}
@Test
fun `importiereZip - vollstaendiger Import aller vier Dateien`() = runTest {
val zip = buildZip(
"VEREIN01.DAT" to vereinZeile(),
"LIZENZ01.DAT" to lizenzZeile(),
"PFERDE01.DAT" to pferdeZeile(),
"RICHT01.DAT" to richterZeile()
)
// Erstelle vier separate InputStreams für die vier Dateien (für isolierte Extraktion)
val zipVerein = buildZip("VEREIN01.DAT" to vereinZeile())
val zipLizenz = buildZip("LIZENZ01.DAT" to lizenzZeile())
val zipPferde = buildZip("PFERDE01.DAT" to pferdeZeile())
val zipRicht = buildZip("RICHT01.DAT" to funktionaerZeile())
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
coEvery { vereinRepository.save(any()) } answers { firstArg<Verein>() }
coEvery { reiterRepository.findBySatznummer(any()) } returns null
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
coEvery { reiterRepository.save(any()) } answers { firstArg<Reiter>() }
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
coEvery { horseRepository.findByName(any(), any()) } returns emptyList()
coEvery { horseRepository.save(any()) } answers { firstArg<Pferd>() }
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
coEvery { vereinRepository.findByExactName(any()) } returns null
coEvery { bundeslandRepository.findByNr(any()) } returns null
coEvery { landRepository.findByIsoAlpha3Code(any()) } returns null
val result = service.importiereZip(zip)
// Importiere nacheinander (Simulation eines vollständigen Workflows)
val res1 = service.importiereZip(zipVerein)
val res2 = service.importiereZip(zipLizenz)
val res3 = service.importiereZip(zipPferde)
val res4 = service.importiereZip(zipRicht)
assertThat(result.gesamtImportiert).isEqualTo(4)
assertThat(result.gesamtAktualisiert).isEqualTo(0)
assertThat(result.fehler).isEmpty()
assertThat(result.hatFehler).isFalse()
assertThat(res1.vereineImportiert).isEqualTo(1)
assertThat(res2.reiterImportiert).isEqualTo(1)
assertThat(res3.pferdeImportiert).isEqualTo(1)
assertThat(res4.richterImportiert).isEqualTo(1)
assertThat(res1.fehler).isEmpty()
assertThat(res2.fehler).isEmpty()
assertThat(res3.fehler).isEmpty()
assertThat(res4.fehler).isEmpty()
}
@Test
@@ -216,4 +306,52 @@ class ZnsImportServiceTest {
assertThat(result.gesamtAktualisiert).isEqualTo(0)
assertThat(result.fehler).isEmpty()
}
@Test
fun `importiereZip - Funktionaer mit mehrfachen Qualifikationen`() = runTest {
// Zeile mit vielen Qualifikationen (Satznummer X014346)
val qualifikationen = "DM,DPF,GAR-SP,SPF,SS*,RD,RS"
val zeile = "X014346Schubert Renate $qualifikationen"
val zip = buildZip("RICHT01.DAT" to zeile)
coEvery { funktionaerRepository.findBySatz("X", 14346) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
val result = service.importiereZip(zip)
assertThat(result.richterImportiert).isEqualTo(1)
coVerify {
funktionaerRepository.save(match { f ->
f.qualifikationen.size == 7 &&
f.qualifikationen.containsAll(listOf("DM", "DPF", "GAR-SP", "SPF", "SS*", "RD", "RS"))
})
}
}
@Test
fun `importiereZip - Funktionaer Update Strategie (Delete+Insert)`() = runTest {
val zeile = funktionaerZeile(typ = "X", satznummer = "123456", name = "Geaendert Name")
val zip = buildZip("RICHT01.DAT" to zeile)
val existing = Funktionaer(
funktionaerId = kotlin.uuid.Uuid.random(),
satzId = "X",
satzNummer = 123456,
name = "Alt Name"
)
coEvery { funktionaerRepository.findBySatz("X", 123456) } returns existing
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
val result = service.importiereZip(zip)
assertThat(result.richterAktualisiert).isEqualTo(1)
coVerify {
funktionaerRepository.save(match { f ->
f.funktionaerId == existing.funktionaerId && f.name == "Geaendert Name"
})
}
}
}
@@ -0,0 +1,32 @@
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvm()
wasmJs {
browser()
}
sourceSets {
commonMain.dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(libs.kotlinx.datetime)
}
commonTest.dependencies {
implementation(kotlin("test"))
}
jvmTest.dependencies {
implementation(projects.platform.platformTesting)
}
}
}
@@ -0,0 +1,71 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.domain.model
import at.mocode.core.domain.serialization.InstantSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
/**
* Repräsentiert das Kassa-Konto eines Teilnehmers (Reiter oder Besitzer).
* Ein Konto wird pro Veranstaltung/Turnier geführt, kann aber veranstaltungsübergreifend aggregiert werden.
*/
@Serializable
data class TeilnehmerKonto constructor(
val kontoId: Uuid = Uuid.random(),
val veranstaltungId: Uuid,
val personId: Uuid, // Referenz auf Reiter oder Besitzer
val personName: String,
val saldoCent: Long = 0L, // Aktueller Kontostand in Cent
val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant = Clock.System.now()
)
/**
* Ein einzelner Buchungsvorgang (Zahlung, Gutschrift, Gebühr).
*/
@Serializable
data class Buchung constructor(
val buchungId: Uuid = Uuid.random(),
val kontoId: Uuid,
val betragCent: Long, // Positiv für Gutschrift/Zahlung, Negativ für Gebühr/Soll
val typ: BuchungsTyp,
val verwendungszweck: String,
@Serializable(with = InstantSerializer::class)
val gebuchtAm: Instant = Clock.System.now(),
val storniertBuchungId: Uuid? = null // Referenz auf die ursprüngliche Buchung, falls dies ein Storno ist
)
/**
* Repräsentiert einen Kassa-Tagesabschluss.
*/
@Serializable
data class Tagesabschluss(
val tagesabschlussId: Uuid = Uuid.random(),
val veranstaltungId: Uuid,
val abgeschlossenAm: Instant = Clock.System.now(),
val abgeschlossenVon: String,
val summeBarCent: Long,
val summeKarteCent: Long,
val summeGutschriftCent: Long,
val anzahlBuchungen: Int,
val bemerkungen: String? = null
)
@Serializable
enum class BuchungsTyp {
NENNGEBUEHR,
NENNGELD,
NACHNENNGEBUEHR,
STARTGEBUEHR,
BOXENGEBUEHR,
SPORTFOERDERBEITRAG,
ZAHLUNG_BAR,
ZAHLUNG_KARTE,
GUTSCHRIFT,
STORNIERUNG
}
@@ -0,0 +1,44 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.domain.repository
import at.mocode.billing.domain.model.Buchung
import at.mocode.billing.domain.model.Tagesabschluss
import at.mocode.billing.domain.model.TeilnehmerKonto
import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
/**
* Repository für den Zugriff auf Teilnehmer-Konten.
*/
interface TeilnehmerKontoRepository {
fun findByVeranstaltungAndPerson(veranstaltungId: Uuid, personId: Uuid): TeilnehmerKonto?
fun findById(kontoId: Uuid): TeilnehmerKonto?
fun findByVeranstaltung(veranstaltungId: Uuid): List<TeilnehmerKonto>
fun findOffenePosten(veranstaltungId: Uuid): List<TeilnehmerKonto>
fun save(konto: TeilnehmerKonto): TeilnehmerKonto
fun updateSaldo(kontoId: Uuid, saldoCent: Long): Long
}
/**
* Repository für den Zugriff auf Buchungen.
*/
interface BuchungRepository {
fun findByKonto(kontoId: Uuid): List<Buchung>
fun findById(buchungId: Uuid): Buchung?
fun findByVeranstaltungAndZeitraum(
veranstaltungId: Uuid,
von: Instant,
bis: Instant
): List<Buchung>
fun save(buchung: Buchung): Buchung
}
/**
* Repository für den Zugriff auf Tagesabschlüsse.
*/
interface TagesabschlussRepository {
fun findByVeranstaltung(veranstaltungId: Uuid): List<Tagesabschluss>
fun save(abschluss: Tagesabschluss): Tagesabschluss
}
@@ -0,0 +1,127 @@
# ===================================================================
# Multi-stage Dockerfile for Meldestelle Billing Service
# Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts
# Version: 2.6.0 - Reliable Monorepo Build
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG BUILD_DATE
ARG VERSION=1.0.0-SNAPSHOT
# ===================================================================
# Build Stage
# ===================================================================
FROM eclipse-temurin:${JAVA_VERSION}-jdk-alpine AS builder
ARG VERSION
ARG BUILD_DATE
LABEL stage=builder \
service="billing-service" \
maintainer="Meldestelle Development Team"
WORKDIR /workspace
# Gradle optimizations
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.daemon=false \
-Dorg.gradle.parallel=true \
-Dorg.gradle.workers.max=2 \
-Dorg.gradle.jvmargs=-Xmx2g \
-XX:+UseParallelGC \
-XX:MaxMetaspaceSize=512m"
ENV GRADLE_USER_HOME=/root/.gradle
# 1. Copy full project structure for a reliable monorepo build
# .dockerignore should be used to exclude unnecessary files (IDE, logs, etc.)
COPY . .
RUN chmod +x gradlew
# 2. Build the service
RUN --mount=type=cache,target=/root/.gradle/caches \
--mount=type=cache,target=/root/.gradle/wrapper \
./gradlew :backend:services:billing:billing-service:bootJar --no-daemon --info
# 3. Extract layers
WORKDIR /builder
RUN cp /workspace/backend/services/billing/billing-service/build/libs/*.jar app.jar && \
java -Djarmode=layertools -jar app.jar extract
# ===================================================================
# Runtime Stage
# ===================================================================
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
ARG BUILD_DATE
ARG VERSION
ARG JAVA_VERSION
LABEL service="billing-service" \
version="${VERSION}" \
description="Microservice for Billing and Payments" \
maintainer="Meldestelle Development Team" \
java.version="${JAVA_VERSION}" \
build.date="${BUILD_DATE}"
ARG APP_USER=appuser
ARG APP_GROUP=appgroup
ARG APP_UID=1001
ARG APP_GID=1001
WORKDIR /app
RUN apk update && \
apk upgrade && \
apk add --no-cache curl tzdata tini && \
rm -rf /var/cache/apk/* && addgroup -g ${APP_GID} -S ${APP_GROUP} && \
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh && \
mkdir -p /app/logs /app/tmp /app/config && \
chown -R ${APP_USER}:${APP_GROUP} /app && \
chmod -R 750 /app
# Copy Spring Boot layers
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/spring-boot-loader/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/snapshot-dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/application/ ./
USER ${APP_USER}
EXPOSE 8087 5005
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS --max-time 2 http://localhost:8087/actuator/health/readiness || exit 1
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-XX:+UseStringDeduplication \
-XX:+UseContainerSupport \
-XX:G1HeapRegionSize=16m \
-XX:G1ReservePercent=25 \
-XX:InitiatingHeapOccupancyPercent=30 \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-Djava.security.egd=file:/dev/./urandom \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Europe/Vienna \
-Dspring.backgroundpreinitializer.ignore=true \
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus \
-Dmanagement.endpoint.health.show-details=always \
-Dmanagement.prometheus.metrics.export.enabled=true"
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
SERVER_PORT=8087 \
LOGGING_LEVEL_ROOT=INFO
ENTRYPOINT ["tini", "--", "sh", "-c", "\
echo 'Starting Billing Service with Java ${JAVA_VERSION}...'; \
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
echo 'DEBUG mode enabled'; \
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 org.springframework.boot.loader.launch.JarLauncher; \
else \
exec java ${JAVA_OPTS} org.springframework.boot.loader.launch.JarLauncher; \
fi"]
@@ -0,0 +1,46 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinSpring)
}
springBoot {
mainClass.set("at.mocode.billing.service.BillingServiceApplicationKt")
}
dependencies {
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreUtils)
implementation(projects.core.coreDomain)
implementation(projects.backend.services.billing.billingDomain)
// Spring Boot Starters
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
implementation(libs.jackson.module.kotlin)
implementation(libs.openpdf)
implementation(libs.spring.cloud.starter.consul.discovery)
implementation(libs.micrometer.tracing.bridge.brave)
implementation(libs.zipkin.reporter.brave)
implementation(libs.zipkin.sender.okhttp3)
// Datenbank-Abhängigkeiten
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlin.datetime)
implementation(libs.hikari.cp)
runtimeOnly(libs.postgresql.driver)
testRuntimeOnly(libs.h2.driver)
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.spring.boot.starter.test)
}
tasks.test {
useJUnitPlatform()
}
@@ -0,0 +1,158 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.api.rest
import at.mocode.billing.domain.model.Buchung
import at.mocode.billing.domain.model.BuchungsTyp
import at.mocode.billing.domain.model.TeilnehmerKonto
import at.mocode.billing.service.PdfService
import at.mocode.billing.service.TeilnehmerKontoService
import at.mocode.core.domain.serialization.InstantSerializer
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import kotlinx.serialization.Serializable
@RestController
@RequestMapping("/api/billing")
class BillingController(
private val kontoService: TeilnehmerKontoService,
private val pdfService: PdfService
) {
data class KontoDto(
val kontoId: String,
val veranstaltungId: String,
val personId: String,
val personName: String,
val saldoCent: Long,
val bemerkungen: String?,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
data class BuchungDto(
val buchungId: String,
val kontoId: String,
val betragCent: Long,
val typ: BuchungsTyp,
val verwendungszweck: String,
@Serializable(with = InstantSerializer::class)
val gebuchtAm: Instant
)
data class CreateKontoRequest(
@field:NotNull val veranstaltungId: String,
@field:NotNull val personId: String,
@field:NotBlank val personName: String
)
data class BuchungRequest(
@field:NotNull val betragCent: Long,
@field:NotNull val typ: BuchungsTyp,
@field:NotBlank val verwendungszweck: String
)
@GetMapping("/konten/{kontoId}")
fun getKonto(@PathVariable kontoId: String): ResponseEntity<KontoDto> {
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getKontoById(uuid) ?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(konto.toDto())
}
@GetMapping("/konten")
fun getKontoByVeranstaltungUndPerson(
@RequestParam veranstaltungId: String,
@RequestParam personId: String
): ResponseEntity<KontoDto> {
val vUuid = try { Uuid.parse(veranstaltungId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val pUuid = try { Uuid.parse(personId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getOrCreateKonto(vUuid, pUuid, "Unbekannt") // Name wird bei getOrCreate ggf. ignoriert wenn existiert
return ResponseEntity.ok(konto.toDto())
}
@PostMapping("/konten")
fun createKonto(@Valid @RequestBody request: CreateKontoRequest): ResponseEntity<KontoDto> {
val vUuid = try { Uuid.parse(request.veranstaltungId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val pUuid = try { Uuid.parse(request.personId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getOrCreateKonto(vUuid, pUuid, request.personName)
return ResponseEntity.ok(konto.toDto())
}
@GetMapping("/konten/{kontoId}/buchungen")
fun getBuchungen(@PathVariable kontoId: String): ResponseEntity<List<BuchungDto>> {
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val buchungen = kontoService.getBuchungsHistorie(uuid)
return ResponseEntity.ok(buchungen.map { it.toDto() })
}
@PostMapping("/konten/{kontoId}/buchungen")
fun addBuchung(
@PathVariable kontoId: String,
@Valid @RequestBody request: BuchungRequest
): ResponseEntity<KontoDto> {
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.buche(
kontoId = uuid,
betragCent = request.betragCent,
typ = request.typ,
zweck = request.verwendungszweck
)
return ResponseEntity.ok(konto.toDto())
}
@GetMapping("/konten/{kontoId}/rechnung", produces = [MediaType.APPLICATION_PDF_VALUE])
fun downloadRechnung(@PathVariable kontoId: String): ResponseEntity<ByteArray> {
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getKontoById(uuid) ?: return ResponseEntity.notFound().build()
val pdf = pdfService.generateRechnung(konto)
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"rechnung_${konto.personName.replace(" ", "_")}.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf)
}
@GetMapping("/veranstaltungen/{veranstaltungId}/offene-posten")
fun getOffenePosten(@PathVariable veranstaltungId: String): ResponseEntity<List<KontoDto>> {
val uuid = try { Uuid.parse(veranstaltungId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konten = kontoService.getOffenePosten(uuid)
return ResponseEntity.ok(konten.map { it.toDto() })
}
@GetMapping("/veranstaltungen/{veranstaltungId}/konten")
fun getKontenFuerVeranstaltung(@PathVariable veranstaltungId: String): ResponseEntity<List<KontoDto>> {
val uuid = try { Uuid.parse(veranstaltungId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konten = kontoService.getKontenFuerVeranstaltung(uuid)
return ResponseEntity.ok(konten.map { it.toDto() })
}
private fun TeilnehmerKonto.toDto() = KontoDto(
kontoId = kontoId.toString(),
veranstaltungId = veranstaltungId.toString(),
personId = personId.toString(),
personName = personName,
saldoCent = saldoCent,
bemerkungen = bemerkungen,
updatedAt = updatedAt
)
private fun Buchung.toDto() = BuchungDto(
buchungId = buchungId.toString(),
kontoId = kontoId.toString(),
betragCent = betragCent,
typ = typ,
verwendungszweck = verwendungszweck,
gebuchtAm = gebuchtAm
)
}
@@ -0,0 +1,35 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
import org.springframework.context.event.EventListener
import org.springframework.core.env.Environment
import kotlin.uuid.ExperimentalUuidApi
@EnableDiscoveryClient
@SpringBootApplication
class BillingServiceApplication(private val env: Environment) {
private val log = LoggerFactory.getLogger(BillingServiceApplication::class.java)
@EventListener(ApplicationReadyEvent::class)
fun onApplicationReady() {
val springPort = env.getProperty("server.port", "8087")
val appName = env.getProperty("spring.application.name", "billing-service")
log.info("----------------------------------------------------------")
log.info("Application '{}' is running!", appName)
log.info("Spring Management Port: {}", springPort)
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
log.info("----------------------------------------------------------")
}
}
fun main(args: Array<String>) {
runApplication<BillingServiceApplication>(*args)
}
@@ -0,0 +1,92 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service
import at.mocode.billing.domain.model.TeilnehmerKonto
import at.mocode.billing.domain.repository.BuchungRepository
import com.lowagie.text.*
import com.lowagie.text.pdf.PdfPCell
import com.lowagie.text.pdf.PdfPTable
import com.lowagie.text.pdf.PdfWriter
import org.springframework.stereotype.Service
import java.awt.Color
import java.io.ByteArrayOutputStream
import java.text.NumberFormat
import java.util.*
import kotlin.uuid.ExperimentalUuidApi
@Service
class PdfService(
private val buchungRepository: BuchungRepository
) {
fun generateRechnung(konto: TeilnehmerKonto): ByteArray {
val out = ByteArrayOutputStream()
val document = Document(PageSize.A4)
PdfWriter.getInstance(document, out)
document.open()
// Header
val titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18f)
val header = Paragraph("Rechnung / Kontoauszug", titleFont)
header.alignment = Element.ALIGN_CENTER
header.spacingAfter = 20f
document.add(header)
// Teilnehmer Info
val infoFont = FontFactory.getFont(FontFactory.HELVETICA, 12f)
document.add(Paragraph("Teilnehmer: ${konto.personName}", infoFont))
document.add(Paragraph("Datum: ${java.time.LocalDate.now()}", infoFont))
document.add(Paragraph("Konto-ID: ${konto.kontoId}", infoFont))
document.add(Paragraph("Veranstaltung: ${konto.veranstaltungId}", infoFont))
document.add(Paragraph(" ", infoFont))
// Tabelle
val table = PdfPTable(4)
table.widthPercentage = 100f
table.setWidths(floatArrayOf(2f, 4f, 2f, 2f))
val headFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 11f)
fun addCell(text: String, font: Font = headFont, bgColor: Color? = Color.LIGHT_GRAY) {
val cell = PdfPCell(Phrase(text, font))
if (bgColor != null) cell.backgroundColor = bgColor
cell.setPadding(5f)
table.addCell(cell)
}
addCell("Datum")
addCell("Zweck")
addCell("Typ")
addCell("Betrag")
val buchungen = buchungRepository.findByKonto(konto.kontoId)
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.GERMANY)
val bodyFont = FontFactory.getFont(FontFactory.HELVETICA, 10f)
buchungen.forEach { b ->
addCell(b.gebuchtAm.toString().substring(0, 10), bodyFont, null)
addCell(b.verwendungszweck, bodyFont, null)
addCell(b.typ.name, bodyFont, null)
val betragStr = currencyFormat.format(b.betragCent / 100.0)
addCell(betragStr, bodyFont, null)
}
document.add(table)
// Saldo
val saldoFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14f)
val saldoPara = Paragraph(" ", saldoFont)
saldoPara.spacingBefore = 20f
document.add(saldoPara)
val saldoText = "Gesamtsaldo: ${currencyFormat.format(konto.saldoCent / 100.0)}"
val finalSaldo = Paragraph(saldoText, saldoFont)
finalSaldo.alignment = Element.ALIGN_RIGHT
document.add(finalSaldo)
document.close()
return out.toByteArray()
}
}
@@ -0,0 +1,67 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service
import at.mocode.billing.domain.model.BuchungsTyp
import at.mocode.billing.domain.model.Tagesabschluss
import at.mocode.billing.domain.repository.BuchungRepository
import at.mocode.billing.domain.repository.TagesabschlussRepository
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.springframework.stereotype.Service
import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Service
class TagesabschlussService(
private val buchungRepository: BuchungRepository,
private val tagesabschlussRepository: TagesabschlussRepository
) {
/**
* Erstellt einen Tagesabschluss für die angegebene Veranstaltung und den Zeitraum.
* Standardmäßig wird der Zeitraum von "heute 00:00" bis "jetzt" genommen,
* wenn keine Zeiten angegeben sind.
*/
fun erstelleAbschluss(
veranstaltungId: Uuid,
von: Instant,
bis: Instant,
abgeschlossenVon: String,
bemerkungen: String? = null
): Tagesabschluss {
return transaction {
val buchungen = buchungRepository.findByVeranstaltungAndZeitraum(veranstaltungId, von, bis)
val summeBar = buchungen
.filter { it.typ == BuchungsTyp.ZAHLUNG_BAR }
.sumOf { it.betragCent }
val summeKarte = buchungen
.filter { it.typ == BuchungsTyp.ZAHLUNG_KARTE }
.sumOf { it.betragCent }
val summeGutschrift = buchungen
.filter { it.typ == BuchungsTyp.GUTSCHRIFT }
.sumOf { it.betragCent }
val abschluss = Tagesabschluss(
veranstaltungId = veranstaltungId,
abgeschlossenVon = abgeschlossenVon,
summeBarCent = summeBar,
summeKarteCent = summeKarte,
summeGutschriftCent = summeGutschrift,
anzahlBuchungen = buchungen.size,
bemerkungen = bemerkungen
)
tagesabschlussRepository.save(abschluss)
}
}
fun getAbschluesse(veranstaltungId: Uuid): List<Tagesabschluss> {
return transaction {
tagesabschlussRepository.findByVeranstaltung(veranstaltungId)
}
}
}
@@ -0,0 +1,130 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service
import at.mocode.billing.domain.model.Buchung
import at.mocode.billing.domain.model.BuchungsTyp
import at.mocode.billing.domain.model.TeilnehmerKonto
import at.mocode.billing.domain.repository.BuchungRepository
import at.mocode.billing.domain.repository.TeilnehmerKontoRepository
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.springframework.stereotype.Service
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Service
class TeilnehmerKontoService(
private val kontoRepository: TeilnehmerKontoRepository,
private val buchungRepository: BuchungRepository
) {
fun getOrCreateKonto(veranstaltungId: Uuid, personId: Uuid, personName: String): TeilnehmerKonto {
return transaction {
kontoRepository.findByVeranstaltungAndPerson(veranstaltungId, personId)
?: kontoRepository.save(
TeilnehmerKonto(
veranstaltungId = veranstaltungId,
personId = personId,
personName = personName
)
)
}
}
fun getKontoById(kontoId: Uuid): TeilnehmerKonto? {
return transaction {
kontoRepository.findById(kontoId)
}
}
fun getKonto(veranstaltungId: Uuid, personId: Uuid): TeilnehmerKonto? {
return transaction {
kontoRepository.findByVeranstaltungAndPerson(veranstaltungId, personId)
}
}
fun getBuchungsHistorie(kontoId: Uuid): List<Buchung> {
return transaction {
buchungRepository.findByKonto(kontoId)
}
}
fun buche(kontoId: Uuid, betragCent: Long, typ: BuchungsTyp, zweck: String): TeilnehmerKonto {
return transaction {
val konto = kontoRepository.findById(kontoId) ?: throw IllegalArgumentException("Konto nicht gefunden: $kontoId")
// Validierung: Bestimmte Typen sind immer "Soll" (negativ), andere "Haben" (positiv/Zahlung)
val validierterBetrag = when (typ) {
BuchungsTyp.NENNGELD,
BuchungsTyp.NENNGEBUEHR,
BuchungsTyp.NACHNENNGEBUEHR,
BuchungsTyp.STARTGEBUEHR,
BuchungsTyp.SPORTFOERDERBEITRAG,
BuchungsTyp.BOXENGEBUEHR -> if (betragCent > 0) -betragCent else betragCent
BuchungsTyp.ZAHLUNG_BAR,
BuchungsTyp.ZAHLUNG_KARTE,
BuchungsTyp.GUTSCHRIFT -> if (betragCent < 0) -betragCent else betragCent
BuchungsTyp.STORNIERUNG -> betragCent // Storno kann beides sein (Gegenbuchung)
}
val buchung = Buchung(
kontoId = kontoId,
betragCent = validierterBetrag,
typ = typ,
verwendungszweck = zweck
)
buchungRepository.save(buchung)
val neuerSaldo = konto.saldoCent + validierterBetrag
kontoRepository.updateSaldo(kontoId, neuerSaldo)
kontoRepository.findById(kontoId)!!
}
}
fun getKontenFuerVeranstaltung(veranstaltungId: Uuid): List<TeilnehmerKonto> {
return transaction {
kontoRepository.findByVeranstaltung(veranstaltungId)
}
}
fun getOffenePosten(veranstaltungId: Uuid): List<TeilnehmerKonto> {
return transaction {
kontoRepository.findOffenePosten(veranstaltungId)
}
}
/**
* Storniert eine existierende Buchung durch eine Gegenbuchung.
*/
fun storniereBuchung(buchungId: Uuid, grund: String): TeilnehmerKonto {
return transaction {
val ursprung = buchungRepository.findById(buchungId)
?: throw IllegalArgumentException("Buchung nicht gefunden: $buchungId")
if (ursprung.typ == BuchungsTyp.STORNIERUNG) {
throw IllegalArgumentException("Ein Storno kann nicht erneut storniert werden.")
}
val konto = kontoRepository.findById(ursprung.kontoId)!!
// Gegenbuchung erstellen (Betrag umkehren)
val stornoBuchung = Buchung(
kontoId = ursprung.kontoId,
betragCent = -ursprung.betragCent,
typ = BuchungsTyp.STORNIERUNG,
verwendungszweck = "Storno von ${ursprung.buchungId}: $grund",
storniertBuchungId = ursprung.buchungId
)
buchungRepository.save(stornoBuchung)
val neuerSaldo = konto.saldoCent - ursprung.betragCent
kontoRepository.updateSaldo(konto.kontoId, neuerSaldo)
kontoRepository.findById(konto.kontoId)!!
}
}
}
@@ -0,0 +1,44 @@
package at.mocode.billing.service.config
import at.mocode.billing.service.persistence.BuchungTable
import at.mocode.billing.service.persistence.TagesabschlussTable
import at.mocode.billing.service.persistence.TeilnehmerKontoTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
@Configuration
class BillingDatabaseConfiguration(
@Value("\${spring.datasource.url:}") private val jdbcUrl: String,
@Value("\${spring.datasource.username:}") private val username: String,
@Value("\${spring.datasource.password:}") private val password: String
) {
private val log = LoggerFactory.getLogger(BillingDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
if (jdbcUrl.isBlank()) {
log.warn("No spring.datasource.url provided. Skipping Billing database initialization.")
return
}
log.info("Initializing database schema for Billing Service...")
try {
Database.connect(jdbcUrl, user = username, password = password)
transaction {
SchemaUtils.create(
TeilnehmerKontoTable,
BuchungTable,
TagesabschlussTable
)
}
log.info("Billing database schema initialized successfully")
} catch (e: Exception) {
log.error("Failed to initialize billing database schema", e)
}
}
}
@@ -0,0 +1,69 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
import kotlin.uuid.ExperimentalUuidApi
/**
* Exposed-Tabellendefinition für das Teilnehmer-Konto.
*/
object TeilnehmerKontoTable : Table("teilnehmer_konten") {
val id = uuid("konto_id")
val veranstaltungId = uuid("veranstaltung_id")
val personId = uuid("person_id")
val personName = varchar("person_name", 200)
val saldoCent = long("saldo_cent").default(0L)
val bemerkungen = text("bemerkungen").nullable()
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_konto_veranstaltung_person", isUnique = true, veranstaltungId, personId)
}
}
/**
* Exposed-Tabellendefinition für Buchungen.
*/
object BuchungTable : Table("buchungen") {
val id = uuid("buchung_id")
val kontoId = uuid("konto_id")
val betragCent = long("betrag_cent")
val typ = varchar("typ", 50)
val verwendungszweck = varchar("verwendungszweck", 500)
val gebuchtAm = timestamp("gebucht_am").defaultExpression(CurrentTimestamp)
val storniertBuchungId = uuid("storniert_buchung_id").nullable()
override val primaryKey = PrimaryKey(id)
init {
index("idx_buchung_konto", isUnique = false, kontoId)
}
}
/**
* Exposed-Tabellendefinition für Tagesabschlüsse.
*/
object TagesabschlussTable : Table("tagesabschluesse") {
val id = uuid("tagesabschluss_id")
val veranstaltungId = uuid("veranstaltung_id")
val abgeschlossenAm = timestamp("abgeschlossen_am").defaultExpression(CurrentTimestamp)
val abgeschlossenVon = varchar("abgeschlossen_von", 200)
val summeBarCent = long("summe_bar_cent")
val summeKarteCent = long("summe_karte_cent")
val summeGutschriftCent = long("summe_gutschrift_cent")
val anzahlBuchungen = integer("anzahl_buchungen")
val bemerkungen = text("bemerkungen").nullable()
override val primaryKey = PrimaryKey(id)
init {
index("idx_tagesabschluss_veranstaltung", isUnique = false, veranstaltungId)
}
}
@@ -0,0 +1,189 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service.persistence
import at.mocode.billing.domain.model.Buchung
import at.mocode.billing.domain.model.BuchungsTyp
import at.mocode.billing.domain.model.Tagesabschluss
import at.mocode.billing.domain.model.TeilnehmerKonto
import at.mocode.billing.domain.repository.BuchungRepository
import at.mocode.billing.domain.repository.TagesabschlussRepository
import at.mocode.billing.domain.repository.TeilnehmerKontoRepository
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import org.springframework.stereotype.Repository
import kotlin.time.Instant
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Repository
class ExposedTeilnehmerKontoRepository : TeilnehmerKontoRepository {
override fun findByVeranstaltungAndPerson(veranstaltungId: Uuid, personId: Uuid): TeilnehmerKonto? {
return TeilnehmerKontoTable
.selectAll()
.where { (TeilnehmerKontoTable.veranstaltungId eq veranstaltungId) and (TeilnehmerKontoTable.personId eq personId) }
.singleOrNull()
?.toModel()
}
override fun findById(kontoId: Uuid): TeilnehmerKonto? {
return TeilnehmerKontoTable
.selectAll()
.where { TeilnehmerKontoTable.id eq kontoId }
.singleOrNull()
?.toModel()
}
override fun findByVeranstaltung(veranstaltungId: Uuid): List<TeilnehmerKonto> {
return TeilnehmerKontoTable
.selectAll()
.where { TeilnehmerKontoTable.veranstaltungId eq veranstaltungId }
.map { it.toModel() }
}
override fun findOffenePosten(veranstaltungId: Uuid): List<TeilnehmerKonto> {
return TeilnehmerKontoTable
.selectAll()
.where { (TeilnehmerKontoTable.veranstaltungId eq veranstaltungId) and (TeilnehmerKontoTable.saldoCent less 0) }
.map { it.toModel() }
}
override fun save(konto: TeilnehmerKonto): TeilnehmerKonto {
val existing = findById(konto.kontoId)
if (existing == null) {
TeilnehmerKontoTable.insert {
it[id] = konto.kontoId
it[veranstaltungId] = konto.veranstaltungId
it[personId] = konto.personId
it[personName] = konto.personName
it[saldoCent] = konto.saldoCent
it[bemerkungen] = konto.bemerkungen
}
} else {
TeilnehmerKontoTable.update({ TeilnehmerKontoTable.id eq konto.kontoId }) {
it[personName] = konto.personName
it[saldoCent] = konto.saldoCent
it[bemerkungen] = konto.bemerkungen
it[updatedAt] = CurrentTimestamp
}
}
return findById(konto.kontoId)!!
}
override fun updateSaldo(kontoId: Uuid, saldoCent: Long): Long {
TeilnehmerKontoTable.update({ TeilnehmerKontoTable.id eq kontoId }) {
it[this.saldoCent] = saldoCent
it[updatedAt] = CurrentTimestamp
}
return saldoCent
}
private fun ResultRow.toModel() = TeilnehmerKonto(
kontoId = this[TeilnehmerKontoTable.id],
veranstaltungId = this[TeilnehmerKontoTable.veranstaltungId],
personId = this[TeilnehmerKontoTable.personId],
personName = this[TeilnehmerKontoTable.personName],
saldoCent = this[TeilnehmerKontoTable.saldoCent],
bemerkungen = this[TeilnehmerKontoTable.bemerkungen],
updatedAt = this[TeilnehmerKontoTable.updatedAt]
)
}
@Repository
class ExposedBuchungRepository : BuchungRepository {
override fun findByKonto(kontoId: Uuid): List<Buchung> {
return BuchungTable
.selectAll()
.where { BuchungTable.kontoId eq kontoId }
.map { it.toModel() }
}
override fun findById(buchungId: Uuid): Buchung? {
return BuchungTable
.selectAll()
.where { BuchungTable.id eq buchungId }
.singleOrNull()
?.toModel()
}
override fun findByVeranstaltungAndZeitraum(
veranstaltungId: Uuid,
von: Instant,
bis: Instant
): List<Buchung> {
// Da Buchungen über Konten verknüpft sind, müssen wir einen Join machen oder über die Konten der Veranstaltung filtern
return Join(BuchungTable, TeilnehmerKontoTable, JoinType.INNER, BuchungTable.kontoId, TeilnehmerKontoTable.id)
.selectAll()
.where {
(TeilnehmerKontoTable.veranstaltungId eq veranstaltungId) and
(BuchungTable.gebuchtAm.between(von, bis))
}
.map { it.toModel() }
}
override fun save(buchung: Buchung): Buchung {
BuchungTable.insert {
it[id] = buchung.buchungId
it[kontoId] = buchung.kontoId
it[betragCent] = buchung.betragCent
it[typ] = buchung.typ.name
it[verwendungszweck] = buchung.verwendungszweck
it[gebuchtAm] = buchung.gebuchtAm
it[storniertBuchungId] = buchung.storniertBuchungId
}
return buchung
}
private fun ResultRow.toModel() = Buchung(
buchungId = this[BuchungTable.id],
kontoId = this[BuchungTable.kontoId],
betragCent = this[BuchungTable.betragCent],
typ = BuchungsTyp.valueOf(this[BuchungTable.typ]),
verwendungszweck = this[BuchungTable.verwendungszweck],
gebuchtAm = this[BuchungTable.gebuchtAm],
storniertBuchungId = this[BuchungTable.storniertBuchungId]
)
}
@Repository
class ExposedTagesabschlussRepository : TagesabschlussRepository {
override fun findByVeranstaltung(veranstaltungId: Uuid): List<Tagesabschluss> {
return TagesabschlussTable
.selectAll()
.where { TagesabschlussTable.veranstaltungId eq veranstaltungId }
.map { it.toModel() }
}
override fun save(abschluss: Tagesabschluss): Tagesabschluss {
TagesabschlussTable.insert {
it[id] = abschluss.tagesabschlussId
it[veranstaltungId] = abschluss.veranstaltungId
it[abgeschlossenAm] = abschluss.abgeschlossenAm
it[abgeschlossenVon] = abschluss.abgeschlossenVon
it[summeBarCent] = abschluss.summeBarCent
it[summeKarteCent] = abschluss.summeKarteCent
it[summeGutschriftCent] = abschluss.summeGutschriftCent
it[anzahlBuchungen] = abschluss.anzahlBuchungen
it[bemerkungen] = abschluss.bemerkungen
}
return abschluss
}
private fun ResultRow.toModel() = Tagesabschluss(
tagesabschlussId = this[TagesabschlussTable.id],
veranstaltungId = this[TagesabschlussTable.veranstaltungId],
abgeschlossenAm = this[TagesabschlussTable.abgeschlossenAm],
abgeschlossenVon = this[TagesabschlussTable.abgeschlossenVon],
summeBarCent = this[TagesabschlussTable.summeBarCent],
summeKarteCent = this[TagesabschlussTable.summeKarteCent],
summeGutschriftCent = this[TagesabschlussTable.summeGutschriftCent],
anzahlBuchungen = this[TagesabschlussTable.anzahlBuchungen],
bemerkungen = this[TagesabschlussTable.bemerkungen]
)
}
@@ -0,0 +1,50 @@
spring:
application:
name: billing-service
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
driver-class-name: org.postgresql.Driver
cloud:
consul:
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
discovery:
enabled: true
register: true
prefer-ip-address: true
health-check-path: /actuator/health
health-check-interval: 10s
# health-check-port: 8089
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
service-name: ${spring.application.name}
port: ${billing.http.port:8089}
server:
port: 8089
billing:
http:
port: 8089 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
probes:
enabled: true
logging:
level:
root: INFO
# at.mocode.billing: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
@@ -0,0 +1,210 @@
openapi: 3.0.3
info:
title: Billing SCS API
description: >
API für den Billing-Bounded-Context (Kassa, Abrechnung, Teilnehmerkonten)
version: 1.0.0
servers:
- url: http://localhost:8089
description: Lokaler Entwicklungs-Server
paths:
/api/billing/konten:
get:
summary: Teilnehmerkonto suchen
description: Sucht ein Konto basierend auf Veranstaltungs-ID und Personen-ID. Erstellt das Konto, falls es nicht existiert.
parameters:
- name: veranstaltungId
in: query
required: true
schema:
type: string
format: uuid
- name: personId
in: query
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Teilnehmerkonto
content:
application/json:
schema:
$ref: '#/components/schemas/KontoDto'
'400':
description: Ungültige UUID-Formate
post:
summary: Teilnehmerkonto erstellen oder abrufen
description: Erstellt ein neues Teilnehmerkonto für eine Veranstaltung und eine Person.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateKontoRequest'
responses:
'200':
description: Teilnehmerkonto (neu erstellt oder bestehend)
content:
application/json:
schema:
$ref: '#/components/schemas/KontoDto'
'400':
description: Validierungsfehler
/api/billing/konten/{kontoId}:
get:
summary: Teilnehmerkonto nach ID abrufen
parameters:
- name: kontoId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Details zum Teilnehmerkonto
content:
application/json:
schema:
$ref: '#/components/schemas/KontoDto'
'404':
description: Konto nicht gefunden
'400':
description: Ungültige Konto-ID
/api/billing/konten/{kontoId}/buchungen:
get:
summary: Buchungshistorie abrufen
description: Liefert alle Buchungen für ein bestimmtes Teilnehmerkonto.
parameters:
- name: kontoId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Liste von Buchungen
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BuchungDto'
'400':
description: Ungültige Konto-ID
post:
summary: Buchung hinzufügen
description: Führt eine neue Buchung auf dem Teilnehmerkonto durch und aktualisiert den Saldo.
parameters:
- name: kontoId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BuchungRequest'
responses:
'200':
description: Aktualisiertes Teilnehmerkonto nach der Buchung
content:
application/json:
schema:
$ref: '#/components/schemas/KontoDto'
'400':
description: Validierungsfehler oder ungültige Konto-ID
components:
schemas:
KontoDto:
type: object
properties:
kontoId:
type: string
format: uuid
veranstaltungId:
type: string
format: uuid
personId:
type: string
format: uuid
personName:
type: string
saldoCent:
type: integer
format: int64
description: Aktueller Saldo in Cent
bemerkungen:
type: string
nullable: true
updatedAt:
type: string
format: date-time
description: Zeitpunkt der letzten Aktualisierung
BuchungDto:
type: object
properties:
buchungId:
type: string
format: uuid
kontoId:
type: string
format: uuid
betragCent:
type: integer
format: int64
description: Betrag in Cent (positiv für Gutschriften, negativ für Belastungen)
typ:
$ref: '#/components/schemas/BuchungsTyp'
verwendungszweck:
type: string
gebuchtAm:
type: string
format: date-time
CreateKontoRequest:
type: object
required:
- veranstaltungId
- personId
- personName
properties:
veranstaltungId:
type: string
format: uuid
personId:
type: string
format: uuid
personName:
type: string
minLength: 1
BuchungRequest:
type: object
required:
- betragCent
- typ
- verwendungszweck
properties:
betragCent:
type: integer
format: int64
typ:
$ref: '#/components/schemas/BuchungsTyp'
verwendungszweck:
type: string
minLength: 1
BuchungsTyp:
type: string
enum:
- NENNGEBUEHR
- KOPPELGEBUEHR
- NACHNENNGEBUEHR
- STARTGEBUEHR
- EINZAHLUNG
- AUSZAHLUNG
- SONSTIGES
@@ -0,0 +1,60 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service
import at.mocode.billing.domain.model.BuchungsTyp
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import kotlin.time.Clock
import kotlin.time.Duration.Companion.hours
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@SpringBootTest
@ActiveProfiles("test")
class TagesabschlussServiceTest {
@Autowired
lateinit var kontoService: TeilnehmerKontoService
@Autowired
lateinit var tagesabschlussService: TagesabschlussService
@Test
fun `Tagesabschluss aggregiert Buchungen korrekt`() {
val vId = Uuid.random()
val k1 = kontoService.getOrCreateKonto(vId, Uuid.random(), "Reiter A")
val k2 = kontoService.getOrCreateKonto(vId, Uuid.random(), "Reiter B")
val jetzt = Clock.System.now()
val von = jetzt - 1.hours
val bis = jetzt + 1.hours
// Buchungen erstellen
kontoService.buche(k1.kontoId, 5000L, BuchungsTyp.ZAHLUNG_BAR, "Barzahlung 1")
kontoService.buche(k2.kontoId, 3000L, BuchungsTyp.ZAHLUNG_BAR, "Barzahlung 2")
kontoService.buche(k1.kontoId, 2500L, BuchungsTyp.ZAHLUNG_KARTE, "Kartenzahlung")
kontoService.buche(k2.kontoId, 1000L, BuchungsTyp.GUTSCHRIFT, "Gutschrift")
// Gebühren (sollten nicht in den Zahlungs-Summen auftauchen)
kontoService.buche(k1.kontoId, 1500L, BuchungsTyp.NENNGEBUEHR, "Gebühr")
// Abschluss erstellen
val abschluss = tagesabschlussService.erstelleAbschluss(
veranstaltungId = vId,
von = von,
bis = bis,
abgeschlossenVon = "Admin"
)
assertNotNull(abschluss)
assertEquals(8000L, abschluss.summeBarCent)
assertEquals(2500L, abschluss.summeKarteCent)
assertEquals(1000L, abschluss.summeGutschriftCent)
assertEquals(5, abschluss.anzahlBuchungen) // 2x Bar + 1x Karte + 1x Gutschrift + 1x Gebühr
}
}
@@ -0,0 +1,94 @@
@file:OptIn(ExperimentalUuidApi::class)
package at.mocode.billing.service
import at.mocode.billing.domain.model.BuchungsTyp
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@SpringBootTest
@ActiveProfiles("test")
class TeilnehmerKontoServiceTest {
@Autowired
lateinit var service: TeilnehmerKontoService
@Test
fun `Konto erstellen und buchen`() {
val veranstaltungId = Uuid.random()
val personId = Uuid.random()
val personName = "Max Mustermann"
// 1. Konto erstellen
val konto = service.getOrCreateKonto(veranstaltungId, personId, personName)
assertNotNull(konto)
assertEquals(personName, konto.personName)
assertEquals(0L, konto.saldoCent)
// 2. Buchung durchführen
val updatedKonto = service.buche(
kontoId = konto.kontoId,
betragCent = 1500L,
typ = BuchungsTyp.NENNGEBUEHR,
zweck = "Nennung Bewerb 1"
)
assertEquals(-1500L, updatedKonto.saldoCent)
// 3. Buchungshistorie prüfen
val buchungen = service.getBuchungsHistorie(konto.kontoId)
assertEquals(1, buchungen.size)
assertEquals(-1500L, buchungen[0].betragCent)
assertEquals("Nennung Bewerb 1", buchungen[0].verwendungszweck)
}
@Test
fun `Mehrere Buchungen summieren sich korrekt`() {
val vId = Uuid.random()
val pId = Uuid.random()
val konto = service.getOrCreateKonto(vId, pId, "Susi Sorglos")
service.buche(konto.kontoId, 2000L, BuchungsTyp.STARTGEBUEHR, "Startgeld")
val finalKonto = service.buche(konto.kontoId, 500L, BuchungsTyp.STORNIERUNG, "Storno")
assertEquals(-1500L, finalKonto.saldoCent)
val historian = service.getBuchungsHistorie(konto.kontoId)
assertEquals(2, historian.size)
}
@Test
fun `Buchung stornieren`() {
val veranstaltungId = Uuid.random()
val personId = Uuid.random()
val konto = service.getOrCreateKonto(veranstaltungId, personId, "Storno Test")
// 1. Ursprüngliche Buchung
val gebuchtKonto = service.buche(
kontoId = konto.kontoId,
betragCent = 2500L,
typ = BuchungsTyp.BOXENGEBUEHR,
zweck = "Boxenmiete"
)
assertEquals(-2500L, gebuchtKonto.saldoCent)
val buchung = service.getBuchungsHistorie(konto.kontoId).first()
// 2. Stornieren
val storniertKonto = service.storniereBuchung(buchung.buchungId, "Falsche Box")
assertEquals(0L, storniertKonto.saldoCent)
// 3. Historie prüfen
val buchungen = service.getBuchungsHistorie(konto.kontoId)
assertEquals(2, buchungen.size)
assertTrue(buchungen.any { it.typ == BuchungsTyp.STORNIERUNG })
val storno = buchungen.find { it.typ == BuchungsTyp.STORNIERUNG }!!
assertEquals(2500L, storno.betragCent)
assertEquals(buchung.buchungId, storno.storniertBuchungId)
}
}
@@ -0,0 +1,16 @@
spring:
application:
name: billing-service-test
cloud:
consul:
enabled: false
discovery:
enabled: false
datasource:
url: jdbc:h2:mem:billing_test;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password: ""
h2:
console:
enabled: true
+39 -43
View File
@@ -1,23 +1,30 @@
# ===================================================================
# Multi-stage Dockerfile for Meldestelle Entries Service
# Features: Security hardening, monitoring support, optimal caching, BuildKit cache mounts
# Version: 2.6.0 - Reliable Monorepo Build
# ===================================================================
# === CENTRALIZED BUILD ARGUMENTS ===
ARG GRADLE_VERSION
ARG JAVA_VERSION
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG BUILD_DATE
ARG VERSION
ARG VERSION=1.0.0-SNAPSHOT
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
# ===================================================================
# Build Stage
# ===================================================================
FROM eclipse-temurin:${JAVA_VERSION}-jdk-alpine AS builder
ARG VERSION
ARG BUILD_DATE
LABEL stage=builder \
service=entries-service \
maintainer="Meldestelle Development Team" \
version="${VERSION}" \
build.date="${BUILD_DATE}"
service="entries-service" \
maintainer="Meldestelle Development Team"
WORKDIR /workspace
# Gradle optimizations
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.daemon=false \
-Dorg.gradle.parallel=true \
@@ -25,33 +32,26 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-Dorg.gradle.jvmargs=-Xmx2g \
-XX:+UseParallelGC \
-XX:MaxMetaspaceSize=512m"
ENV GRADLE_USER_HOME=/root/.gradle
ENV GRADLE_USER_HOME=/home/gradle/.gradle
# 1. Copy full project structure for a reliable monorepo build
# .dockerignore should be used to exclude unnecessary files (IDE, logs, etc.)
COPY . .
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
COPY gradle/ gradle/
RUN chmod +x gradlew
COPY platform/ platform/
COPY frontend/ frontend/
COPY core/ core/
COPY backend/ backend/
COPY docs/ docs/
COPY entries-service/build.gradle.kts ./
# Copy entries modules
COPY backend/services/entries/entries-api/ backend/services/entries/entries-api/
COPY backend/services/entries/entries-service/ backend/services/entries/entries-service/
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
./gradlew :backend:services:entries:entries-service:dependencies --no-daemon --info
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
# 2. Build the service
RUN --mount=type=cache,target=/root/.gradle/caches \
--mount=type=cache,target=/root/.gradle/wrapper \
./gradlew :backend:services:entries:entries-service:bootJar --no-daemon --info
# 3. Extract layers
WORKDIR /builder
RUN cp /workspace/backend/services/entries/entries-service/build/libs/*.jar app.jar && \
java -Djarmode=layertools -jar app.jar extract
# ===================================================================
# Runtime stage
# Runtime Stage
# ===================================================================
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
@@ -59,10 +59,6 @@ ARG BUILD_DATE
ARG VERSION
ARG JAVA_VERSION
ENV JAVA_VERSION=${JAVA_VERSION} \
VERSION=${VERSION} \
BUILD_DATE=${BUILD_DATE}
LABEL service="entries-service" \
version="${VERSION}" \
description="Microservice for Entries Management" \
@@ -80,15 +76,17 @@ WORKDIR /app
RUN apk update && \
apk upgrade && \
apk add --no-cache curl tzdata tini && \
rm -rf /var/cache/apk/* && \
addgroup -g ${APP_GID} -S ${APP_GROUP} && \
rm -rf /var/cache/apk/* && addgroup -g ${APP_GID} -S ${APP_GROUP} && \
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh && \
mkdir -p /app/logs /app/tmp /app/config && \
chown -R ${APP_USER}:${APP_GROUP} /app && \
chmod -R 750 /app
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} \
/workspace/backend/services/entries/entries-service/build/libs/*.jar app.jar
# Copy Spring Boot layers
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/spring-boot-loader/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/snapshot-dependencies/ ./
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/application/ ./
USER ${APP_USER}
@@ -115,17 +113,15 @@ ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
-Dmanagement.endpoint.health.show-details=always \
-Dmanagement.prometheus.metrics.export.enabled=true"
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS
ENV SERVER_PORT=8083
ENV LOGGING_LEVEL_ROOT=INFO
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
SERVER_PORT=8083 \
LOGGING_LEVEL_ROOT=INFO
ENTRYPOINT ["tini", "--", "sh", "-c", "\
echo 'Starting Entries Service with Java ${JAVA_VERSION}...'; \
echo 'Service port: ${SERVER_PORT}'; \
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
echo 'DEBUG mode enabled'; \
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar; \
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 org.springframework.boot.loader.launch.JarLauncher; \
else \
echo 'Starting Entries Service in production mode'; \
exec java ${JAVA_OPTS} -jar app.jar; \
exec java ${JAVA_OPTS} org.springframework.boot.loader.launch.JarLauncher; \
fi"]
@@ -1,3 +1,7 @@
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
@@ -7,38 +11,27 @@ group = "at.mocode"
version = "1.0.0"
kotlin {
// Toolchain is now handled centrally in the root build.gradle.kts
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
// JVM target for backend usage
jvm()
// JS target for frontend usage (Compose/Browser)
js {
wasmJs {
browser()
// no need for binaries.executable() in a library
}
// Optional Wasm target for browser clients
if (enableWasm) {
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
}
sourceSets {
commonMain {
dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(projects.core.coreDomain)
}
commonMain.dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
}
commonTest.dependencies {
implementation(kotlin("test"))
}
jvmTest.dependencies {
implementation(projects.platform.platformTesting)
}
}
}
@@ -2,7 +2,7 @@
package at.mocode.entries.api
import at.mocode.core.domain.model.NennungsStatusE
import at.mocode.core.domain.model.NennStatusE
import at.mocode.core.domain.model.StartwunschE
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
@@ -13,28 +13,28 @@ import kotlin.uuid.Uuid
*/
@Serializable
data class NennungDetailDto(
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val nennungId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val reiterId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val pferdId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val zahlerId: Uuid? = null,
val status: NennungsStatusE,
val startwunsch: StartwunschE,
val istNachnennung: Boolean,
val nachnenngebuehrErlassen: Boolean,
val isNachnenngebuehrFaellig: Boolean,
val bemerkungen: String? = null,
val createdAt: String,
val updatedAt: String
val status: NennStatusE,
val startwunsch: StartwunschE,
val istNachnennung: Boolean,
val nachnenngebuehrErlassen: Boolean,
val isNachnenngebuehrFaellig: Boolean,
val bemerkungen: String? = null,
val createdAt: String,
val updatedAt: String
)
/**
@@ -42,21 +42,21 @@ data class NennungDetailDto(
*/
@Serializable
data class NennungSummaryDto(
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val nennungId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val reiterId: Uuid,
@Serializable(with = UuidSerializer::class)
@Serializable(with = UuidSerializer::class)
val pferdId: Uuid,
val status: NennungsStatusE,
val istNachnennung: Boolean,
val createdAt: String
val status: NennStatusE,
val istNachnennung: Boolean,
val createdAt: String
)
/**
@@ -78,7 +78,8 @@ data class NennungEinreichenRequest(
val zahlerId: Uuid? = null,
val startwunsch: StartwunschE = StartwunschE.KEIN_WUNSCH,
val istNachnennung: Boolean = false,
val bemerkungen: String? = null
val bemerkungen: String? = null,
val email: String? = null
)
/**
@@ -86,8 +87,8 @@ data class NennungEinreichenRequest(
*/
@Serializable
data class NennungStatusAendernRequest(
val neuerStatus: NennungsStatusE,
val bemerkungen: String? = null
val neuerStatus: NennStatusE,
val bemerkungen: String? = null
)
/**
@@ -1,26 +1,39 @@
@file:OptIn(ExperimentalWasmDsl::class)
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvm()
wasmJs {
browser()
}
sourceSets {
commonMain {
kotlin.srcDir("src/main/kotlin")
dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.backend.services.masterdata.masterdataDomain)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
}
all {
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
}
commonTest {
kotlin.srcDir("src/test/kotlin")
dependencies {
implementation(kotlin("test"))
implementation(projects.platform.platformTesting)
}
commonMain.dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.backend.services.masterdata.masterdataDomain)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
}
commonTest.dependencies {
implementation(kotlin("test"))
}
jvmTest.dependencies {
implementation(projects.platform.platformTesting)
}
}
}
@@ -3,6 +3,7 @@
package at.mocode.entries.domain.model
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
import at.mocode.core.domain.model.BesichtigungsTypE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
@@ -13,7 +14,7 @@ import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Abteilung im registration-context.
*
* Eine Abteilung ist die kleinste startbare Einheit innerhalb eines [DomBewerb]s.
* Eine Abteilung ist die kleinste startbare Einheit innerhalb eines [Bewerb]s.
* Ein Bewerb kann in mehrere Abteilungen aufgeteilt sein (z.B. Abt. 1: ohne Lizenz,
* Abt. 2: mit Lizenz R1). Die Aufteilung erfolgt gemäß ÖTO § 39 und den
* spartenspezifischen Bestimmungen.
@@ -32,7 +33,7 @@ import kotlin.uuid.Uuid
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomAbteilung(
data class Abteilung(
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid = Uuid.random(),
@@ -55,6 +56,9 @@ data class DomAbteilung(
// Zeitplanung
var startzeit: String? = null,
/** Besichtigungstyp für diese Abteilung (optional, wenn abweichend von Standard). */
var besichtigungsTyp: BesichtigungsTypE? = null,
// Verwaltung
var bemerkungen: String? = null,
@@ -81,22 +85,31 @@ data class DomAbteilung(
* Validiert die Abteilung auf Überschreitung des maximalen Starter-Limits (§ 39 Abs. 2).
* Gibt Warnungen zurück (kein harter Fehler Override-Event möglich, ADR-0016).
*/
fun validateStarterLimit(): List<String> {
val warnings = mutableListOf<String>()
fun validateStarterLimit(): List<AbteilungsWarnung> {
val warnings = mutableListOf<AbteilungsWarnung>()
// Maximale Abteilungsgröße nach Teilung: > 80 Starter → erneute Teilung verpflichtend (§ 39 Abs. 2)
if (starterAnzahl > 80) {
warnings.add(
"WARN_ABTEILUNG_PFLICHT_TEILUNG_UEBERSCHRITTEN: ${getDisplayName()}, " +
"Starter: $starterAnzahl > 80. Erneute Teilung verpflichtend (§ 39 Abs. 2). " +
"Override möglich (TBA-Entscheidung)."
AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_ABTEILUNG_ZU_GROSS,
bewerbId = bewerbId,
abteilungId = abteilungId,
nachricht = "WARN_ABTEILUNG_ZU_GROSS: ${getDisplayName()}, Starter: $starterAnzahl > 80. Erneute Teilung verpflichtend.",
oetoParagraph = "§ 39 Abs. 2"
)
)
}
if (maxStarter > 0 && starterAnzahl > maxStarter) {
warnings.add(
"WARN_ABTEILUNG_MAX_STARTER_UEBERSCHRITTEN: ${getDisplayName()}, " +
"Starter: $starterAnzahl > Limit $maxStarter. Override möglich (TBA-Entscheidung)."
AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_ABTEILUNG_MAX_UEBERSCHRITTEN,
bewerbId = bewerbId,
abteilungId = abteilungId,
nachricht = "WARN_ABTEILUNG_MAX_UEBERSCHRITTEN: ${getDisplayName()}, Starter: $starterAnzahl > Limit $maxStarter.",
oetoParagraph = "Hausregel / Ausschreibung"
)
)
}
@@ -106,5 +119,5 @@ data class DomAbteilung(
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomAbteilung = this.copy(updatedAt = Clock.System.now())
fun withUpdatedTimestamp(): Abteilung = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,71 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Value Object für eine Abteilungs-Warnung (ÖTO § 39).
*
* Eine Warnung wird ausgegeben, wenn Schwellenwerte überschritten werden oder
* strukturelle Teilungen fehlen. Alle Warnungen sind overridebar (ADR-0007).
*/
@Serializable
data class AbteilungsWarnung(
val code: AbteilungsWarnungCodeE,
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid? = null,
val nachricht: String,
val oetoParagraph: String,
val istOverridebar: Boolean = true,
@Serializable(with = InstantSerializer::class)
val timestamp: Instant = Clock.System.now()
)
/**
* Maschinenlesbare Codes für Abteilungs-Warnungen.
*/
enum class AbteilungsWarnungCodeE {
/** Starterzahl > Pflicht-Schwellenwert (§ 39 Abs. 2) */
WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN,
/** Starterzahl > Kann-Schwellenwert, keine Teilung konfiguriert (§ 39 Abs. 2) */
WARN_KANN_TEILUNG_EMPFOHLEN,
/** Abteilung nach Teilung > 80 Starter (§ 39 Abs. 2) */
WARN_ABTEILUNG_ZU_GROSS,
/** Starter > konfiguriertes maxStarter-Limit */
WARN_ABTEILUNG_MAX_UEBERSCHRITTEN,
/** Vorgeschriebene Abteilungs-Struktur nicht vorhanden */
WARN_STRUKTURELLE_TEILUNG_FEHLT,
/** Abteilungs-Struktur vorhanden, aber Teilnehmerkreis falsch/unvollständig */
WARN_STRUKTURELLE_TEILUNG_UNVOLLSTAENDIG,
/** Mehrere Bewerbe zur gleichen Zeit am gleichen Platz */
WARN_ZEITPLAN_PLATZ_KONFLIKT,
/** Richter hat zeitgleiche Einsätze in verschiedenen Bewerben */
WARN_ZEITPLAN_RICHTER_KONFLIKT
}
/**
* Event, das gespeichert wird, wenn ein TBA eine Warnung überschreibt.
*/
@Serializable
data class AbteilungsWarnungOverrideEvent(
@Serializable(with = UuidSerializer::class)
val overrideId: Uuid = Uuid.random(),
val warnungCode: AbteilungsWarnungCodeE,
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid? = null,
val begruendung: String,
@Serializable(with = UuidSerializer::class)
val tbaUserId: Uuid,
@Serializable(with = InstantSerializer::class)
val timestamp: Instant = Clock.System.now()
)
@@ -0,0 +1,33 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.model.BesichtigungsTypE
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
/**
* Repräsentiert einen Zeitblock für die Parcoursbesichtigung.
* Kann mit mehreren Abteilungen oder Bewerben verknüpft sein.
*/
@Serializable
data class BesichtigungsBlock(
@Serializable(with = UuidSerializer::class)
val besichtigungsBlockId: Uuid = Uuid.random(),
/** Typ der Besichtigung (zu Fuß / zu Pferd). */
val typ: BesichtigungsTypE = BesichtigungsTypE.ZU_FUSS,
/** Geplante Dauer in Minuten. */
val dauerMinuten: Int = 15,
/**
* Liste der verknüpften Abteilungs-IDs.
* Eine Besichtigung kann für mehrere Abteilungen gleichzeitig stattfinden.
*/
val abteilungIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
/** Optionaler Puffer nach der Besichtigung bis zum ersten Start (Standard: 5 Min gemäß ÖTO). */
val pufferMinuten: Int = 5
)
@@ -0,0 +1,318 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
import at.mocode.core.domain.model.BeginnZeitTypE
import at.mocode.core.domain.model.PruefungsTypE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.domain.model.TurnierkategorieE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für einen Bewerb im registration-context.
*
* Ein Bewerb ist eine einzelne Prüfung innerhalb eines Turniers (z.B. Stilspringen 90 cm").
* Er kann in mehrere [Abteilung]en aufgeteilt sein. Die Abteilungs-Warn-Logik basiert
* auf den ÖTO-Schwellenwerten (§ 39 A-Teil + spartenspezifische Bestimmungen).
*
* Aggregate Root des `registration-context` für den Bewerbs-Workflow.
*
* @property bewerbId Eindeutige interne ID (UUID).
* @property turnierId Referenz auf das übergeordnete Turnier (UUID).
* @property bewerbNummer Laufende Nummer des Bewerbs innerhalb des Turniers (z.B. 1, 2, 3).
* @property bezeichnung Offizielle Bezeichnung des Bewerbs (z.B. Stilspringen 90 cm").
* @property sparte Sportliche Sparte (Springen, Dressur, Vielseitigkeit, ...).
* @property turnierkategorie Turnierkategorie (A*, A, B*, B, C, ...).
* @property pruefungsTyp Typ der Prüfung bestimmt den Abteilungs-Schwellenwert (§ 39).
* @property hoeheCm Höhe in cm (relevant für Springen und Vielseitigkeit).
* @property teilungsTyp Kriterium, nach dem der Bewerb in Abteilungen aufgeteilt wird.
* @property maxStarterProAbteilung Maximale Starter pro Abteilung (0 = kein Limit gesetzt).
* @property istMeisterschaft Ob es sich um einen Meisterschaftsbewerb handelt (Ausnahme von § 39 Abs. 4).
* @property istNachnennungErlaubt Ob Nachnennungen für diesen Bewerb zugelassen sind.
* @property beschreibung Optionale Beschreibung (z.B. "Pony Einsteiger Cup").
* @property aufgabe Aufgaben-Bezeichnung gemäß ÖTO (z.B. "R1", "L1").
* @property aufgabenNummer Aufgaben-Nummer (z.B. "R1/2024").
* @property paraGrade Para-Equestrian Grade (z.B. "Grade I"), falls zutreffend.
* @property austragungsplatzId Referenz auf den Austragungsplatz (UUID aus events-context).
* @property richterEinsaetze Liste der Richter-Einsätze für diesen Bewerb.
* @property geplantesDatum Geplantes Datum des Bewerbs.
* @property beginnZeitTyp Typ des Beginnzeit-Eintrags (FIX oder ANSCHLIESSEND).
* @property beginnZeit Geplante Beginnzeit (nur bei FIX).
* @property reitdauerMinuten Geplante Reitdauer in Minuten.
* @property umbauMinuten Geplante Umbauzeit in Minuten.
* @property besichtigungMinuten Geplante Besichtigungszeit in Minuten.
* @property stechenGeplant Ob ein Stechen geplant ist.
* @property startgeldCent Startgeld in Cent (z.B. 1500 = 15,00 ).
* @property geldpreisAusbezahlt Ob der Geldpreis bereits ausbezahlt wurde.
* @property bemerkungen Interne Notizen.
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class Bewerb(
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid = Uuid.random(),
// Zuordnung
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
// Identifikation
var bewerbNummer: Int,
var bezeichnung: String,
// Fachliche Klassifikation
var sparte: SparteE,
var turnierkategorie: TurnierkategorieE,
var pruefungsTyp: PruefungsTypE,
var hoeheCm: Int? = null,
// Abteilungs-Konfiguration
var teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
var maxStarterProAbteilung: Int = 0,
// Text & Details
var beschreibung: String? = null,
var aufgabe: String? = null,
var aufgabenNummer: String? = null,
var paraGrade: String? = null,
// Ort & Funktionäre
@Serializable(with = UuidSerializer::class)
var austragungsplatzId: Uuid? = null,
var richterEinsaetze: List<RichterEinsatz> = emptyList(),
// Zeitplan
var geplantesDatum: LocalDate? = null,
var beginnZeitTyp: BeginnZeitTypE? = null,
var beginnZeit: LocalTime? = null,
var reitdauerMinuten: Int? = null,
var umbauMinuten: Int? = null,
var besichtigungMinuten: Int? = null,
/** Konfiguration für Pausen während der Prüfung. */
var pausenKonfiguration: PausenKonfiguration? = null,
var stechenGeplant: Boolean = false,
// Finanzen
var startgeldCent: Long? = null,
var geldpreisAusbezahlt: Boolean = false,
// Flags
var istMeisterschaft: Boolean = false,
var istNachnennungErlaubt: Boolean = true,
// Verwaltung
var bemerkungen: String? = null,
// Audit
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt den Anzeigenamen mit Nummer zurück (z.B. 1 Stilspringen 90 cm").
*/
fun getDisplayName(): String = "$bewerbNummer $bezeichnung"
/**
* Liefert den Pflicht-Teilungs-Schwellenwert gemäß ÖTO § 39 für diesen Prüfungstyp.
* Gibt null zurück, wenn keine Pflicht-Teilung aufgrund der Starterzahl gilt
* (strukturelle Teilungen werden separat über [pruefungsTyp] und [teilungsTyp] abgebildet).
*
* Meisterschaftsbewerbe sind von der Pflicht-Teilung ausgenommen (§ 39 Abs. 4).
*/
fun getPflichtTeilungsSchwellenwert(): Int? {
if (istMeisterschaft) return null
return when (pruefungsTyp) {
PruefungsTypE.STIL_SPRINGEN,
PruefungsTypE.SPRINGPFERDE,
PruefungsTypE.DRESSURPFERDE -> 30
PruefungsTypE.VIELSEITIGKEIT -> 40
PruefungsTypE.SPRINGEN_UEBRIG -> 80
else -> null // Kann-Teilung oder strukturell kein Starter-Schwellenwert
}
}
/**
* Gibt den Kann-Teilungs-Schwellenwert zurück (nur für Dressur: > 30 Starter, § 39 Abs. 2).
* Gibt null zurück, wenn keine Kann-Teilung gilt.
*/
fun getKannTeilungsSchwellenwert(): Int? {
if (istMeisterschaft) return null
return when (pruefungsTyp) {
PruefungsTypE.DRESSUR -> 30
else -> null
}
}
/**
* Validiert den Bewerb auf Abteilungs-Schwellenwerte anhand der aktuellen Starterzahl.
* Gibt Warnungen zurück (kein harter Fehler Override-Event möglich, ADR-0016).
*
* @param aktuelleStarterAnzahl Aktuelle Anzahl der Nennungen für diesen Bewerb.
*/
fun validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl: Int): List<AbteilungsWarnung> {
val warnings = mutableListOf<AbteilungsWarnung>()
val pflichtSchwellenwert = getPflichtTeilungsSchwellenwert()
if (pflichtSchwellenwert != null && aktuelleStarterAnzahl > pflichtSchwellenwert) {
warnings.add(
AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN,
bewerbId = bewerbId,
nachricht = "WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN: Bewerb ${getDisplayName()}, Prüfungstyp $pruefungsTyp, Starter: $aktuelleStarterAnzahl > Schwellenwert $pflichtSchwellenwert. Empfehlung: Teilung nach ${teilungsTyp.name}.",
oetoParagraph = "§ 39 Abs. 2"
)
)
}
val kannSchwellenwert = getKannTeilungsSchwellenwert()
if (kannSchwellenwert != null && aktuelleStarterAnzahl > kannSchwellenwert &&
teilungsTyp == AbteilungsTeilungsTypE.KEINE
) {
warnings.add(
AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_KANN_TEILUNG_EMPFOHLEN,
bewerbId = bewerbId,
nachricht = "WARN_KANN_TEILUNG_EMPFOHLEN: Bewerb ${getDisplayName()}, Prüfungstyp $pruefungsTyp, Starter: $aktuelleStarterAnzahl > $kannSchwellenwert. Kann-Teilung empfohlen.",
oetoParagraph = "§ 39 Abs. 2"
)
)
}
return warnings
}
/**
* Validiert die strukturelle Teilung des Bewerbs gemäß ÖTO.
* Prüft, ob die vorgeschriebenen Abteilungen (z.B. nach Lizenz oder Pferdealter) vorhanden sind.
*/
fun validateStrukturellesTeilung(abteilungen: List<Abteilung>): List<AbteilungsWarnung> {
val warnings = mutableListOf<AbteilungsWarnung>()
// 1. CSN Stilspringen bis 95 cm (§ 200 Abs. 5.3)
if (sparte == SparteE.SPRINGEN && pruefungsTyp == PruefungsTypE.STIL_SPRINGEN && (hoeheCm ?: 0) <= 95) {
val hatOhneLizenz = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("ohne Lizenz", ignoreCase = true) == true }
val hatR1 = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("R1", ignoreCase = true) == true }
if (!hatOhneLizenz || !hatR1) {
val fehlend = mutableListOf<String>()
if (!hatOhneLizenz) fehlend.add("ohne Lizenz")
if (!hatR1) fehlend.add("R1")
warnings.add(
AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_STRUKTURELLE_TEILUNG_FEHLT,
bewerbId = bewerbId,
nachricht = "WARN_STRUKTURELLE_TEILUNG_FEHLT: Stilspringen bis 95 cm erfordert getrennte Abteilungen für: ${fehlend.joinToString(", ")}.",
oetoParagraph = "ÖTO B-Teil § 200 Abs. 5.3"
)
)
}
}
// 2. Springpferdeprüfung 95-110 cm / Dressurpferdeprüfung Kl. A (§ 200 Abs. 6 / § 100 Abs. 5)
val isSpringpferdeA = sparte == SparteE.SPRINGEN && pruefungsTyp == PruefungsTypE.SPRINGPFERDE && (hoeheCm ?: 0) in 95..110
val isDressurpferdeA = sparte == SparteE.DRESSUR && pruefungsTyp == PruefungsTypE.DRESSURPFERDE && aufgabe?.contains("A", ignoreCase = true) == true
if (isSpringpferdeA || isDressurpferdeA) {
val hat4Jaehrig = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("4-jährig", ignoreCase = true) == true }
val hat56Jaehrig = abteilungen.any {
it.teilnehmerkreisBeschreibung?.contains("5-jährig", ignoreCase = true) == true ||
it.teilnehmerkreisBeschreibung?.contains("6-jährig", ignoreCase = true) == true ||
it.teilnehmerkreisBeschreibung?.contains("5-6-jährig", ignoreCase = true) == true
}
if (!hat4Jaehrig || !hat56Jaehrig) {
val fehlend = mutableListOf<String>()
if (!hat4Jaehrig) fehlend.add("4-jährige")
if (!hat56Jaehrig) fehlend.add("5-6-jährige")
warnings.add(
AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_STRUKTURELLE_TEILUNG_FEHLT,
bewerbId = bewerbId,
nachricht = "WARN_STRUKTURELLE_TEILUNG_FEHLT: Pferdeprüfung Kl. A erfordert Trennung nach Alter: ${fehlend.joinToString(", ")}.",
oetoParagraph = if (isSpringpferdeA) "ÖTO B-Teil § 200 Abs. 6" else "ÖTO B-Teil § 100 Abs. 5"
)
)
}
}
// 3. CSN-C-NEU (§ 231)
if (turnierkategorie == TurnierkategorieE.C_NEU && sparte == SparteE.SPRINGEN) {
if ((hoeheCm ?: 0) <= 95) {
val hatOhneLizenz = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("ohne Lizenz", ignoreCase = true) == true }
val hatMitLizenz = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("mit Lizenz", ignoreCase = true) == true }
if (!hatOhneLizenz || !hatMitLizenz) {
warnings.add(AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_STRUKTURELLE_TEILUNG_FEHLT,
bewerbId = bewerbId,
nachricht = "WARN_STRUKTURELLE_TEILUNG_FEHLT: CSN-C-NEU bis 95 cm erfordert Abt. ohne Lizenz und Abt. mit Lizenz.",
oetoParagraph = "ÖTO B-Teil § 231"
))
}
} else if ((hoeheCm ?: 0) >= 100) {
val hatR1 = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("R1", ignoreCase = true) == true }
val hatR2Plus = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("R2", ignoreCase = true) == true }
if (!hatR1 || !hatR2Plus) {
warnings.add(AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_STRUKTURELLE_TEILUNG_FEHLT,
bewerbId = bewerbId,
nachricht = "WARN_STRUKTURELLE_TEILUNG_FEHLT: CSN-C-NEU ab 100 cm erfordert Abt. R1 und Abt. R2+.",
oetoParagraph = "ÖTO B-Teil § 231"
))
}
}
}
// 4. Caprilli (§ 803 Abs. 2)
if (pruefungsTyp == PruefungsTypE.CAPRILLI) {
val hatLizenzfrei = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("lizenzfrei", ignoreCase = true) == true }
val hatRD1Plus = abteilungen.any { it.teilnehmerkreisBeschreibung?.contains("RD1", ignoreCase = true) == true || it.teilnehmerkreisBeschreibung?.contains("R1", ignoreCase = true) == true }
if (!hatLizenzfrei || !hatRD1Plus) {
warnings.add(AbteilungsWarnung(
code = AbteilungsWarnungCodeE.WARN_STRUKTURELLE_TEILUNG_FEHLT,
bewerbId = bewerbId,
nachricht = "WARN_STRUKTURELLE_TEILUNG_FEHLT: Caprilli-Prüfung erfordert Abt. lizenzfrei und Abt. RD1+.",
oetoParagraph = "ÖTO B-Teil § 803 Abs. 2"
))
}
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): Bewerb = this.copy(updatedAt = Clock.System.now())
}
/**
* Konfiguration für automatische Pausen nach einer bestimmten Anzahl von Startern.
*/
@Serializable
data class PausenKonfiguration(
/** Pause alle X Starter (0 = keine automatischen Pausen). */
val starterIntervall: Int = 0,
/** Dauer der Pause in Minuten. */
val dauerMinuten: Int = 10,
/** Optionale Bezeichnung (z.B. "Platzpflege"). */
val bezeichnung: String? = null
)
@@ -13,7 +13,7 @@ import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Startliste im registration-context.
*
* Eine Startliste gehört zu einer [DomAbteilung] und enthält die geordnete Liste
* Eine Startliste gehört zu einer [Abteilung] und enthält die geordnete Liste
* der Starter (Nennungen) mit ihren Startnummern. Sie durchläuft einen definierten
* Workflow: NICHT_ERSTELLT ENTWURF VEROEFFENTLICHT GESPERRT ARCHIVIERT.
*
@@ -128,7 +128,7 @@ data class DomStartliste(
/**
* Ein einzelner Eintrag in einer Startliste.
*
* Verbindet eine Startnummer mit einer Nennung ([DomNennung]).
* Verbindet eine Startnummer mit einer Nennung ([Nennung]).
*
* @property startnummer Die zugewiesene Startnummer (Kopfnummer gemäß Ubiquitous Language).
* @property nennungId Referenz auf die zugehörige Nennung (UUID).

Some files were not shown because too many files have changed in this diff Show More