Compare commits
161 Commits
88983f2b4e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0817d49dfc | |||
| 44cf2b3edc | |||
| 4acbd6b0b2 | |||
| 843bd145a8 | |||
| 98425b8fa8 | |||
| 5b6459a041 | |||
| d493734660 | |||
| 0aaa160b95 | |||
| 03184aa951 | |||
| 34bd42a009 | |||
| 897394e27e | |||
| 9ab914dbfb | |||
| 9659fe3f8a | |||
| 5cbf4fdfc0 | |||
| bd06efe05d | |||
| 23c3e40390 | |||
| 1201755077 | |||
| 162e2ef414 | |||
| 3f291c907c | |||
| 251647a6ab | |||
| 277254ebbd | |||
| f97bfeff47 | |||
| 02a778751a | |||
| af0ece8ded | |||
| 03fa74abba | |||
| 71aea3f41d | |||
| 16c8674eff | |||
| df5276abf2 | |||
| 636ecc9883 | |||
| 92950dbbe6 | |||
| 5c51664e6c | |||
| 3244efd5e0 | |||
| af02e14f2d | |||
| 8730ffa7db | |||
| f7d11ccf97 | |||
| 76e6cebd90 | |||
| dbbca96c69 | |||
| eea022b862 | |||
| 6de5b55810 | |||
| 07bd114df1 | |||
| 84d38f5eb5 | |||
| 9db85236ec | |||
| f2a6078421 | |||
| 568d9dbb32 | |||
| f620f46d15 | |||
| 46d3d7cf35 | |||
| cb22b1bb96 | |||
| 5544b04b07 | |||
| 49d8b205d7 | |||
| f296a076dc | |||
| 1caefe6603 | |||
| 6b690232ff | |||
| 309834d90c | |||
| 8b44edda90 | |||
| 255343145d | |||
| 5baa971b46 | |||
| e65384768f | |||
| beb20e0cf7 | |||
| 98c241fc64 | |||
| d4cc0eb77d | |||
| e0b1ce8836 | |||
| f18b002f4e | |||
| f8913f81b8 | |||
| 9195cdb14d | |||
| 3f4ba9eea9 | |||
| 92028d9e02 | |||
| bdb45eefe4 | |||
| 148b71db48 | |||
| c54ad3830d | |||
| d66bd63cc9 | |||
| 3b4e3db51d | |||
| 2d7046d0e3 | |||
| d9b5c6bfea | |||
| 91a8c38b25 | |||
| 19ba044ec0 | |||
| 9556e0ac67 | |||
| 4692bd186c | |||
| b11432df16 | |||
| 319cb52160 | |||
| a35dfa1434 | |||
| 237c71e5a0 | |||
| ec124e9acd | |||
| 0ab1807235 | |||
| 7cfdd06d1e | |||
| 544fbf792c | |||
| 18e619abfc | |||
| 5eeff24b3a | |||
| f13c2eb35b | |||
| 2662d4e82e | |||
| 574f8c470c | |||
| 9b4af2bb56 | |||
| 1a295c18c8 | |||
| 01bf440f21 | |||
| 7acd9ea4c2 | |||
| 30b53584f8 | |||
| c1327f3186 | |||
| 7a2c5700f9 | |||
| 5b8ef5ea2d | |||
| db58c24613 | |||
| edfe05cbe3 | |||
| 6feb139a46 | |||
| b94e0f2d9d | |||
| 8806d11e3c | |||
| a1bf93342e | |||
| 5887ac7b6c | |||
| 8aef46bba1 | |||
| 2489beab59 | |||
| f8820847fa | |||
| 345c329350 | |||
| d4aeba4666 | |||
| 9fe889b2c1 | |||
| 85ac1cae9c | |||
| dfaa2e8545 | |||
| bcabb86841 | |||
| 189ebc6565 | |||
| 83adb4ae07 | |||
| 54f91c7309 | |||
| a645bb4dbc | |||
| 691861a706 | |||
| ef5d4fdc81 | |||
| afad3c5a02 | |||
| 512eb730b0 | |||
| 8c1abaebad | |||
| 3428261bff | |||
| 34cab61567 | |||
| 4419e55ee1 | |||
| bd8899a829 | |||
| 8148ceb318 | |||
| 58454ec9af | |||
| 2e7078424d | |||
| 56ecee4cba | |||
| 9222ae7a1c | |||
| 9578b92e7a | |||
| f02e172ff0 | |||
| cef579f91b | |||
| c8655bfc7f | |||
| 28a7c5dc44 | |||
| b19f7cadb8 | |||
| cb6db36adb | |||
| 0e694341b8 | |||
| 2ab1840237 | |||
| 96bdc92723 | |||
| cee0a8437f | |||
| 2b05eebad9 | |||
| 9037b6ce1c | |||
| ec861b8f81 | |||
| 767d78af27 | |||
| 8a3ef98c44 | |||
| dc66dfb537 | |||
| ae39eb4637 | |||
| 64d749be3a | |||
| 1b20e480f4 | |||
| c29c8179a1 | |||
| 2bd2a26ab9 | |||
| fb520c6607 | |||
| bad6f44122 | |||
| 280debce09 | |||
| fb77a5065b | |||
| e91b10daa3 | |||
| 7bbb991e69 | |||
| 315517f03f |
Vendored
+41
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
Executable
+42
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
@@ -193,7 +193,7 @@ secrets/
|
|||||||
# ===================================================================
|
# ===================================================================
|
||||||
TODO*.md
|
TODO*.md
|
||||||
NOTES*.md
|
NOTES*.md
|
||||||
**/.junie/
|
.junie/
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Keep essential files (override exclusions)
|
# Keep essential files (override exclusions)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
name: Desktop CI — Headless Tests & Build
|
name: Desktop CI — Headless Tests & Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
# Nur ausführen, wenn explizit das Desktop-Shell-Modul geändert wurde
|
||||||
push:
|
push:
|
||||||
branches: [ main, master ]
|
branches: [ main, master ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/shells/meldestelle-desktop/**'
|
||||||
|
- '.gitea/workflows/desktop-tests.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, master ]
|
branches: [ main, master ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/shells/meldestelle-desktop/**'
|
||||||
|
# Manuell startbar, falls benötigt
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
desktop-tests:
|
desktop-tests:
|
||||||
|
# Komplett deaktivierbar über Repo-Variable: Settings → Variables → DESKTOP_CI_ENABLED=true
|
||||||
|
# Zusätzlich: Für Plan‑B‑Builds ü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
|
name: Compose Desktop — Tests (headless) & Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -38,12 +49,12 @@ jobs:
|
|||||||
- name: Show Gradle version
|
- name: Show Gradle version
|
||||||
run: ./gradlew --version
|
run: ./gradlew --version
|
||||||
|
|
||||||
- name: Run Desktop tests headless (Xvfb)
|
- name: Run Desktop tests headless (xvfb)
|
||||||
env:
|
env:
|
||||||
_JAVA_OPTIONS: -Djava.awt.headless=true
|
_JAVA_OPTIONS: -Djava.awt.headless=true
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -y
|
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" \
|
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
|
||||||
./gradlew :frontend:shells:meldestelle-desktop:jvmTest --stacktrace --no-daemon
|
./gradlew :frontend:shells:meldestelle-desktop:jvmTest --stacktrace --no-daemon
|
||||||
|
|
||||||
|
|||||||
@@ -33,18 +33,11 @@ jobs:
|
|||||||
max-parallel: 1
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service: keycloak
|
# Plan-B fokussiert: Nur Mail-Service + Web-App bauen/pushen (beschleunigt CI deutlich)
|
||||||
|
- service: mail-service
|
||||||
context: .
|
context: .
|
||||||
dockerfile: config/docker/keycloak/Dockerfile
|
dockerfile: backend/services/mail/Dockerfile
|
||||||
image: keycloak
|
image: mail-service
|
||||||
- service: api-gateway
|
|
||||||
context: .
|
|
||||||
dockerfile: backend/infrastructure/gateway/Dockerfile
|
|
||||||
image: api-gateway
|
|
||||||
- service: ping-service
|
|
||||||
context: .
|
|
||||||
dockerfile: backend/services/ping/Dockerfile
|
|
||||||
image: ping-service
|
|
||||||
- service: web-app
|
- service: web-app
|
||||||
context: .
|
context: .
|
||||||
dockerfile: config/docker/caddy/web-app/Dockerfile
|
dockerfile: config/docker/caddy/web-app/Dockerfile
|
||||||
@@ -61,43 +54,42 @@ jobs:
|
|||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Setup Gradle Cache
|
# --- SCHRITT 1: Build mit radikalem Clean (gegen die März-Leichen) ---
|
||||||
uses: actions/cache@v4
|
- name: Build Frontend (Wasm JS)
|
||||||
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)
|
|
||||||
if: matrix.service == 'web-app'
|
if: matrix.service == 'web-app'
|
||||||
run: |
|
run: |
|
||||||
chmod +x gradlew
|
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 \
|
-Pproduction=true \
|
||||||
--max-workers=4 \
|
--max-workers=4 \
|
||||||
-Dkotlin.daemon.jvm.options="-Xmx4g"
|
-Dkotlin.daemon.jvm.options="-Xmx4g"
|
||||||
|
|
||||||
# Pangolin-Bypass: Credentials direkt in config.json schreiben.
|
# --- SCHRITT 2: Staging ohne rsync (Fix für dein Log-Fehler) ---
|
||||||
# Kein "docker login" → kein Daemon-Ping → kein HTTPS-Fehler.
|
- name: Stage Web Assets for Docker build
|
||||||
# BuildKit liest ~/.docker/config.json und verwendet diese Credentials beim Push.
|
if: matrix.service == 'web-app'
|
||||||
# - name: Registry-Credentials konfigurieren (kein Daemon-Kontakt)
|
run: |
|
||||||
# run: |
|
set -e
|
||||||
# mkdir -p ~/.docker
|
DIST_DIR="frontend/shells/meldestelle-web/build/dist/wasmJs/productionExecutable"
|
||||||
# AUTH=$(echo -n "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" | base64 -w 0)
|
TARGET_DIR="config/docker/caddy/web-app/_site"
|
||||||
# printf '{"auths":{"%s":{"auth":"%s"}}}\n' "${{ env.REGISTRY_INTERNAL }}" "${AUTH}" > ~/.docker/config.json
|
|
||||||
# echo "✓ Credentials für ${{ env.REGISTRY_INTERNAL }} gespeichert"
|
|
||||||
|
|
||||||
|
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):
|
# NEU (sauber, nach daemon.json-Fix):
|
||||||
- name: Login to Gitea Registry
|
- name: Login to Gitea Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -122,7 +114,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
|
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=latest
|
||||||
type=sha,format=long
|
type=sha,format=long
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
@@ -137,9 +129,5 @@ jobs:
|
|||||||
provenance: false
|
provenance: false
|
||||||
sbom: false
|
sbom: false
|
||||||
build-args: |
|
build-args: |
|
||||||
DOCKER_BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
BUILD_DATE=${{ github.event.head_commit.timestamp || 'unknown' }}
|
||||||
VERSION=${{ github.sha }}
|
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 }}
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ on:
|
|||||||
branches: [ "**" ]
|
branches: [ "**" ]
|
||||||
jobs:
|
jobs:
|
||||||
no-hardcoded-versions:
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ jobs:
|
|||||||
# =============================================================
|
# =============================================================
|
||||||
tag-release:
|
tag-release:
|
||||||
name: 🏷️ Git-Tag setzen
|
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
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.read-version.outputs.version }}
|
version: ${{ steps.read-version.outputs.version }}
|
||||||
@@ -62,7 +64,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Git-Tag erstellen & pushen
|
- 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: |
|
run: |
|
||||||
TAG="${{ steps.read-version.outputs.tag }}"
|
TAG="${{ steps.read-version.outputs.tag }}"
|
||||||
VERSION="${{ steps.read-version.outputs.version }}"
|
VERSION="${{ steps.read-version.outputs.version }}"
|
||||||
@@ -77,6 +79,8 @@ jobs:
|
|||||||
# =============================================================
|
# =============================================================
|
||||||
package-linux:
|
package-linux:
|
||||||
name: 📦 Linux .deb Packaging
|
name: 📦 Linux .deb Packaging
|
||||||
|
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit
|
||||||
|
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: tag-release
|
needs: tag-release
|
||||||
|
|
||||||
@@ -84,11 +88,11 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup JDK 21 (Temurin)
|
- name: Setup JDK 25 (Temurin)
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Gradle cache
|
- name: Gradle cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -123,6 +127,8 @@ jobs:
|
|||||||
# =============================================================
|
# =============================================================
|
||||||
package-windows:
|
package-windows:
|
||||||
name: 📦 Windows .msi Packaging
|
name: 📦 Windows .msi Packaging
|
||||||
|
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit
|
||||||
|
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: tag-release
|
needs: tag-release
|
||||||
|
|
||||||
@@ -130,11 +136,11 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup JDK 21 (Temurin)
|
- name: Setup JDK 25 (Temurin)
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: '21'
|
java-version: '25'
|
||||||
|
|
||||||
- name: Gradle cache
|
- name: Gradle cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -173,11 +179,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Summary ausgeben
|
- name: Summary ausgeben
|
||||||
run: |
|
run: |
|
||||||
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITEA_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITEA_STEP_SUMMARY
|
||||||
echo "| Artefakt | Status |" >> $GITHUB_STEP_SUMMARY
|
echo "| Artefakt | Status |" >> $GITEA_STEP_SUMMARY
|
||||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
echo "|----------|--------|" >> $GITEA_STEP_SUMMARY
|
||||||
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITHUB_STEP_SUMMARY
|
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITEA_STEP_SUMMARY
|
||||||
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITHUB_STEP_SUMMARY
|
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITEA_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITEA_STEP_SUMMARY
|
||||||
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITEA_STEP_SUMMARY
|
||||||
|
|||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
# 🐧 [DevOps Engineer] Optimierte .gitignore für Meldestelle (KMP / Gradle / Docker)
|
||||||
|
|
||||||
|
# --- AI ---
|
||||||
|
.ai/dist/
|
||||||
|
|
||||||
|
# --- IDE & Editor ---
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
.vscode/
|
||||||
|
.history/
|
||||||
|
.shelf/
|
||||||
|
|
||||||
|
# --- Gradle ---
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
!**/src/**/build/
|
||||||
|
gradle-app.setting
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
.gradletasknamecache
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# --- Kotlin / KMP ---
|
||||||
|
.kotlin/
|
||||||
|
kotlin-js-store/
|
||||||
|
.jetbrains/
|
||||||
|
|
||||||
|
# --- Android (falls relevant) ---
|
||||||
|
*.ap_
|
||||||
|
*.apk
|
||||||
|
*.dex
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# --- Node / JS (Compose Web / KMP JS) ---
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.npm/
|
||||||
|
|
||||||
|
# --- Docker & Infrastructure ---
|
||||||
|
.docker/
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
.env
|
||||||
|
!.env.example
|
||||||
|
.data/
|
||||||
|
postgres-data/
|
||||||
|
|
||||||
|
# --- OS Specific ---
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# --- Project Specific ---
|
||||||
|
docs/temp/
|
||||||
|
docs/Bin/
|
||||||
|
docs/_archive/
|
||||||
@@ -1,43 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# check-docs-drift.sh
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
# - Kein Guidelines-System mehr.
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
# - Single Source of Truth: `docs/`
|
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p build/diagrams
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
shopt -s nullglob
|
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||||
for f in docs/architecture/c4/*.puml; do
|
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||||
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
|
exec "$ROOT_DIR/.ai/scripts/render-plantuml.sh" "$@"
|
||||||
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
|
|
||||||
done
|
|
||||||
|
|||||||
@@ -1,136 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# validate-links.sh - Link-Validierung für Projektdokumentation (`docs/**`).
|
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||||
# Zweck: Guardrail für die "Docs-as-Code"-Strategie.
|
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")"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
|
||||||
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
|
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+7
@@ -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" "$@"
|
||||||
Executable
+7
@@ -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" "$@"
|
||||||
@@ -34,6 +34,8 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
### Behoben
|
### 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
|
- **Identity-Modul:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen und Behebung von
|
||||||
Persistenz-Konflikten im `ExposedDeviceRepository`.
|
Persistenz-Konflikten im `ExposedDeviceRepository`.
|
||||||
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
|
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
|
||||||
|
|||||||
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.
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 +0,0 @@
|
|||||||
Placeholder to ensure directory exists
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
-27
@@ -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
|
|
||||||
)
|
|
||||||
|
|
||||||
-8
@@ -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>
|
|
||||||
}
|
|
||||||
-178
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-75
@@ -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
|
|
||||||
}
|
|
||||||
-37
@@ -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()
|
|
||||||
+20
-10
@@ -11,9 +11,8 @@ import org.springframework.security.authentication.AbstractAuthenticationToken
|
|||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
import org.springframework.security.oauth2.jwt.Jwt
|
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
|
||||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
|
import org.springframework.security.oauth2.jwt.*
|
||||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
|
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter
|
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
@@ -38,7 +37,6 @@ class SecurityConfig(
|
|||||||
.authorizeExchange { exchanges ->
|
.authorizeExchange { exchanges ->
|
||||||
exchanges
|
exchanges
|
||||||
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll()
|
.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
|
.pathMatchers("/api/v1/import/zns", "/api/v1/import/zns/**").permitAll() // TEMPORAER fuer Debugging
|
||||||
.anyExchange().authenticated()
|
.anyExchange().authenticated()
|
||||||
}
|
}
|
||||||
@@ -67,16 +65,28 @@ class SecurityConfig(
|
|||||||
if (delegate == null) {
|
if (delegate == null) {
|
||||||
if (jwkSetUri.isBlank()) {
|
if (jwkSetUri.isBlank()) {
|
||||||
logger.error("JWK Set URI is missing – all authenticated requests will be rejected.")
|
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 {
|
try {
|
||||||
logger.info("Attempting to initialize JWT Decoder with URI: {}", jwkSetUri)
|
logger.info("Attempting to initialize JWT Decoder with URI: {}", jwkSetUri)
|
||||||
delegate = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
// Wir deaktivieren die Issuer-Validierung, da Keycloak intern "keycloak:8080"
|
||||||
logger.info("JWT Decoder successfully initialized.")
|
// 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) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Could not initialize JWT Decoder: {}", e.message)
|
logger.warn("Could not initialize JWT Decoder: {}", e.message)
|
||||||
// Throw BadJwtException so Spring Security returns 401, not 500 or passthrough
|
return Mono.error(BadJwtException("Identity Provider unavailable: ${e.message}"))
|
||||||
return Mono.error(org.springframework.security.oauth2.jwt.BadJwtException("Identity Provider unavailable: ${e.message}"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +117,7 @@ class SecurityConfig(
|
|||||||
val configuration = CorsConfiguration().apply {
|
val configuration = CorsConfiguration().apply {
|
||||||
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
|
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
|
||||||
allowedMethods = securityProperties.cors.allowedMethods.toList()
|
allowedMethods = securityProperties.cors.allowedMethods.toList()
|
||||||
allowedHeaders = securityProperties.cors.allowedHeaders.toList()
|
allowedHeaders = listOf("*") // Alles erlauben fuer Postman/Frontend
|
||||||
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
|
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
|
||||||
allowCredentials = securityProperties.cors.allowCredentials
|
allowCredentials = securityProperties.cors.allowCredentials
|
||||||
maxAge = securityProperties.cors.maxAge.seconds
|
maxAge = securityProperties.cors.maxAge.seconds
|
||||||
|
|||||||
@@ -44,6 +44,27 @@ spring:
|
|||||||
issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI:http://localhost:8180/realms/meldestelle}
|
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}
|
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:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
|
|||||||
+95
-1
@@ -8,7 +8,7 @@
|
|||||||
"variable": [
|
"variable": [
|
||||||
{
|
{
|
||||||
"key": "baseUrl",
|
"key": "baseUrl",
|
||||||
"value": "http://localhost:8080",
|
"value": "http://localhost:8081",
|
||||||
"type": "string"
|
"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",
|
"name": "Master Data Context",
|
||||||
"item": [
|
"item": [
|
||||||
|
|||||||
+45
-6
@@ -6,9 +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.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy
|
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.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@@ -19,19 +27,18 @@ class GlobalSecurityConfig {
|
|||||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http
|
http
|
||||||
.csrf { it.disable() } // CSRF nicht nötig für Stateless REST APIs
|
.csrf { it.disable() } // CSRF nicht nötig für Stateless REST APIs
|
||||||
// WICHTIG: CORS explizit deaktivieren!
|
// WICHTIG: CORS wieder aktivieren für Plan-B (Direktzugriff ohne Gateway möglich)
|
||||||
// Das API-Gateway kümmert sich um CORS. Die Microservices dürfen KEINE
|
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||||
// Access-Control-Allow-Origin Header setzen, sonst haben wir doppelte Header beim Client.
|
|
||||||
.cors { it.disable() }
|
|
||||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||||
.addFilterBefore(DeviceSecurityFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
.addFilterBefore(DeviceSecurityFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
||||||
.authorizeHttpRequests { auth ->
|
.authorizeHttpRequests { auth ->
|
||||||
// Explizite Freigaben (Health, Info, Public Endpoints)
|
// Explizite Freigaben (Health, Information, Public-Endpoints)
|
||||||
auth.requestMatchers("/actuator/**").permitAll()
|
auth.requestMatchers("/actuator/**").permitAll()
|
||||||
auth.requestMatchers("/api/v1/devices/register").permitAll() // Onboarding erlauben
|
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/public").permitAll()
|
||||||
auth.requestMatchers("/ping/simple").permitAll()
|
auth.requestMatchers("/ping/simple").permitAll()
|
||||||
auth.requestMatchers("/ping/enhanced").permitAll()
|
|
||||||
auth.requestMatchers("/ping/health").permitAll()
|
auth.requestMatchers("/ping/health").permitAll()
|
||||||
auth.requestMatchers("/error").permitAll()
|
auth.requestMatchers("/error").permitAll()
|
||||||
|
|
||||||
@@ -41,16 +48,48 @@ class GlobalSecurityConfig {
|
|||||||
.oauth2ResourceServer { oauth2 ->
|
.oauth2ResourceServer { oauth2 ->
|
||||||
oauth2.jwt { jwt ->
|
oauth2.jwt { jwt ->
|
||||||
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
||||||
|
// Auch hier den Issuer-Check entspannen, da der Service intern validiert
|
||||||
|
jwt.decoder(jwtDecoder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.build()
|
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
|
@Bean
|
||||||
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
|
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
|
||||||
val converter = JwtAuthenticationConverter()
|
val converter = JwtAuthenticationConverter()
|
||||||
converter.setJwtGrantedAuthoritiesConverter(KeycloakRoleConverter())
|
converter.setJwtGrantedAuthoritiesConverter(KeycloakRoleConverter())
|
||||||
return converter
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ plugins {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js(IR) {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: billing-service
|
name: billing-service
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
consul:
|
consul:
|
||||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||||
@@ -15,13 +18,19 @@ spring:
|
|||||||
prefer-ip-address: true
|
prefer-ip-address: true
|
||||||
health-check-path: /actuator/health
|
health-check-path: /actuator/health
|
||||||
health-check-interval: 10s
|
health-check-interval: 10s
|
||||||
health-check-port: 8089
|
# health-check-port: 8089
|
||||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||||
service-name: ${spring.application.name}
|
service-name: ${spring.application.name}
|
||||||
|
port: ${billing.http.port:8089}
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8089
|
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:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
@@ -30,3 +39,12 @@ management:
|
|||||||
endpoint:
|
endpoint:
|
||||||
health:
|
health:
|
||||||
show-details: always
|
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"
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ version = "1.0.0"
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
js(IR) {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ plugins {
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
js(IR) {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ dependencies {
|
|||||||
// Common service extras
|
// Common service extras
|
||||||
implementation(libs.spring.boot.starter.validation)
|
implementation(libs.spring.boot.starter.validation)
|
||||||
implementation(libs.spring.boot.starter.mail)
|
implementation(libs.spring.boot.starter.mail)
|
||||||
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on the classpath
|
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on the classpath
|
||||||
//implementation("org.springframework.boot:spring-boot-starter-web")
|
//implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ plugins {
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
js(IR) {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ dependencies {
|
|||||||
|
|
||||||
// Spring Boot Starters
|
// Spring Boot Starters
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
implementation(libs.spring.boot.starter.security)
|
||||||
|
implementation(libs.spring.boot.starter.oauth2.resource.server)
|
||||||
|
implementation(projects.backend.infrastructure.security)
|
||||||
implementation(libs.spring.boot.starter.validation)
|
implementation(libs.spring.boot.starter.validation)
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
implementation(libs.spring.boot.starter.mail)
|
implementation(libs.spring.boot.starter.mail)
|
||||||
|
|||||||
+3
-1
@@ -10,9 +10,9 @@ import jakarta.mail.Session
|
|||||||
import jakarta.mail.internet.InternetAddress
|
import jakarta.mail.internet.InternetAddress
|
||||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||||
import org.springframework.context.event.EventListener
|
import org.springframework.context.event.EventListener
|
||||||
import org.springframework.mail.SimpleMailMessage
|
import org.springframework.mail.SimpleMailMessage
|
||||||
@@ -20,6 +20,7 @@ import org.springframework.mail.javamail.JavaMailSender
|
|||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
@@ -27,6 +28,7 @@ import kotlin.uuid.Uuid
|
|||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@Service
|
@Service
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@ConditionalOnProperty(value = ["mail.polling.enabled"], havingValue = "true", matchIfMissing = false)
|
||||||
class MailPollingService(
|
class MailPollingService(
|
||||||
private val mailSender: JavaMailSender,
|
private val mailSender: JavaMailSender,
|
||||||
private val nennungRepository: NennungRepository,
|
private val nennungRepository: NennungRepository,
|
||||||
|
|||||||
+28
-1
@@ -4,22 +4,49 @@ import org.slf4j.LoggerFactory
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.event.EventListener
|
import org.springframework.context.event.EventListener
|
||||||
import org.springframework.core.env.Environment
|
import org.springframework.core.env.Environment
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(scanBasePackages = ["at.mocode.mail", "at.mocode.infrastructure.security"])
|
||||||
class MailServiceApplication(private val env: Environment) {
|
class MailServiceApplication(private val env: Environment) {
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(MailServiceApplication::class.java)
|
private val log = LoggerFactory.getLogger(MailServiceApplication::class.java)
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun corsConfigurer(): WebMvcConfigurer {
|
||||||
|
return object : WebMvcConfigurer {
|
||||||
|
override fun addCorsMappings(registry: CorsRegistry) {
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOrigins("*")
|
||||||
|
.allowedMethods("*")
|
||||||
|
.allowedHeaders("*")
|
||||||
|
.allowCredentials(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent::class)
|
@EventListener(ApplicationReadyEvent::class)
|
||||||
fun onApplicationReady() {
|
fun onApplicationReady() {
|
||||||
val springPort = env.getProperty("server.port", "8083")
|
val springPort = env.getProperty("server.port", "8083")
|
||||||
val appName = env.getProperty("spring.application.name", "mail-service")
|
val appName = env.getProperty("spring.application.name", "mail-service")
|
||||||
|
|
||||||
|
val mailHost = env.getProperty("spring.mail.host")
|
||||||
|
val mailPort = env.getProperty("spring.mail.port")
|
||||||
|
val mailUser = env.getProperty("spring.mail.username")
|
||||||
|
val mailPass = env.getProperty("spring.mail.password")?.take(3) + "***"
|
||||||
|
val connTimeout = env.getProperty("spring.mail.properties.mail.smtp.connectiontimeout")
|
||||||
|
|
||||||
|
val envHost = System.getenv("SPRING_MAIL_HOST")
|
||||||
|
val envPort = System.getenv("SPRING_MAIL_PORT")
|
||||||
|
|
||||||
log.info("----------------------------------------------------------")
|
log.info("----------------------------------------------------------")
|
||||||
log.info("Application '{}' is running!", appName)
|
log.info("Application '{}' is running!", appName)
|
||||||
log.info("Spring Management Port: {}", springPort)
|
log.info("Spring Management Port: {}", springPort)
|
||||||
|
log.info("SMTP Config (Resolved): host={}, port={}, user={}, pass={}, timeout={}", mailHost, mailPort, mailUser, mailPass, connTimeout)
|
||||||
|
log.info("SMTP Config (Raw Env): host={}, port={}, pass={}", envHost, envPort, System.getenv("SPRING_MAIL_PASSWORD")?.take(3) + "***")
|
||||||
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
log.info("Profiles: {}", env.activeProfiles.joinToString(", "))
|
||||||
log.info("----------------------------------------------------------")
|
log.info("----------------------------------------------------------")
|
||||||
}
|
}
|
||||||
|
|||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.mail.service
|
|
||||||
|
|
||||||
import at.mocode.mail.service.persistence.NennungEntity
|
|
||||||
import at.mocode.mail.service.persistence.NennungRepository
|
|
||||||
import org.springframework.web.bind.annotation.*
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/mail/nennungen")
|
|
||||||
class NennungController(
|
|
||||||
private val nennungRepository: NennungRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
fun getAllNennungen(): List<NennungEntity> {
|
|
||||||
return nennungRepository.findAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}/status")
|
|
||||||
fun updateStatus(
|
|
||||||
@PathVariable id: String,
|
|
||||||
@RequestBody newStatus: String
|
|
||||||
) {
|
|
||||||
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
fun createNennung(@RequestBody nennung: NennungEntity) {
|
|
||||||
nennungRepository.save(nennung)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+83
-13
@@ -39,7 +39,6 @@ data class NennungRequest(
|
|||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/mail")
|
@RequestMapping("/api/mail")
|
||||||
@CrossOrigin(origins = ["http://localhost:8080", "https://nennung.mo-code.at"]) // Für Wasm-Web-App (Compose HTML/Wasm)
|
|
||||||
class MailController(
|
class MailController(
|
||||||
private val nennungRepository: NennungRepository,
|
private val nennungRepository: NennungRepository,
|
||||||
private val mailSender: JavaMailSender
|
private val mailSender: JavaMailSender
|
||||||
@@ -50,7 +49,7 @@ class MailController(
|
|||||||
private lateinit var baseMailAddress: String
|
private lateinit var baseMailAddress: String
|
||||||
|
|
||||||
@PostMapping("/nennung")
|
@PostMapping("/nennung")
|
||||||
fun receiveNennung(@Valid @RequestBody request: NennungRequest) {
|
fun receiveNennung(@Valid @RequestBody request: NennungRequest): Map<String, Any> {
|
||||||
logger.info("Nennung via API erhalten: ${request.vorname} ${request.nachname} für Turnier ${request.turnierNr}")
|
logger.info("Nennung via API erhalten: ${request.vorname} ${request.nachname} für Turnier ${request.turnierNr}")
|
||||||
|
|
||||||
val entity = NennungEntity(
|
val entity = NennungEntity(
|
||||||
@@ -71,19 +70,45 @@ class MailController(
|
|||||||
nennungRepository.save(entity)
|
nennungRepository.save(entity)
|
||||||
logger.info("Nennung ${entity.id} in Datenbank persistiert.")
|
logger.info("Nennung ${entity.id} in Datenbank persistiert.")
|
||||||
|
|
||||||
// Bestätigung an Reiter senden
|
// --- PLAN B: Benachrichtigung an die Meldestelle (online-nennen@mo-code.at) senden ---
|
||||||
|
logger.info("Versuche Benachrichtigungs-Mail an $baseMailAddress zu senden...")
|
||||||
|
try {
|
||||||
|
val notification = SimpleMailMessage()
|
||||||
|
notification.from = baseMailAddress // Mailserver erfordert oft, dass From == Username ist
|
||||||
|
notification.setTo(baseMailAddress) // Wir senden es an uns selbst
|
||||||
|
// WICHTIG: Die Turniernummer im Betreff für das einfache Mail-Filtering!
|
||||||
|
notification.subject = "[NENNUNG] Turnier ${request.turnierNr} - ${request.vorname} ${request.nachname}"
|
||||||
|
|
||||||
|
val textBody = buildString {
|
||||||
|
appendLine("Neue Online-Nennung eingegangen!")
|
||||||
|
appendLine("----------------------------------")
|
||||||
|
appendLine("Turnier: ${request.turnierNr}")
|
||||||
|
appendLine("Reiter: ${request.vorname} ${request.nachname}")
|
||||||
|
appendLine("Lizenz: ${request.lizenz}")
|
||||||
|
appendLine("Pferd: ${request.pferdName} (Alter: ${request.pferdAlter})")
|
||||||
|
appendLine("E-Mail: ${request.email}")
|
||||||
|
appendLine("Telefon: ${request.telefon ?: "-"}")
|
||||||
|
appendLine("Bewerbe: ${request.bewerbe}")
|
||||||
|
appendLine("Bemerkungen: ${request.bemerkungen ?: "-"}")
|
||||||
|
appendLine("----------------------------------")
|
||||||
|
appendLine("System-ID: ${entity.id}")
|
||||||
|
}
|
||||||
|
notification.text = textBody
|
||||||
|
|
||||||
|
mailSender.send(notification)
|
||||||
|
logger.info("Plan-B Nennungs-Mail an die Meldestelle gesendet. Betreff: ${notification.subject}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("KRITISCH: Fehler beim Senden der Plan-B Nennungs-Mail an die Meldestelle: ${e.message}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Ursprüngliche Bestätigung an den Reiter (optional, bleibt vorerst erhalten) ---
|
||||||
|
logger.info("Versuche Bestätigungs-Mail an ${request.email} zu senden...")
|
||||||
try {
|
try {
|
||||||
val message = SimpleMailMessage()
|
val message = SimpleMailMessage()
|
||||||
|
|
||||||
// Dynamische Absenderadresse mit Plus-Addressing (z.B. online-nennen+26128@mo-code.at)
|
// PLAN B Fallback: Kein Plus-Addressing, da World4You es nicht unterstützt
|
||||||
val dynamicFrom = try {
|
// Wir verwenden als Absender einfach die Basis-Adresse
|
||||||
val (user, domain) = baseMailAddress.split("@")
|
message.from = baseMailAddress
|
||||||
"$user+${request.turnierNr}@$domain"
|
|
||||||
} catch (_: Exception) {
|
|
||||||
baseMailAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
message.from = dynamicFrom
|
|
||||||
message.setTo(request.email)
|
message.setTo(request.email)
|
||||||
message.subject = "Bestätigung: Ihre Online-Nennung für Turnier ${request.turnierNr}"
|
message.subject = "Bestätigung: Ihre Online-Nennung für Turnier ${request.turnierNr}"
|
||||||
message.text = """
|
message.text = """
|
||||||
@@ -103,12 +128,57 @@ class MailController(
|
|||||||
mailSender.send(message)
|
mailSender.send(message)
|
||||||
logger.info("Bestätigungs-Mail an ${request.email} gesendet.")
|
logger.info("Bestätigungs-Mail an ${request.email} gesendet.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error("Fehler beim Senden der Bestätigungs-Mail: ${e.message}")
|
logger.error("KRITISCH: Fehler beim Senden der Bestätigungs-Mail an ${request.email}: ${e.message}", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"success" to true,
|
||||||
|
"message" to "Nennung erhalten und verarbeitet",
|
||||||
|
"id" to entity.id.toString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/nennungen")
|
@GetMapping("/nennungen")
|
||||||
fun getAllNennungen(): List<NennungEntity> {
|
fun getAllNennungen(): List<NennungEntity> {
|
||||||
return nennungRepository.findAll()
|
return nennungRepository.findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/nennungen/{id}/status")
|
||||||
|
fun updateStatus(
|
||||||
|
@PathVariable id: String,
|
||||||
|
@RequestBody newStatus: String
|
||||||
|
) {
|
||||||
|
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/nennungen")
|
||||||
|
fun createNennung(@RequestBody nennung: NennungEntity) {
|
||||||
|
nennungRepository.save(nennung)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/send-reply")
|
||||||
|
fun sendReply(
|
||||||
|
@RequestParam email: String,
|
||||||
|
@RequestParam turnierNr: String,
|
||||||
|
@RequestParam vorname: String,
|
||||||
|
@RequestParam nachname: String
|
||||||
|
) {
|
||||||
|
val message = SimpleMailMessage()
|
||||||
|
// PLAN B Fallback: Kein Plus-Addressing
|
||||||
|
message.from = baseMailAddress
|
||||||
|
message.setTo(email)
|
||||||
|
message.subject = "Bestätigung: Nennung für Turnier $turnierNr manuell übernommen"
|
||||||
|
message.text = """
|
||||||
|
Sehr geehrte(r) $vorname $nachname,
|
||||||
|
|
||||||
|
Ihre Online-Nennung für das Turnier $turnierNr wurde von uns manuell in das Turniersystem übernommen.
|
||||||
|
|
||||||
|
Viel Erfolg beim Turnier!
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen,
|
||||||
|
Ihre Meldestelle
|
||||||
|
""".trimIndent()
|
||||||
|
mailSender.send(message)
|
||||||
|
logger.info("Antwort-Mail an $email gesendet.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package at.mocode.mail.service.config
|
||||||
|
|
||||||
|
import at.mocode.mail.service.persistence.NennungTable
|
||||||
|
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.context.annotation.Configuration
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires Spring's DataSource into Exposed and ensures the schema exists.
|
||||||
|
* This replaces the implicit init that previously happened in the polling service.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
class ExposedConfiguration(
|
||||||
|
private val dataSource: DataSource,
|
||||||
|
) {
|
||||||
|
private val log = LoggerFactory.getLogger(ExposedConfiguration::class.java)
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun connectAndInitSchema() {
|
||||||
|
// Bind Exposed to Spring's DataSource
|
||||||
|
Database.connect(dataSource)
|
||||||
|
|
||||||
|
// Create required tables if missing (idempotent for H2 and typical RDBMS)
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(NennungTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Exposed connected to DataSource and schema initialized (NennungTable).")
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -5,9 +5,9 @@ package at.mocode.mail.service.persistence
|
|||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.insert
|
import org.jetbrains.exposed.v1.jdbc.insert
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|||||||
@@ -12,23 +12,28 @@ spring:
|
|||||||
show-sql: true
|
show-sql: true
|
||||||
mail:
|
mail:
|
||||||
host: ${SPRING_MAIL_HOST:smtp.world4you.com}
|
host: ${SPRING_MAIL_HOST:smtp.world4you.com}
|
||||||
port: ${SPRING_MAIL_PORT:587}
|
port: 587
|
||||||
username: ${SPRING_MAIL_USERNAME:online-nennen@mo-code.at}
|
username: ${SPRING_MAIL_USERNAME:online-nennen@mo-code.at}
|
||||||
password: ${SPRING_MAIL_PASSWORD:}
|
password: ${SPRING_MAIL_PASSWORD:Mogi#2reiten}
|
||||||
properties:
|
properties:
|
||||||
mail:
|
mail:
|
||||||
smtp:
|
smtp:
|
||||||
auth: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH:true}
|
auth: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH:true}
|
||||||
|
connectiontimeout: 5000
|
||||||
|
timeout: 5000
|
||||||
|
writetimeout: 5000
|
||||||
starttls:
|
starttls:
|
||||||
enable: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE:true}
|
enable: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||||
|
required: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED:true}
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
consul:
|
consul:
|
||||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||||
|
enabled: ${SPRING_CLOUD_CONSUL_ENABLED:false}
|
||||||
discovery:
|
discovery:
|
||||||
enabled: true
|
enabled: ${SPRING_CLOUD_CONSUL_DISCOVERY_ENABLED:false}
|
||||||
register: true
|
register: ${SPRING_CLOUD_CONSUL_DISCOVERY_REGISTER:false}
|
||||||
prefer-ip-address: true
|
prefer-ip-address: true
|
||||||
health-check-path: /actuator/health
|
health-check-path: /actuator/health
|
||||||
health-check-interval: 10s
|
health-check-interval: 10s
|
||||||
@@ -43,4 +48,14 @@ management:
|
|||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
exposure:
|
exposure:
|
||||||
include: "health,info,prometheus"
|
include: health,info,prometheus
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
probes:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Feature-Flags
|
||||||
|
mail:
|
||||||
|
polling:
|
||||||
|
enabled: ${MAIL_POLLING_ENABLED:false}
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ USER ${APP_USER}
|
|||||||
|
|
||||||
EXPOSE 8086 5005
|
EXPOSE 8086 5005
|
||||||
|
|
||||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \
|
||||||
CMD curl -fsS --max-time 2 http://localhost:8086/actuator/health/readiness || exit 1
|
CMD curl -fsS --max-time 5 http://localhost:${SERVER_PORT:-8086}/actuator/health/readiness || exit 1
|
||||||
|
|
||||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||||
-XX:+UseG1GC \
|
-XX:+UseG1GC \
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
|
|||||||
* GET /funktionaer — Alle Funktionäre (paginiert).
|
* GET /funktionaer — Alle Funktionäre (paginiert).
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(5000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = funktionaerRepository.findAll(limit, offset)
|
val results = funktionaerRepository.findAll(limit, offset)
|
||||||
|
|||||||
+5
-5
@@ -62,11 +62,11 @@ class HorseController(private val horseRepository: HorseRepository) {
|
|||||||
route("/horse") {
|
route("/horse") {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang.
|
* GET /horse — alle Pferde (paginiert), optional gefiltert nach Jahrgang.
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
|
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(50000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = when {
|
val results = when {
|
||||||
@@ -77,7 +77,7 @@ class HorseController(private val horseRepository: HorseRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /horse/search?q=... — Sucht Pferde nach Lebensnummer.
|
* GET /horse/search?q= … — Sucht Pferde nach Lebensnummer.
|
||||||
*/
|
*/
|
||||||
get("/search") {
|
get("/search") {
|
||||||
val query = call.request.queryParameters["q"] ?: ""
|
val query = call.request.queryParameters["q"] ?: ""
|
||||||
@@ -86,7 +86,7 @@ class HorseController(private val horseRepository: HorseRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /horse/{id} — Ruft ein spezifisches Pferd ab.
|
* GET /horse/{id} — ruft ein spezifisches Pferd ab.
|
||||||
*/
|
*/
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||||
@@ -104,7 +104,7 @@ class HorseController(private val horseRepository: HorseRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /horse — Erstellt ein neues Pferd.
|
* POST /horse — erstellt ein neues Pferd.
|
||||||
*/
|
*/
|
||||||
post {
|
post {
|
||||||
val req = call.receive<HorseCreateRequest>()
|
val req = call.receive<HorseCreateRequest>()
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
|
|||||||
* GET /reiter — Alle Reiter (paginiert).
|
* GET /reiter — Alle Reiter (paginiert).
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(50000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = reiterRepository.findAll(limit, offset)
|
val results = reiterRepository.findAll(limit, offset)
|
||||||
|
|||||||
+5
-5
@@ -76,11 +76,11 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
|||||||
route("/verein") {
|
route("/verein") {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /verein — Alle Vereine (paginiert), optional gefiltert nach verband/bundesland.
|
* GET /verein — alle Vereine (paginiert), optional gefiltert nach Verband/Bundesland.
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val verband = call.request.queryParameters["verband"]
|
val verband = call.request.queryParameters["verband"]
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(5000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = if (verband != null) {
|
val results = if (verband != null) {
|
||||||
@@ -92,7 +92,7 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /verein/search?q=... — Sucht Vereine nach Name.
|
* GET /verein/search?q= … — Sucht Vereine nach Namen.
|
||||||
*/
|
*/
|
||||||
get("/search") {
|
get("/search") {
|
||||||
val query = call.request.queryParameters["q"] ?: ""
|
val query = call.request.queryParameters["q"] ?: ""
|
||||||
@@ -101,7 +101,7 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /verein/{id} — Ruft einen spezifischen Verein ab.
|
* GET /verein/{id} — ruft einen spezifischen Verein ab.
|
||||||
*/
|
*/
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||||
@@ -119,7 +119,7 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /verein — Erstellt einen neuen Verein.
|
* POST /verein — erstellt einen neuen Verein.
|
||||||
*/
|
*/
|
||||||
post {
|
post {
|
||||||
val req = call.receive<VereinCreateRequest>()
|
val req = call.receive<VereinCreateRequest>()
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ plugins {
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
js(IR) {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,61 @@
|
|||||||
|
server:
|
||||||
|
port: ${MASTERDATA_SERVER_PORT:8086}
|
||||||
|
|
||||||
|
ktor:
|
||||||
|
port: ${MASTERDATA_KTOR_PORT:8091}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: masterdata-service
|
name: masterdata-service
|
||||||
main:
|
main:
|
||||||
banner-mode: "off"
|
banner-mode: "off"
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
flyway:
|
flyway:
|
||||||
enabled: true
|
enabled: true
|
||||||
baseline-on-migrate: true
|
baseline-on-migrate: true
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
consul:
|
consul:
|
||||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||||
enabled: ${CONSUL_ENABLED:true}
|
|
||||||
discovery:
|
discovery:
|
||||||
enabled: ${CONSUL_ENABLED:true}
|
enabled: true
|
||||||
register: ${CONSUL_ENABLED:true}
|
register: true
|
||||||
prefer-ip-address: true
|
prefer-ip-address: true
|
||||||
health-check-path: /actuator/health
|
health-check-path: /actuator/health/readiness
|
||||||
health-check-interval: 20s
|
health-check-interval: 10s
|
||||||
health-check-timeout: 10s
|
health-check-timeout: 5s
|
||||||
# deregister-critical-service-after: 5m
|
health-check-port: 8086
|
||||||
# health-check-port: 8086 # Spring Boot Management Port (Actuator)
|
health-check-critical-timeout: 2m
|
||||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
deregister-critical-service-after: 5m
|
||||||
|
instance-id: ${spring.application.name}:${random.uuid}
|
||||||
service-name: ${spring.application.name}
|
service-name: ${spring.application.name}
|
||||||
port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel)
|
port: 8091
|
||||||
|
|
||||||
server:
|
#server:
|
||||||
port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
# port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
||||||
address: 0.0.0.0 # Erreichbar für Consul Health Checks
|
# address: 0.0.0.0 # Erreichbar für Consul Health Checks
|
||||||
|
|
||||||
masterdata:
|
#masterdata:
|
||||||
http:
|
# http:
|
||||||
port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
# port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
||||||
address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
# address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
exposure:
|
exposure:
|
||||||
include: "health,info,metrics,prometheus"
|
include: health,info,metrics,prometheus
|
||||||
endpoint:
|
endpoint:
|
||||||
health:
|
health:
|
||||||
show-details: always
|
show-details: always
|
||||||
|
show-components: always
|
||||||
probes:
|
probes:
|
||||||
enabled: true
|
enabled: true
|
||||||
prometheus:
|
prometheus:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
alias(libs.plugins.kotlinJpa)
|
alias(libs.plugins.kotlinJpa)
|
||||||
@@ -37,8 +37,7 @@ dependencies {
|
|||||||
implementation(libs.bundles.database.complete)
|
implementation(libs.bundles.database.complete)
|
||||||
|
|
||||||
// === Resilience ===
|
// === Resilience ===
|
||||||
implementation(libs.resilience4j.spring.boot3)
|
implementation(libs.bundles.resilience)
|
||||||
implementation(libs.resilience4j.reactor)
|
|
||||||
implementation(libs.spring.boot.starter.aop)
|
implementation(libs.spring.boot.starter.aop)
|
||||||
|
|
||||||
// === Testing ===
|
// === Testing ===
|
||||||
|
|||||||
+17
-8
@@ -2,6 +2,7 @@ package at.mocode.ping.infrastructure.web
|
|||||||
|
|
||||||
import at.mocode.ping.api.*
|
import at.mocode.ping.api.*
|
||||||
import at.mocode.ping.application.PingUseCase
|
import at.mocode.ping.application.PingUseCase
|
||||||
|
import at.mocode.ping.domain.Ping
|
||||||
import at.mocode.ping.infrastructure.PingProperties
|
import at.mocode.ping.infrastructure.PingProperties
|
||||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -20,7 +21,7 @@ import kotlin.uuid.ExperimentalUuidApi
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class PingController(
|
open class PingController(
|
||||||
private val pingUseCase: PingUseCase,
|
private val pingUseCase: PingUseCase,
|
||||||
private val properties: PingProperties
|
private val properties: PingProperties
|
||||||
) : PingApi {
|
) : PingApi {
|
||||||
@@ -43,10 +44,16 @@ class PingController(
|
|||||||
override suspend fun enhancedPing(
|
override suspend fun enhancedPing(
|
||||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||||
): EnhancedPingResponse {
|
): EnhancedPingResponse {
|
||||||
|
logger.info("Enhanced ping requested, simulate: {}", simulate)
|
||||||
val start = System.nanoTime()
|
val start = System.nanoTime()
|
||||||
|
|
||||||
if (simulate && Random.nextDouble() < 0.6) {
|
if (simulate) {
|
||||||
throw RuntimeException("Simulated service failure")
|
if (Random.nextDouble() < 0.6) {
|
||||||
|
logger.info("Simulating service failure now...")
|
||||||
|
throw SimulatedException("Simulated service failure")
|
||||||
|
} else {
|
||||||
|
logger.info("Simulation mode ACTIVE, but this time lucky: Request passed!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
||||||
@@ -61,6 +68,8 @@ class PingController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SimulatedException(message: String) : RuntimeException(message)
|
||||||
|
|
||||||
// Neue Endpunkte
|
// Neue Endpunkte
|
||||||
|
|
||||||
@GetMapping("/ping/public")
|
@GetMapping("/ping/public")
|
||||||
@@ -70,7 +79,7 @@ class PingController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/ping/secure")
|
@GetMapping("/ping/secure")
|
||||||
@PreAuthorize("hasRole('MELD_USER') or hasRole('MELD_ADMIN')") // Beispiel-Rollen
|
@PreAuthorize("hasRole('ROLE_MELD_USER') or hasRole('ROLE_MELD_ADMIN')") // Beispiel-Rollen
|
||||||
override suspend fun securePing(): PingResponse {
|
override suspend fun securePing(): PingResponse {
|
||||||
val domainPing = pingUseCase.executePing("Secure Ping")
|
val domainPing = pingUseCase.executePing("Secure Ping")
|
||||||
return createResponse(domainPing, "secure-pong")
|
return createResponse(domainPing, "secure-pong")
|
||||||
@@ -79,7 +88,7 @@ class PingController(
|
|||||||
@GetMapping("/ping/sync")
|
@GetMapping("/ping/sync")
|
||||||
override suspend fun syncPings(
|
override suspend fun syncPings(
|
||||||
// Changed the parameter name to 'since' to match SyncManager convention
|
// Changed the parameter name to 'since' to match SyncManager convention
|
||||||
@RequestParam(required = false, defaultValue = "0") since: Long
|
@RequestParam(name = "lastSyncTimestamp", required = false, defaultValue = "0") since: Long
|
||||||
): List<PingEvent> {
|
): List<PingEvent> {
|
||||||
return pingUseCase.getPingsSince(since).map {
|
return pingUseCase.getPingsSince(since).map {
|
||||||
PingEvent(
|
PingEvent(
|
||||||
@@ -91,7 +100,7 @@ class PingController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper
|
// Helper
|
||||||
private fun createResponse(domainPing: at.mocode.ping.domain.Ping, status: String) = PingResponse(
|
private fun createResponse(domainPing: Ping, status: String) = PingResponse(
|
||||||
status = status,
|
status = status,
|
||||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||||
service = properties.serviceName
|
service = properties.serviceName
|
||||||
@@ -99,8 +108,8 @@ class PingController(
|
|||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
@Suppress("unused", "UNUSED_PARAMETER")
|
@Suppress("unused", "UNUSED_PARAMETER")
|
||||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
open fun fallbackPing(simulate: Boolean, ex: Throwable): EnhancedPingResponse {
|
||||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
logger.error("CIRCUIT BREAKER FALLBACK TRIGGERED! Reason: {}", ex.message, ex)
|
||||||
return EnhancedPingResponse(
|
return EnhancedPingResponse(
|
||||||
status = "fallback",
|
status = "fallback",
|
||||||
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package at.mocode.ping.infrastructure.web
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ProblemDetail
|
||||||
|
import org.springframework.security.access.AccessDeniedException
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class PingExceptionHandler {
|
||||||
|
private val log = LoggerFactory.getLogger(PingExceptionHandler::class.java)
|
||||||
|
|
||||||
|
@ExceptionHandler(AccessDeniedException::class)
|
||||||
|
fun handleAccessDenied(ex: AccessDeniedException): ProblemDetail {
|
||||||
|
log.warn("Zugriff verweigert: ${ex.message}")
|
||||||
|
return ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, "Nicht berechtigt: ${ex.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception::class)
|
||||||
|
fun handleAll(ex: Exception): ProblemDetail {
|
||||||
|
log.error("Unerwarteter Fehler: ", ex)
|
||||||
|
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.message ?: "Ein interner Fehler ist aufgetreten")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException::class)
|
||||||
|
fun handleRuntime(ex: RuntimeException): ProblemDetail {
|
||||||
|
log.error("Interner Fehler: ", ex)
|
||||||
|
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.message ?: "Unbekannter Fehler")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+23
-5
@@ -40,12 +40,17 @@ import kotlin.uuid.ExperimentalUuidApi
|
|||||||
controllers = [PingController::class],
|
controllers = [PingController::class],
|
||||||
properties = ["spring.aop.proxy-target-class=true"]
|
properties = ["spring.aop.proxy-target-class=true"]
|
||||||
)
|
)
|
||||||
|
@Import(
|
||||||
|
PingControllerTest.PingControllerTestConfig::class,
|
||||||
|
io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.CircuitBreakerAutoConfiguration::class,
|
||||||
|
io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.CircuitBreakerMetricsAutoConfiguration::class,
|
||||||
|
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration::class
|
||||||
|
)
|
||||||
@ContextConfiguration(classes = [TestPingServiceApplication::class])
|
@ContextConfiguration(classes = [TestPingServiceApplication::class])
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
@Import(PingControllerTest.PingControllerTestConfig::class)
|
|
||||||
@AutoConfigureMockMvc(addFilters = false)
|
@AutoConfigureMockMvc(addFilters = false)
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class PingControllerTest {
|
open class PingControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
@@ -125,11 +130,24 @@ class PingControllerTest {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
val json = objectMapper.readTree(result.response.contentAsString)
|
val json = objectMapper.readTree(result.response.contentAsString)
|
||||||
assertThat(json["status"].asText()).isEqualTo("pong")
|
|
||||||
assertThat(json["service"].asText()).isEqualTo(properties.serviceName)
|
assertThat(json["service"].asText()).isEqualTo(properties.serviceName)
|
||||||
verify { pingUseCase.executePing("Enhanced Ping") }
|
verify { pingUseCase.executePing("Enhanced Ping") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return fallback when simulation failure occurs`() {
|
||||||
|
// Given
|
||||||
|
val controller = PingController(pingUseCase, properties)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val response = controller.fallbackPing(simulate = true, ex = PingController.SimulatedException("test"))
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.status).isEqualTo("fallback")
|
||||||
|
assertThat(response.service).isEqualTo(properties.serviceNameFallback)
|
||||||
|
assertThat(response.circuitBreakerState).isEqualTo("OPEN")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `should return health check response with status up`() {
|
fun `should return health check response with status up`() {
|
||||||
// When
|
// When
|
||||||
@@ -159,7 +177,7 @@ class PingControllerTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("since", timestamp.toString()))
|
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("lastSyncTimestamp", timestamp.toString()))
|
||||||
.andExpect(request().asyncStarted())
|
.andExpect(request().asyncStarted())
|
||||||
.andReturn()
|
.andReturn()
|
||||||
|
|
||||||
@@ -183,7 +201,7 @@ class PingControllerTest {
|
|||||||
every { pingUseCase.getPingsSince(timestamp) } returns emptyList()
|
every { pingUseCase.getPingsSince(timestamp) } returns emptyList()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("since", timestamp.toString()))
|
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("lastSyncTimestamp", timestamp.toString()))
|
||||||
.andExpect(request().asyncStarted())
|
.andExpect(request().asyncStarted())
|
||||||
.andReturn()
|
.andReturn()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
metrics
|
metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
:4000 {
|
:80 {
|
||||||
root * /usr/share/caddy
|
root * /usr/share/caddy
|
||||||
log {
|
log {
|
||||||
output stdout
|
output stdout
|
||||||
@@ -17,14 +17,56 @@
|
|||||||
|
|
||||||
encode gzip zstd
|
encode gzip zstd
|
||||||
|
|
||||||
|
# Same-Origin Strategy: Alle /api/* Anfragen werden intern an den Mail-Service weitergeleitet
|
||||||
|
# Dadurch sieht der Browser nur noch app.mo-code.at und CORS wird hinfällig.
|
||||||
handle /api/* {
|
handle /api/* {
|
||||||
reverse_proxy api-gateway:8081
|
reverse_proxy mail-service:8085 {
|
||||||
|
header_up Host {upstream_hostport}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
Access-Control-Allow-Origin "*"
|
||||||
|
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
|
Access-Control-Allow-Headers "*"
|
||||||
|
X-Caddy-Strategy "same-origin-v32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle /health {
|
handle /health {
|
||||||
respond "healthy" 200
|
respond "healthy" 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Korrekte MIME für .wasm sicherstellen (Caddy erkennt es i. d. R. automatisch; hier explizit)
|
||||||
|
@wasm {
|
||||||
|
path *.wasm
|
||||||
|
}
|
||||||
|
header @wasm Content-Type "application/wasm"
|
||||||
|
|
||||||
|
# Caching-Strategie: Immutable Assets (hash-Dateien)
|
||||||
|
# WICHTIG: .wasm und .js werden hier gecached. Falls die Dateinamen gleich bleiben,
|
||||||
|
# wird der Browser sie NICHT neu laden.
|
||||||
|
@immutable {
|
||||||
|
path *.png *.svg *.ico *.woff2 *.map
|
||||||
|
}
|
||||||
|
header @immutable Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
# Wasm und JS Dateien: Kein Cache während der aktiven Entwicklungsphase (Plan-B)
|
||||||
|
# um "Alte Seite" Probleme zu vermeiden.
|
||||||
|
@wasm_js {
|
||||||
|
path *.wasm *.js
|
||||||
|
}
|
||||||
|
header @wasm_js Cache-Control "no-store, no-cache, must-revalidate"
|
||||||
|
|
||||||
|
# Keine Cache-Header für SPA-Einstieg und Laufzeitkonfig
|
||||||
|
@nocache {
|
||||||
|
path /index.html /config.json
|
||||||
|
}
|
||||||
|
header @nocache Cache-Control "no-store"
|
||||||
|
|
||||||
|
# Static file serving mit SPA-Fallback
|
||||||
handle {
|
handle {
|
||||||
try_files {path} /index.html
|
try_files {path} /index.html
|
||||||
file_server
|
file_server
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ COPY config/docker/caddy/web-app/entrypoint.sh /entrypoint.sh
|
|||||||
COPY config/docker/caddy/web-app/config.json /usr/share/caddy/config.json.tmpl
|
COPY config/docker/caddy/web-app/config.json /usr/share/caddy/config.json.tmpl
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Copy Pre-built Static Assets from Host
|
# Copy Pre-built Static Assets from Host (WasmJs)
|
||||||
# NOTE: You must run `./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution -Pproduction=true` locally first!
|
# NOTE: BUILD_DATE wird hier genutzt, um den Layer-Cache zu invalidieren,
|
||||||
COPY frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/ /usr/share/caddy/
|
# falls sich der Code geändert hat, aber die Dateimetadaten im Runner-Cache gleich blieben.
|
||||||
|
ARG BUILD_DATE
|
||||||
|
COPY config/docker/caddy/web-app/_site/ /usr/share/caddy/
|
||||||
# index.html wird als Template abgelegt; der Entrypoint erzeugt daraus zur Laufzeit die finale index.html
|
# index.html wird als Template abgelegt; der Entrypoint erzeugt daraus zur Laufzeit die finale index.html
|
||||||
RUN mv /usr/share/caddy/index.html /usr/share/caddy/index.html.tmpl
|
RUN mv /usr/share/caddy/index.html /usr/share/caddy/index.html.tmpl
|
||||||
|
|
||||||
@@ -41,10 +43,10 @@ RUN mv /usr/share/caddy/index.html /usr/share/caddy/index.html.tmpl
|
|||||||
# Using the shared asset from existing config structure
|
# Using the shared asset from existing config structure
|
||||||
COPY config/docker/nginx/web-app/favicon.svg /usr/share/caddy/favicon.svg
|
COPY config/docker/nginx/web-app/favicon.svg /usr/share/caddy/favicon.svg
|
||||||
|
|
||||||
EXPOSE 4000
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"apiBaseUrl": "${API_BASE_URL}",
|
"apiBaseUrl": "${API_BASE_URL}",
|
||||||
|
"mailServiceUrl": "${MAIL_SERVICE_URL}",
|
||||||
"keycloakUrl": "${KEYCLOAK_URL}"
|
"keycloakUrl": "${KEYCLOAK_URL}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Ersetze ${API_BASE_URL} und ${KEYCLOAK_URL} in index.html und config.json zur Container-Startzeit.
|
# Ersetze ${API_BASE_URL}, ${MAIL_SERVICE_URL} und ${KEYCLOAK_URL} in index.html und config.json zur Container-Startzeit.
|
||||||
# Caddy bekommt fertige, statische Dateien — kein Template-Parsing mehr nötig.
|
# Caddy bekommt fertige, statische Dateien — kein Template-Parsing mehr nötig.
|
||||||
envsubst '${API_BASE_URL} ${KEYCLOAK_URL}' \
|
# Wir fügen zusätzlich einen Cache-Buster (Zeitstempel) an den Script-Tag in der index.html an
|
||||||
< /usr/share/caddy/index.html.tmpl \
|
CACHE_BUSTER=$(date +%s)
|
||||||
|
envsubst '${API_BASE_URL} ${MAIL_SERVICE_URL} ${KEYCLOAK_URL}' \
|
||||||
|
< /usr/share/caddy/index.html.tmpl | \
|
||||||
|
sed "s|meldestelle-web.js|meldestelle-web.js?v=${CACHE_BUSTER}|g" \
|
||||||
> /usr/share/caddy/index.html
|
> /usr/share/caddy/index.html
|
||||||
|
|
||||||
envsubst '${API_BASE_URL} ${KEYCLOAK_URL}' \
|
envsubst '${API_BASE_URL} ${MAIL_SERVICE_URL} ${KEYCLOAK_URL}' \
|
||||||
< /usr/share/caddy/config.json.tmpl \
|
< /usr/share/caddy/config.json.tmpl \
|
||||||
> /usr/share/caddy/config.json
|
> /usr/share/caddy/config.json
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,6 @@ version = "1.0.0"
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
js(IR) {
|
|
||||||
binaries.library()
|
|
||||||
browser {
|
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
binaries.library()
|
binaries.library()
|
||||||
browser {
|
browser {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
@@ -10,17 +12,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
js(IR) {
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
binaries.library()
|
|
||||||
// Re-enabled browser environment after Root NodeJs fix
|
|
||||||
browser {
|
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
binaries.library()
|
binaries.library()
|
||||||
browser()
|
browser()
|
||||||
@@ -35,17 +27,10 @@ kotlin {
|
|||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
api(libs.kotlinx.serialization.json)
|
api(libs.kotlinx.serialization.json)
|
||||||
api(libs.kotlinx.datetime)
|
api(libs.kotlinx.datetime)
|
||||||
}
|
|
||||||
|
|
||||||
commonTest.dependencies {
|
|
||||||
implementation(libs.kotlin.test)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsMain.dependencies {
|
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
@@ -5,15 +7,7 @@ plugins {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js {
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
browser {
|
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Wasm support enabled?
|
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ plugins {
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
|
||||||
js(IR) {
|
|
||||||
binaries.library()
|
|
||||||
browser {
|
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wasmJs {
|
wasmJs {
|
||||||
binaries.library()
|
binaries.library()
|
||||||
browser {
|
browser {
|
||||||
|
|||||||
+3
-1
@@ -59,7 +59,7 @@ services:
|
|||||||
|
|
||||||
# --- SERVICE URLs ---
|
# --- SERVICE URLs ---
|
||||||
PING_SERVICE_URL: "http://ping-service:8082"
|
PING_SERVICE_URL: "http://ping-service:8082"
|
||||||
MASTERDATA_SERVICE_URL: "http://masterdata-service:8086"
|
MASTERDATA_SERVICE_URL: "http://masterdata-service:8091"
|
||||||
EVENTS_SERVICE_URL: "http://events-service:8085"
|
EVENTS_SERVICE_URL: "http://events-service:8085"
|
||||||
MAIL_SERVICE_URL: "http://mail-service:8083"
|
MAIL_SERVICE_URL: "http://mail-service:8083"
|
||||||
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
||||||
@@ -204,6 +204,8 @@ services:
|
|||||||
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}"
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}"
|
||||||
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MASTERDATA_CONSUL_PREFER_IP:-true}"
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MASTERDATA_CONSUL_PREFER_IP:-true}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME: "${MASTERDATA_SERVICE_HOSTNAME:-masterdata-service}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_HEALTH_CHECK_PATH: "/actuator/health"
|
||||||
|
|
||||||
# - DATENBANK VERBINDUNG -
|
# - DATENBANK VERBINDUNG -
|
||||||
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
|
|||||||
+37
-37
@@ -6,43 +6,43 @@ services:
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# --- WEB-APP ---
|
# --- WEB-APP ---
|
||||||
web-app:
|
# web-app:
|
||||||
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/web-app:${DOCKER_TAG:-latest}"
|
# image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/web-app:${DOCKER_TAG:-latest}"
|
||||||
build:
|
# build:
|
||||||
context: . # Wichtig: Root Context für Monorepo Zugriff
|
# context: . # Wichtig: Root Context für Monorepo Zugriff
|
||||||
dockerfile: config/docker/caddy/web-app/Dockerfile
|
# dockerfile: config/docker/caddy/web-app/Dockerfile
|
||||||
args:
|
# args:
|
||||||
# Frontend spezifisch:
|
# # Frontend spezifisch:
|
||||||
CADDY_VERSION: "${DOCKER_CADDY_VERSION:-2.11-alpine}"
|
# CADDY_VERSION: "${DOCKER_CADDY_VERSION:-2.11-alpine}"
|
||||||
# Metadaten:
|
# # Metadaten:
|
||||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
# VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
# BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||||
labels:
|
# labels:
|
||||||
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
# - "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||||
container_name: "${PROJECT_NAME:-meldestelle}-web-app"
|
# container_name: "${PROJECT_NAME:-meldestelle}-web-app"
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
ports:
|
# ports:
|
||||||
- "${WEB_APP_PORT:-4000:4000}"
|
# - "${WEB_APP_PORT:-4000:4000}"
|
||||||
environment:
|
# environment:
|
||||||
# Runtime Configuration — via envsubst in entrypoint.sh in config.json & index.html injiziert.
|
# # Runtime Configuration — via envsubst in entrypoint.sh in config.json & index.html injiziert.
|
||||||
# Muss die öffentlich erreichbare URL sein (Browser-Zugriff!), NICHT localhost.
|
# # Muss die öffentlich erreichbare URL sein (Browser-Zugriff!), NICHT localhost.
|
||||||
API_BASE_URL: "${WEB_APP_API_URL:-http://localhost:8081}"
|
# API_BASE_URL: "${WEB_APP_API_URL:-http://localhost:8081}"
|
||||||
# Keycloak Public URL (muss vom Browser aus erreichbar sein)
|
# # Keycloak Public URL (muss vom Browser aus erreichbar sein)
|
||||||
KEYCLOAK_URL: "${WEB_APP_KEYCLOAK_URL:-http://localhost:8180}"
|
# KEYCLOAK_URL: "${WEB_APP_KEYCLOAK_URL:-http://localhost:8180}"
|
||||||
depends_on:
|
# depends_on:
|
||||||
api-gateway:
|
# api-gateway:
|
||||||
condition: "service_started"
|
# condition: "service_started"
|
||||||
healthcheck:
|
# healthcheck:
|
||||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:4000/" ]
|
# test: [ "CMD", "wget", "--spider", "-q", "http://localhost:4000/" ]
|
||||||
interval: 20s
|
# interval: 20s
|
||||||
timeout: 5s
|
# timeout: 5s
|
||||||
retries: 5
|
# retries: 5
|
||||||
start_period: 20s
|
# start_period: 20s
|
||||||
networks:
|
# networks:
|
||||||
meldestelle-network:
|
# meldestelle-network:
|
||||||
aliases:
|
# aliases:
|
||||||
- "web-app"
|
# - "web-app"
|
||||||
profiles: [ "gui", "all" ]
|
# profiles: [ "gui", "all" ]
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
meldestelle-network:
|
meldestelle-network:
|
||||||
|
|||||||
+8
-2
@@ -82,13 +82,18 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: [ "infra", "all" ]
|
profiles: [ "infra", "all" ]
|
||||||
environment:
|
environment:
|
||||||
KC_BOOTSTRAP_ADMIN_USERNAME: "${KC_ADMIN_USERNAME:-kc-admin}"
|
KC_BOOTSTRAP_ADMIN_USERNAME: "${KC_BOOTSTRAP_ADMIN_USERNAME:-kc-admin}"
|
||||||
KC_BOOTSTRAP_ADMIN_PASSWORD: "${KC_ADMIN_PASSWORD:-kc-password}"
|
KC_BOOTSTRAP_ADMIN_PASSWORD: "${KC_BOOTSTRAP_ADMIN_PASSWORD:-kc-password}"
|
||||||
|
|
||||||
|
KC_FRONTEND_URL: "${KC_FRONTEND_URL:-http://localhost:8180}"
|
||||||
|
KC_PROXY_HEADERS: "${KC_PROXY_HEADERS:-xforwarded}"
|
||||||
|
|
||||||
KC_DB: "${KC_DB:-postgres}"
|
KC_DB: "${KC_DB:-postgres}"
|
||||||
KC_DB_SCHEMA: "${KC_DB_SCHEMA:-keycloak}"
|
KC_DB_SCHEMA: "${KC_DB_SCHEMA:-keycloak}"
|
||||||
KC_DB_URL: "jdbc:postgresql://postgres:5432/${POSTGRES_DB:-pg-meldestelle-db}"
|
KC_DB_URL: "jdbc:postgresql://postgres:5432/${POSTGRES_DB:-pg-meldestelle-db}"
|
||||||
KC_DB_USERNAME: "${POSTGRES_USER:-pg-user}"
|
KC_DB_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
KC_DB_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
KC_DB_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
|
|
||||||
# Hostname-Konfiguration: Für lokale Entwicklung "localhost", auf dem Server die echte IP/Domain setzen
|
# Hostname-Konfiguration: Für lokale Entwicklung "localhost", auf dem Server die echte IP/Domain setzen
|
||||||
KC_HOSTNAME: "${KC_HOSTNAME:-localhost}"
|
KC_HOSTNAME: "${KC_HOSTNAME:-localhost}"
|
||||||
# WICHTIG: false erlaubt Zugriff über beliebige Hostnamen (nötig für Server-Betrieb ohne TLS)
|
# WICHTIG: false erlaubt Zugriff über beliebige Hostnamen (nötig für Server-Betrieb ohne TLS)
|
||||||
@@ -98,6 +103,7 @@ services:
|
|||||||
KC_HTTP_ENABLED: "true"
|
KC_HTTP_ENABLED: "true"
|
||||||
# Admin-Interface explizit auf allen Interfaces binden (0.0.0.0)
|
# Admin-Interface explizit auf allen Interfaces binden (0.0.0.0)
|
||||||
KC_HTTP_MANAGEMENT_PORT: "9000"
|
KC_HTTP_MANAGEMENT_PORT: "9000"
|
||||||
|
|
||||||
KC_HEALTH_ENABLED: "true"
|
KC_HEALTH_ENABLED: "true"
|
||||||
KC_METRICS_ENABLED: "true"
|
KC_METRICS_ENABLED: "true"
|
||||||
# Integration der Power-Flags
|
# Integration der Power-Flags
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: "${PROJECT_NAME:-meldestelle}"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# --- Statische Web-App (WASM) ---
|
||||||
|
web-app:
|
||||||
|
image: ${REGISTRY_INTERNAL:-10.0.0.22:3000}/mocode-software/meldestelle/web-app:${DOCKER_TAG:-latest}
|
||||||
|
container_name: ${PROJECT_NAME:-meldestelle}-web-app
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Diese Variablen werden vom Web-Container verwendet, um die Ziel-URLs in die index.html zu injizieren
|
||||||
|
API_BASE_URL: https://api.mo-code.at
|
||||||
|
MAIL_SERVICE_URL: https://api.mo-code.at
|
||||||
|
ports:
|
||||||
|
- "${WEB_APP_PORT:-4000:4000}"
|
||||||
|
networks: [meldestelle-network]
|
||||||
|
|
||||||
|
# --- Mail-Service (Plan-B: Form -> E-Mail) ---
|
||||||
|
mail-service:
|
||||||
|
image: ${REGISTRY_INTERNAL:-10.0.0.22:3000}/mocode-software/meldestelle/mail-service:${DOCKER_TAG:-latest}
|
||||||
|
container_name: ${PROJECT_NAME:-meldestelle}-mail-service
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Server-Port im Container (Spring Boot)
|
||||||
|
SERVER_PORT: "8085"
|
||||||
|
|
||||||
|
# Plan-B: Zipkin-Fehler unterdrücken
|
||||||
|
MANAGEMENT_TRACING_ENABLED: "false"
|
||||||
|
SPRING_ZIPKIN_ENABLED: "false"
|
||||||
|
|
||||||
|
# SMTP (World4You - PROD)
|
||||||
|
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: "true"
|
||||||
|
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "true"
|
||||||
|
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED: "true"
|
||||||
|
|
||||||
|
# Feature-Flags / Infra-Off
|
||||||
|
MAIL_POLLING_ENABLED: ${MAIL_POLLING_ENABLED:-false}
|
||||||
|
SPRING_CLOUD_CONSUL_ENABLED: ${SPRING_CLOUD_CONSUL_ENABLED:-false}
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_ENABLED: ${SPRING_CLOUD_CONSUL_DISCOVERY_ENABLED:-false}
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_REGISTER: ${SPRING_CLOUD_CONSUL_DISCOVERY_REGISTER:-false}
|
||||||
|
|
||||||
|
# Datenbank: H2 In-Memory (Default in application.yaml) – KEINE Postgres-Variablen setzen
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "8092:${SERVER_PORT:-8085}" # Extern 8092 beibehalten
|
||||||
|
networks: [meldestelle-network]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
meldestelle-network:
|
||||||
|
driver: bridge
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
type: Roadmap
|
type: Roadmap
|
||||||
status: ACTIVE
|
status: ACTIVE
|
||||||
owner: Lead Architect
|
owner: Lead Architect
|
||||||
last_update: 2026-04-11
|
last_update: 2026-04-21
|
||||||
---
|
---
|
||||||
|
|
||||||
# MASTER ROADMAP: Meldestelle
|
# MASTER ROADMAP: Meldestelle
|
||||||
|
|
||||||
🏗️ **[Lead Architect]** | 11. April 2026
|
🏗️ **[Lead Architect]** | 20. April 2026
|
||||||
|
|
||||||
**Strategisches Ziel:**
|
**Strategisches Ziel:**
|
||||||
Entwicklung einer ÖTO-konformen, offline-fähigen Turnier-Meldestelle als Compose Desktop App (KMP).
|
Entwicklung einer ÖTO-konformen, offline-fähigen Turnier-Meldestelle als Compose Desktop App (KMP).
|
||||||
@@ -175,8 +175,71 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 3. Initiative: Wizard-Orchestrator & Offline-Drafts (Q2/Q3 2026)
|
||||||
|
|
||||||
|
🏗️ Verantwortlich: Lead Architect · 🎨 Frontend · 🖌️ UI/UX · 👷 Backend · 🧐 QA · 🧹 Curator
|
||||||
|
|
||||||
|
Ziel: Konsolidierung aller „Wizards“ auf ein deklaratives Orchestrierungs-Framework (Graph + Guards + Effects), vereinheitlichte Validierung und Offline-Draft-Fähigkeit inkl. Delta‑Sync. Desktop-first, tastaturbedienbar, testbar.
|
||||||
|
|
||||||
|
### 3.1 Kernbausteine
|
||||||
|
- Orchestrator Runtime & DSL: `StepId`, `WizardContext`, `WizardState`, `Guard`, `Transition`, `StepEffects`.
|
||||||
|
- WizardScaffold: Breadcrumb, Kontext-Chips, Footer mit Hotkeys (Enter/Shift+Enter/Alt+S), Fehler-Summary.
|
||||||
|
- DraftStore: Autosave pro Step (`onLeave`), Resume, `flowVersion`, Konfliktanzeige.
|
||||||
|
- DevTools: strukturierte Transition-Logs, Graph-Export (DOT/PlantUML).
|
||||||
|
|
||||||
|
Referenzen/Dokumente:
|
||||||
|
- ADR‑0025: Wizard-Orchestrator (State‑Machine, DSL, Guards, Effects) → `docs/01_Architecture/adr/0025-wizard-orchestrator-de.md`
|
||||||
|
- ADR‑0026: Step-Validation-Policy (sync vs. async, Fehlersichtbarkeit, Hotkeys) → `docs/01_Architecture/adr/0026-validation-policy-de.md`
|
||||||
|
- ADR‑0027: Draft-Domain & Delta‑Sync (Versionierung, Konfliktlösung, Idempotenz) → `docs/01_Architecture/adr/0027-draft-domain-and-delta-sync-de.md`
|
||||||
|
- Reference: Wizard‑DSL README (Beispiel-Flow Event) → `docs/01_Architecture/Reference/Wizard-DSL-README.md`
|
||||||
|
|
||||||
|
### 3.2 Migrationsstrategie (Strangler)
|
||||||
|
1) Parallelbetrieb: Neuer Orchestrator in `frontend/core/wizard`; bestehende VMs delegieren schrittweise.
|
||||||
|
2) Inkrement 1: Event‑Flow – zunächst 2 Steps (ZNS_CHECK, VERANSTALTER_SELECTION), dann alle 6 Steps.
|
||||||
|
3) Feature‑Flag `WizardRuntimeEnabled` für risikoarmen Rollout.
|
||||||
|
|
||||||
|
### 3.3 Phasenplanung (Auszug)
|
||||||
|
- Phase 1 (Core & Tooling, 2–3 Wochen): Runtime/DSL, DevLogs, Graph‑Export, Scaffold‑MVP, Unit‑Tests.
|
||||||
|
- Phase 2 (Event‑Flow, 2–3 Wochen): `EventStep/Acc/Guards`, Flow‑DSL, VM‑Delegation, Validierung, Autosave/Resume.
|
||||||
|
- Phase 3 (Backend, 2–4 Wochen): Draft-/Validate‑APIs, Offline‑Queue, Delta‑Sync für Turniere.
|
||||||
|
- Phase 4 (Skalierung, 6–10 Wochen, parallel): Weitere Flows je Bounded Context.
|
||||||
|
- Phase 5–7 (2–3 + 1–2 + 1–2 Wochen): UX‑Härtung, Observability/Rollout‑Gates, Stabilisierung & Abschaltung Altlogik.
|
||||||
|
|
||||||
|
Grobe Gesamtdauer: 17–29 Wochen je nach Parallelisierung.
|
||||||
|
|
||||||
|
### 3.4 Akzeptanzkriterien (DoD Initiative)
|
||||||
|
- Alle priorisierten Flows laufen über Orchestrator; Next/Back/History deterministisch; Graph‑Export aktuell.
|
||||||
|
- DraftStore produktiv; Resume deterministisch; Delta‑Sync idempotent; Konflikte nicht‑blockierend sichtbar.
|
||||||
|
- Validierungs‑Policy konsistent; Tastatur‑Bedienung vollständig; Performance‑Gates eingehalten.
|
||||||
|
- ADR‑0025/0026/0027 veröffentlicht; Wizard‑DSL‑Reference vorhanden; CI grün; Metriken/Alerts aktiv.
|
||||||
|
|
||||||
|
### 3.5 10‑Tage‑Startplan
|
||||||
|
- Tag 1–2: Runtime/DSL‑Skelett, Scaffold‑MVP, Feature‑Flag, README Skeleton.
|
||||||
|
- Tag 3: EventStep/Acc/Guards, EventFlow (2 Steps), VM‑Delegation minimal.
|
||||||
|
- Tag 4: Tests Runtime/Guards, Graph‑Export, Dev‑Logs.
|
||||||
|
- Tag 5–6: META_DATA/ANSPRECHPERSON migrieren, Validierungs‑API, Fehler‑Summary.
|
||||||
|
- Tag 7: DraftStore lokal (Autosave/Resume), Property‑Test Resume.
|
||||||
|
- Tag 8: TURNIER_ANLAGE einbetten, Sync via `onComplete`.
|
||||||
|
- Tag 9: SUMMARY + Finalisierung, Offload in Offline‑Queue (Stub).
|
||||||
|
- Tag 10: ADR‑0025/0026/0027 Review+Merge; Journal‑Eintrag.
|
||||||
|
|
||||||
|
Journal: `docs/99_Journal/2026-04-21_Wizard-Orchestrator_Roadmap_Anchoring.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 3. Aktuelle Phase
|
## 3. Aktuelle Phase
|
||||||
|
|
||||||
|
### Progress Checkpoint – 2026-04-21 (Wizard-Orchestrator Initiative)
|
||||||
|
|
||||||
|
- Core/DSL: angelegt in `frontend/core/wizard` (Runtime, DSL), erste Tests grün.
|
||||||
|
- UI: `WizardScaffold` (MVP) + Hotkeys-Wrapper (Enter/Shift+Enter/Alt+S) vorhanden.
|
||||||
|
- Feature-Integration: Veranstaltungs-Wizard hinter Flag teilweise delegiert.
|
||||||
|
- Drafts: In‑Memory DraftStore (Autosave/Resume Hooks) angebunden.
|
||||||
|
- DI: Koin-Parameterübergabe für `EventWizardViewModel` vereinheitlicht.
|
||||||
|
- Flag: `WizardRuntimeEnabled = false` (Standard AUS; Dev-Verprobung manuell).
|
||||||
|
|
||||||
|
Nächste Schritte (Kurz): Tests für `needsContactPerson` (beide Zweige), VM‑Delegation für weitere Steps, Footer‑Fehler‑Summary, persistenter DraftStore, Dev‑Overlay.
|
||||||
|
|
||||||
### PHASE 5: P2-Contexts & Integration ✅ ABGESCHLOSSEN
|
### PHASE 5: P2-Contexts & Integration ✅ ABGESCHLOSSEN
|
||||||
|
|
||||||
*Ziel: `competition-context` und `event-management-context` implementieren.*
|
*Ziel: `competition-context` und `event-management-context` implementieren.*
|
||||||
@@ -276,10 +339,18 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
|
|
||||||
## 4. Geplante Phasen
|
## 4. Geplante Phasen
|
||||||
|
|
||||||
### PHASE 13: Export & ZNS-Rückmeldung
|
### PHASE 13: Frontend-Modernisierung & Cleanup ✅ ABGESCHLOSSEN (20. April 2026)
|
||||||
*Ziel: Finalisierung der Turnier-Daten und Rückübermittlung an den OEPS.*
|
*Ziel: Finalisierung der Turnier-Daten, Rückübermittlung an den OEPS und architektonische Bereinigung.*
|
||||||
|
|
||||||
* [x] **Mail-Service Integration:** Online-Nennungen via REST/Mail empfangen und persistieren. ✓ (April 2026)
|
* [x] **"V2"-Bereinigung:** Vollständige Eliminierung aller "V2"-Suffixe in Dateinamen und Symbolen (z.B. `TurnierWizardV2`, `VeranstalterAuswahlV2`). ✓ (20. April 2026)
|
||||||
|
* [x] **Plug-and-Play (Turnier):** Umstellung des `turnier-feature` auf ADR-0024. Entfernung von Reflection-Zugriffen auf die Shell und Einführung von ViewModel-Hoisting. ✓ (20. April 2026)
|
||||||
|
* [x] **Plug-and-Play (Veranstalter):** Umstellung des `veranstalter-feature` auf ADR-0024. Einführung des `VeranstalterDetailViewModel` und Konsolidierung der Screens in der Desktop-Shell. ✓ (20. April 2026)
|
||||||
|
* [x] **Device-Setup ("Lock-and-Edit"):** Einführung eines Review-Modus mit Konfigurations-Sperre, Drucker-Integration und Maskierung des SharedKeys. ✓ (20. April 2026)
|
||||||
|
* [x] **Veranstaltungs-Wizard:** Implementierung eines 6-stufigen Profi-Workflows mit Sticky Preview-Card (WYSIWYG), ZNS-Guard und OEPS-Satznummer-Mapping. ✓ (20. April 2026)
|
||||||
|
* [x] **Code-Hygiene:** Beseitigung von Code-Smells, redundanten Validierungen und ungenutzten Parametern in den zentralen Frontend-Modulen. ✓ (20. April 2026)
|
||||||
|
* [x] **Connectivity-Diagnose:** Stabiles Diagnose-Tool für Backend-, DB- und Auth-Verbindung in der Desktop-App. ✓ (18. April 2026)
|
||||||
|
* [x] **WASM-Transition:** Projektweite Umstellung auf JVM (Desktop) und wasmJs (Web). Eliminierung von `js(IR)`. ✓ (18. April 2026)
|
||||||
|
* [x] **Geräte-Initialisierung:** Refactoring des Onboarding-Prozesses in das Plug-and-Play Modul `device-initialization`. ✓ (18. April 2026)
|
||||||
* [ ] **XML-Export:** Vollständiger B-Satz Export (inkl. Ergebnisse und Platzierungen).
|
* [ ] **XML-Export:** Vollständiger B-Satz Export (inkl. Ergebnisse und Platzierungen).
|
||||||
* [ ] **ZNS-Portal:** Upload-Integration in das OEPS-ZNS.
|
* [ ] **ZNS-Portal:** Upload-Integration in das OEPS-ZNS.
|
||||||
* [ ] **Archivierung:** Langzeit-Archivierung abgeschlossener Turniere.
|
* [ ] **Archivierung:** Langzeit-Archivierung abgeschlossener Turniere.
|
||||||
@@ -307,6 +378,8 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
| 15 | Masterdata: Observability & Operations | ✅ | masterdata-ops.md, CHANGELOG |
|
| 15 | Masterdata: Observability & Operations | ✅ | masterdata-ops.md, CHANGELOG |
|
||||||
| 16 | Tenant-Resolution: Schema-per-Tenant | ✅ | ADR-0021 |
|
| 16 | Tenant-Resolution: Schema-per-Tenant | ✅ | ADR-0021 |
|
||||||
| 17 | LAN-Sync-Protokoll (Lamport-Uhren, Event-Sourcing Light) | ✅ | ADR-0022 |
|
| 17 | LAN-Sync-Protokoll (Lamport-Uhren, Event-Sourcing Light) | ✅ | ADR-0022 |
|
||||||
|
| 18 | Domain-Naming: Kein `Dom`-Präfix für Entitäten | ✅ | ADR-0023 |
|
||||||
|
| 19 | Plug-and-Play Architektur für UI-Komponenten | ✅ | ADR-0024 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user