269 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
741 changed files with 38704 additions and 17243 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)
+13 -2
View File
@@ -1,13 +1,24 @@
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
@@ -38,12 +49,12 @@ jobs:
- name: Show Gradle version
run: ./gradlew --version
- name: Run Desktop tests headless (Xvfb)
- 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
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
+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
+19 -13
View File
@@ -22,6 +22,8 @@ jobs:
# =============================================================
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 }}
@@ -62,7 +64,7 @@ jobs:
fi
- name: Git-Tag erstellen & pushen
if: steps.check-tag.outputs.already_tagged == 'false' && github.event.inputs.dry_run != 'true'
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 }}"
@@ -77,6 +79,8 @@ jobs:
# =============================================================
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
@@ -84,11 +88,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 21 (Temurin)
- name: Setup JDK 25 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '25'
- name: Gradle cache
uses: actions/cache@v4
@@ -123,6 +127,8 @@ jobs:
# =============================================================
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
@@ -130,11 +136,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 21 (Temurin)
- name: Setup JDK 25 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
java-version: '25'
- name: Gradle cache
uses: actions/cache@v4
@@ -173,11 +179,11 @@ jobs:
steps:
- name: Summary ausgeben
run: |
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Artefakt | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
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.
+141 -1
View File
@@ -13,7 +13,124 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
---
## [Unreleased]
### [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):
@@ -73,6 +190,12 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
- **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).
@@ -105,6 +228,23 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
---
## [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
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()
+34 -89
View File
@@ -1,15 +1,14 @@
# ===================================================================
# 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 ===
# HINWEIS: gradle:X.Y-jdkZ-alpine Images existieren nicht für alle Gradle/JDK-Kombinationen.
# Wir verwenden eclipse-temurin als Builder-Basis und das Projekt-eigene ./gradlew-Wrapper.
ARG JAVA_VERSION=21
ARG GRADLE_VERSION=9.4.1
ARG JAVA_VERSION=25
ARG BUILD_DATE
ARG VERSION
ARG VERSION=1.0.0-SNAPSHOT
# ===================================================================
# Build Stage
@@ -19,9 +18,9 @@ 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
@@ -35,62 +34,21 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
-XX:MaxMetaspaceSize=512m"
ENV GRADLE_USER_HOME=/root/.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/shared \
frontend/shells/meldestelle-portal \
frontend/shells/meldestelle-desktop \
frontend/features/ping-feature \
frontend/features/nennung-feature \
frontend/features/zns-import-feature \
frontend/features/billing-feature \
frontend/features/pferde-feature \
frontend/features/verein-feature \
frontend/features/veranstaltung-feature \
frontend/features/veranstalter-feature \
frontend/features/profile-feature \
frontend/features/reiter-feature \
frontend/features/turnier-feature \
docs
# Copy root build configuration
COPY build.gradle.kts ./
# Download and cache dependencies
RUN --mount=type=cache,id=gradle-cache-gateway,target=/root/.gradle/caches \
--mount=type=cache,id=gradle-wrapper-gateway,target=/root/.gradle/wrapper \
./gradlew :backend:infrastructure:gateway:dependencies --info
# Build the application
RUN --mount=type=cache,id=gradle-cache-gateway,target=/root/.gradle/caches \
--mount=type=cache,id=gradle-wrapper-gateway,target=/root/.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
@@ -101,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
@@ -121,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}
@@ -144,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 \
@@ -158,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,10 +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,
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String
) {
class GatewayConfig {
@Bean
fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
@@ -26,11 +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(znsImportServiceUrl)
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,6 @@ class SecurityConfig(
.authorizeExchange { exchanges ->
exchanges
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll()
.pathMatchers("/api/ping/**").permitAll() // TEMPORAER fuer Debugging
.pathMatchers("/api/v1/import/zns", "/api/v1/import/zns/**").permitAll() // TEMPORAER fuer Debugging
.anyExchange().authenticated()
}
@@ -67,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}"))
}
}
}
@@ -107,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
}
@@ -19,10 +19,10 @@ import java.util.zip.ZipInputStream
* Domänenobjekte über die jeweiligen Repositories (Upsert-Logik).
*
* Die Verarbeitungsreihenfolge ist fix:
* 1. VEREIN01.DAT Verein (via VereinRepository)
* 2. LIZENZ01.DAT Reiter (via ReiterRepository)
* 3. PFERDE01.DAT Pferd (via HorseRepository)
* 4. RICHT01.DAT Funktionaer (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)
@@ -47,14 +47,15 @@ class ZnsImportService(
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>>()
@@ -64,47 +65,168 @@ class ZnsImportService(
while (entry != null) {
val fileName = entry.name.uppercase().substringAfterLast("/")
if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
val outputStream = java.io.ByteArrayOutputStream()
val buffer = ByteArray(4096)
var len: Int
while (zip.read(buffer).also { len = it } > 0) {
outputStream.write(buffer, 0, len)
// 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()
}
val content = outputStream.toString(CP850)
val lines = content.split(Regex("\\r?\\n|\\r")).filter { it.isNotBlank() }
dateien[fileName] = lines
println("[DEBUG_LOG] Datei $fileName extrahiert als $targetKey: ${lines.size} Zeilen")
dateien[targetKey] = lines
}
zip.closeEntry()
entry = zip.nextEntry
}
} finally {
// Wir schließen den ZipInputStream NICHT mit use,
// um den zugrunde liegenden zipInputStream nicht vorzeitig zu schließen.
// Falls der Aufrufer den Stream schließen will, soll er das tun.
// Aber wir müssen sicherstellen, dass wir alle Entries gelesen haben.
} 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 {
suspend fun importiereZip(
zipInputStream: InputStream,
mode: ZnsImportMode = ZnsImportMode.FULL
): ZnsImportResult {
val dateien = extrahiereDateien(zipInputStream)
// println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}")
// dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
println("[DEBUG_LOG] Gefundene Dateien im ZIP: ${dateien.keys}")
dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
val fehler = mutableListOf<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) = importiereFunktionaere(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,
@@ -132,7 +254,11 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val verein = ZnsVereinParser.parse(zeile) ?: return@forEachIndexed
val verein = ZnsVereinParser.parse(zeile)
if (verein == null) {
if (index < 5) println("[DEBUG_LOG] Parser lieferte null für Zeile ${index + 1}: '$zeile'")
return@forEachIndexed
}
val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer)
if (vorhanden == null) {
vereinRepository.save(verein)
@@ -167,7 +293,11 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val parsed = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed
val parsed = ZnsReiterParser.parse(zeile)
if (parsed == null) {
if (index < 5) println("[DEBUG_LOG] Reiter-Parser lieferte null für Zeile ${index + 1}: '$zeile'")
return@forEachIndexed
}
// Relationen auflösen
val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) }
@@ -306,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)
}
}
}
@@ -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
)
/**
@@ -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
@@ -55,6 +56,9 @@ data class Abteilung(
// 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 Abteilung(
* 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"
)
)
}
@@ -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
)
@@ -97,6 +97,10 @@ data class Bewerb(
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
@@ -159,15 +163,18 @@ data class Bewerb(
*
* @param aktuelleStarterAnzahl Aktuelle Anzahl der Nennungen für diesen Bewerb.
*/
fun validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl: Int): List<String> {
val warnings = mutableListOf<String>()
fun validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl: Int): List<AbteilungsWarnung> {
val warnings = mutableListOf<AbteilungsWarnung>()
val pflichtSchwellenwert = getPflichtTeilungsSchwellenwert()
if (pflichtSchwellenwert != null && aktuelleStarterAnzahl > pflichtSchwellenwert) {
warnings.add(
"WARN_ABTEILUNG_PFLICHT_TEILUNG_UEBERSCHRITTEN: Bewerb ${getDisplayName()}, " +
"Prüfungstyp $pruefungsTyp, Starter: $aktuelleStarterAnzahl > Schwellenwert $pflichtSchwellenwert. " +
"Empfehlung: Teilung nach ${teilungsTyp.name}. Override möglich (TBA-Entscheidung)."
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"
)
)
}
@@ -176,17 +183,136 @@ data class Bewerb(
teilungsTyp == AbteilungsTeilungsTypE.KEINE
) {
warnings.add(
"WARN_ABTEILUNG_KANN_TEILUNG_EMPFOHLEN: Bewerb ${getDisplayName()}, " +
"Prüfungstyp $pruefungsTyp, Starter: $aktuelleStarterAnzahl > $kannSchwellenwert. " +
"Kann-Teilung empfohlen (§ 39 Abs. 2)."
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
)

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