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
|
||||
NOTES*.md
|
||||
**/.junie/
|
||||
.junie/
|
||||
|
||||
# ===================================================================
|
||||
# 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
|
||||
|
||||
on:
|
||||
# Nur ausführen, wenn explizit das Desktop-Shell-Modul geändert wurde
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- 'frontend/shells/meldestelle-desktop/**'
|
||||
- '.gitea/workflows/desktop-tests.yml'
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- 'frontend/shells/meldestelle-desktop/**'
|
||||
# Manuell startbar, falls benötigt
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
desktop-tests:
|
||||
# Komplett deaktivierbar über Repo-Variable: Settings → Variables → DESKTOP_CI_ENABLED=true
|
||||
# Zusätzlich: Für 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
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -38,12 +49,12 @@ jobs:
|
||||
- name: Show Gradle version
|
||||
run: ./gradlew --version
|
||||
|
||||
- name: Run Desktop tests headless (Xvfb)
|
||||
- name: Run Desktop tests headless (xvfb)
|
||||
env:
|
||||
_JAVA_OPTIONS: -Djava.awt.headless=true
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y Xvfb
|
||||
sudo apt-get install -y xvfb xauth
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \
|
||||
./gradlew :frontend:shells:meldestelle-desktop:jvmTest --stacktrace --no-daemon
|
||||
|
||||
|
||||
@@ -33,18 +33,11 @@ jobs:
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- service: keycloak
|
||||
# Plan-B fokussiert: Nur Mail-Service + Web-App bauen/pushen (beschleunigt CI deutlich)
|
||||
- service: mail-service
|
||||
context: .
|
||||
dockerfile: config/docker/keycloak/Dockerfile
|
||||
image: keycloak
|
||||
- service: api-gateway
|
||||
context: .
|
||||
dockerfile: backend/infrastructure/gateway/Dockerfile
|
||||
image: api-gateway
|
||||
- service: ping-service
|
||||
context: .
|
||||
dockerfile: backend/services/ping/Dockerfile
|
||||
image: ping-service
|
||||
dockerfile: backend/services/mail/Dockerfile
|
||||
image: mail-service
|
||||
- service: web-app
|
||||
context: .
|
||||
dockerfile: config/docker/caddy/web-app/Dockerfile
|
||||
@@ -61,43 +54,42 @@ jobs:
|
||||
distribution: "temurin"
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
# Verhindert mysteriöse Build-Fehler durch korrupte Node/Kotlin-Caches (nur web-app relevant)
|
||||
- name: Cleanup stale build caches
|
||||
if: matrix.service == 'web-app'
|
||||
run: |
|
||||
rm -rf frontend/shells/meldestelle-portal/build/js/node_modules/.cache || true
|
||||
rm -rf frontend/shells/meldestelle-portal/build/js/.yarn/cache || true
|
||||
rm -rf ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-compiler-embeddable || true
|
||||
|
||||
- name: Build Frontend (Kotlin JS)
|
||||
# --- SCHRITT 1: Build mit radikalem Clean (gegen die März-Leichen) ---
|
||||
- name: Build Frontend (Wasm JS)
|
||||
if: matrix.service == 'web-app'
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution \
|
||||
# Löscht alte Build-Stände komplett
|
||||
./gradlew :frontend:shells:meldestelle-web:clean
|
||||
|
||||
./gradlew :frontend:shells:meldestelle-web:wasmJsBrowserDistribution \
|
||||
-Pproduction=true \
|
||||
--max-workers=4 \
|
||||
-Dkotlin.daemon.jvm.options="-Xmx4g"
|
||||
|
||||
# Pangolin-Bypass: Credentials direkt in config.json schreiben.
|
||||
# Kein "docker login" → kein Daemon-Ping → kein HTTPS-Fehler.
|
||||
# BuildKit liest ~/.docker/config.json und verwendet diese Credentials beim Push.
|
||||
# - name: Registry-Credentials konfigurieren (kein Daemon-Kontakt)
|
||||
# run: |
|
||||
# mkdir -p ~/.docker
|
||||
# AUTH=$(echo -n "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" | base64 -w 0)
|
||||
# printf '{"auths":{"%s":{"auth":"%s"}}}\n' "${{ env.REGISTRY_INTERNAL }}" "${AUTH}" > ~/.docker/config.json
|
||||
# echo "✓ Credentials für ${{ env.REGISTRY_INTERNAL }} gespeichert"
|
||||
# --- SCHRITT 2: Staging ohne rsync (Fix für dein Log-Fehler) ---
|
||||
- name: Stage Web Assets for Docker build
|
||||
if: matrix.service == 'web-app'
|
||||
run: |
|
||||
set -e
|
||||
DIST_DIR="frontend/shells/meldestelle-web/build/dist/wasmJs/productionExecutable"
|
||||
TARGET_DIR="config/docker/caddy/web-app/_site"
|
||||
|
||||
if [ ! -d "$DIST_DIR" ]; then
|
||||
echo "❌ Fehler: Build-Verzeichnis nicht gefunden!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ersetzt rsync durch sicheres Löschen & Kopieren
|
||||
rm -rf "$TARGET_DIR"
|
||||
mkdir -p "$TARGET_DIR"
|
||||
cp -r "$DIST_DIR"/. "$TARGET_DIR/"
|
||||
# Kopiere Turnier-Ausschreibungen (PDFs) für Plan-B
|
||||
cp docs/Neumarkt2026/*.pdf "$TARGET_DIR/" || true
|
||||
|
||||
echo "✓ Assets für Docker vorbereitet (Stand: $(date))"
|
||||
|
||||
# --- SCHRITT 3: Login & BuildX ---
|
||||
# NEU (sauber, nach daemon.json-Fix):
|
||||
- name: Login to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -122,7 +114,7 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.REGISTRY_INTERNAL }}/${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=raw,value=latest
|
||||
type=sha,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
@@ -137,9 +129,5 @@ jobs:
|
||||
provenance: false
|
||||
sbom: false
|
||||
build-args: |
|
||||
DOCKER_BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
BUILD_DATE=${{ github.event.head_commit.timestamp || 'unknown' }}
|
||||
VERSION=${{ github.sha }}
|
||||
GRADLE_VERSION=${{ env.GRADLE_VERSION }}
|
||||
JAVA_VERSION=${{ env.JAVA_VERSION }}
|
||||
KEYCLOAK_IMAGE_TAG=${{ env.KEYCLOAK_IMAGE_TAG }}
|
||||
JVM_OPTS_APPEND=${{ env.JVM_OPTS_ARM64 }}
|
||||
|
||||
@@ -4,6 +4,8 @@ on:
|
||||
branches: [ "**" ]
|
||||
jobs:
|
||||
no-hardcoded-versions:
|
||||
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
|
||||
if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -22,6 +22,8 @@ jobs:
|
||||
# =============================================================
|
||||
tag-release:
|
||||
name: 🏷️ Git-Tag setzen
|
||||
# Für Plan-B-Builds überspringen: Commit-Message enthält [planb]
|
||||
if: ${{ !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.read-version.outputs.version }}
|
||||
@@ -62,7 +64,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Git-Tag erstellen & pushen
|
||||
if: steps.check-tag.outputs.already_tagged == 'false' && github.event.inputs.dry_run != 'true'
|
||||
if: steps.check-tag.outputs.already_tagged == 'false' && gitea.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
TAG="${{ steps.read-version.outputs.tag }}"
|
||||
VERSION="${{ steps.read-version.outputs.version }}"
|
||||
@@ -77,6 +79,8 @@ jobs:
|
||||
# =============================================================
|
||||
package-linux:
|
||||
name: 📦 Linux .deb Packaging
|
||||
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit
|
||||
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: tag-release
|
||||
|
||||
@@ -84,11 +88,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 21 (Temurin)
|
||||
- name: Setup JDK 25 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Gradle cache
|
||||
uses: actions/cache@v4
|
||||
@@ -123,6 +127,8 @@ jobs:
|
||||
# =============================================================
|
||||
package-windows:
|
||||
name: 📦 Windows .msi Packaging
|
||||
# Nur ausführen, wenn Desktop-CI explizit aktiviert ist UND kein Plan‑B Commit
|
||||
if: ${{ vars.DESKTOP_CI_ENABLED == 'true' && !contains(gitea.event.head_commit.message, '[planb]') }}
|
||||
runs-on: windows-latest
|
||||
needs: tag-release
|
||||
|
||||
@@ -130,11 +136,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup JDK 21 (Temurin)
|
||||
- name: Setup JDK 25 (Temurin)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
java-version: '25'
|
||||
|
||||
- name: Gradle cache
|
||||
uses: actions/cache@v4
|
||||
@@ -173,11 +179,11 @@ jobs:
|
||||
steps:
|
||||
- name: Summary ausgeben
|
||||
run: |
|
||||
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Artefakt | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## 🚀 Release ${{ needs.tag-release.outputs.version }}" >> $GITEA_STEP_SUMMARY
|
||||
echo "" >> $GITEA_STEP_SUMMARY
|
||||
echo "| Artefakt | Status |" >> $GITEA_STEP_SUMMARY
|
||||
echo "|----------|--------|" >> $GITEA_STEP_SUMMARY
|
||||
echo "| Linux .deb | ${{ needs.package-linux.result }} |" >> $GITEA_STEP_SUMMARY
|
||||
echo "| Windows .msi | ${{ needs.package-windows.result }} |" >> $GITEA_STEP_SUMMARY
|
||||
echo "" >> $GITEA_STEP_SUMMARY
|
||||
echo "**Git-Tag:** \`${{ needs.tag-release.outputs.tag }}\`" >> $GITEA_STEP_SUMMARY
|
||||
|
||||
+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
|
||||
set -euo pipefail
|
||||
|
||||
# check-docs-drift.sh
|
||||
# Zweck: sehr schlanke Drift-Checks gegen die neue Doku-Struktur.
|
||||
# - Kein Guidelines-System mehr.
|
||||
# - Single Source of Truth: `docs/`
|
||||
|
||||
err=0
|
||||
|
||||
has() { grep -q "$2" "$1" || { echo "[DRIFT] '$2' fehlt in $1"; err=1; }; }
|
||||
miss() { grep -q "$2" "$1" && { echo "[DRIFT] Veralteter Begriff '$2' in $1"; err=1; }; }
|
||||
|
||||
# Harte Altlast-Pfade dürfen nicht mehr vorkommen
|
||||
if git grep -n "docs/00_Domain/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/00_Domain/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/adr/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/adr/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/c4/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/c4/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/how-to/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/how-to/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
if git grep -n "docs/reference/" -- docs >/dev/null 2>&1; then
|
||||
echo "[DRIFT] Veralteter Pfad 'docs/reference/' in docs/* gefunden"
|
||||
err=1
|
||||
fi
|
||||
|
||||
# Quelle der Wahrheit: Gateway-Technologie (sollte in Architektur/ADRs/C4 konsistent sein)
|
||||
has docs/01_Architecture/ARCHITECTURE.md "Spring Cloud Gateway"
|
||||
has docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Spring Cloud Gateway"
|
||||
miss docs/01_Architecture/adr/0007-api-gateway-pattern-de.md "Ktor"
|
||||
has docs/01_Architecture/c4/02-container-de.puml "Spring Cloud Gateway"
|
||||
miss docs/01_Architecture/c4/02-container-de.puml "Ktor"
|
||||
|
||||
exit $err
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/check-docs-drift.sh" "$@"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p build/diagrams
|
||||
shopt -s nullglob
|
||||
for f in docs/architecture/c4/*.puml; do
|
||||
docker run --rm -v "$PWD":/data plantuml/plantuml -tsvg "/data/$f" -o "/data/build/diagrams"
|
||||
echo "Rendered build/diagrams/$(basename "${f%.puml}").svg"
|
||||
done
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/render-plantuml.sh" "$@"
|
||||
|
||||
@@ -1,136 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# validate-links.sh - Link-Validierung für Projektdokumentation (`docs/**`).
|
||||
# Zweck: Guardrail für die "Docs-as-Code"-Strategie.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
QUICK_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--quick)
|
||||
QUICK_MODE=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
cat << 'EOF'
|
||||
Docs Link-Validierung
|
||||
|
||||
USAGE:
|
||||
./.junie/scripts/validate-links.sh [--quick]
|
||||
|
||||
BESCHREIBUNG:
|
||||
Prüft Markdown-Links in `docs/**/*.md` auf gebrochene relative Pfade.
|
||||
Ignoriert externe Links (http/https/mailto) sowie reine Anchors (#...).
|
||||
|
||||
OPTIONEN:
|
||||
--quick Führt nur eine Teilmenge der Prüfungen durch (aktuell nicht implementiert).
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unbekannter Parameter: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
root = Path.cwd()
|
||||
docs_dir = root / "docs"
|
||||
|
||||
if not docs_dir.is_dir():
|
||||
print(f"[ERROR] docs-Verzeichnis nicht gefunden: {docs_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Veraltete Pfad-Prüfungen wurden entfernt, da sie zu wartungsintensiv waren.
|
||||
# Das Skript konzentriert sich nun auf die Validierung der Link-Integrität.
|
||||
FORBIDDEN_SUBSTRINGS = []
|
||||
|
||||
md_files = sorted(docs_dir.rglob("*.md"))
|
||||
|
||||
link_pattern = re.compile(r"\]\(([^)]+)\)")
|
||||
|
||||
errors = 0
|
||||
|
||||
def is_external(target: str) -> bool:
|
||||
t = target.lower()
|
||||
return t.startswith("http://") or t.startswith("https://") or t.startswith("mailto:")
|
||||
|
||||
def strip_fragment_and_query(target: str) -> str:
|
||||
# remove fragment and query parts
|
||||
target = target.split("#", 1)[0]
|
||||
target = target.split("?", 1)[0]
|
||||
return target
|
||||
|
||||
for f in md_files:
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
for forbidden in FORBIDDEN_SUBSTRINGS:
|
||||
if forbidden in text:
|
||||
print(f"[ERROR] Veralteter Pfad '{forbidden}' in {f}")
|
||||
errors += 1
|
||||
|
||||
for match in link_pattern.finditer(text):
|
||||
target = match.group(1).strip()
|
||||
|
||||
if not target:
|
||||
continue
|
||||
if is_external(target):
|
||||
continue
|
||||
if target.startswith("#"):
|
||||
continue
|
||||
|
||||
# drop angle brackets <...> used in markdown for urls with spaces
|
||||
if target.startswith("<") and target.endswith(">"):
|
||||
target = target[1:-1]
|
||||
|
||||
target = unquote(strip_fragment_and_query(target))
|
||||
|
||||
# ignore absolute paths in the repo (we treat them as doc-style links; validate only if relative)
|
||||
if target.startswith("/"):
|
||||
continue
|
||||
|
||||
# ignore non-file targets (e.g. empty or protocol-less anchors)
|
||||
if ":" in target.split("/", 1)[0]:
|
||||
# things like "vscode:..." etc.
|
||||
continue
|
||||
|
||||
# treat as file path relative to markdown file
|
||||
resolved = (f.parent / target).resolve()
|
||||
|
||||
# keep validation within repo
|
||||
try:
|
||||
resolved.relative_to(root.resolve())
|
||||
except ValueError:
|
||||
print(f"[ERROR] Link zeigt außerhalb des Repos: {f} -> {target}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
# allow directories if they contain README.md
|
||||
if resolved.is_dir():
|
||||
if not (resolved / "README.md").is_file():
|
||||
print(f"[ERROR] Verlinktes Verzeichnis ohne README.md: {f} -> {target}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
if not resolved.exists():
|
||||
print(f"[ERROR] Broken link: {f} -> {target}")
|
||||
errors += 1
|
||||
|
||||
if errors:
|
||||
print(f"[ERROR] Link-Validierung fehlgeschlagen: {errors} Fehler")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[OK] Link-Validierung erfolgreich: {len(md_files)} Markdown-Dateien geprüft")
|
||||
PY
|
||||
# Shim: Weiterleitung auf zentrale Guardrail in .ai/
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
|
||||
ROOT_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
||||
exec "$ROOT_DIR/.ai/scripts/validate-links.sh" "$@"
|
||||
|
||||
@@ -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
|
||||
|
||||
- **Frontend (Desktop):** Behebung von Kompilierungsfehlern in `ScreenPreviews.kt` durch Implementierung der fehlenden
|
||||
`getStats()` Methode in den `MasterdataRepository`-Mocks.
|
||||
- **Identity-Modul:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen und Behebung von
|
||||
Persistenz-Konflikten im `ExposedDeviceRepository`.
|
||||
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
|
||||
|
||||
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.web.server.ServerHttpSecurity
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
|
||||
import org.springframework.security.oauth2.jwt.*
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
@@ -38,7 +37,6 @@ class SecurityConfig(
|
||||
.authorizeExchange { exchanges ->
|
||||
exchanges
|
||||
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll()
|
||||
.pathMatchers("/api/ping/**").permitAll() // TEMPORAER fuer Debugging
|
||||
.pathMatchers("/api/v1/import/zns", "/api/v1/import/zns/**").permitAll() // TEMPORAER fuer Debugging
|
||||
.anyExchange().authenticated()
|
||||
}
|
||||
@@ -67,16 +65,28 @@ class SecurityConfig(
|
||||
if (delegate == null) {
|
||||
if (jwkSetUri.isBlank()) {
|
||||
logger.error("JWK Set URI is missing – all authenticated requests will be rejected.")
|
||||
return Mono.error(org.springframework.security.oauth2.jwt.BadJwtException("Identity Provider not configured"))
|
||||
return Mono.error(BadJwtException("Identity Provider not configured"))
|
||||
}
|
||||
try {
|
||||
logger.info("Attempting to initialize JWT Decoder with URI: {}", jwkSetUri)
|
||||
delegate = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
logger.info("JWT Decoder successfully initialized.")
|
||||
// Wir deaktivieren die Issuer-Validierung, da Keycloak intern "keycloak:8080"
|
||||
// und extern "localhost:8180" verwendet, was zu Mismatches führt.
|
||||
val nimbusDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
nimbusDecoder.setJwtValidator(JwtValidators.createDefault()) // Standard-Validierung (ohne Issuer-Zwang falls nicht explizit konfiguriert)
|
||||
|
||||
// Da createDefault() den Issuer-Check einbaut, wenn spring.security.oauth2.resourceserver.jwt.issuer-uri gesetzt ist,
|
||||
// nutzen wir einen Custom Validator der den Issuer ignoriert oder flexibel ist.
|
||||
val withAudience = DelegatingOAuth2TokenValidator<Jwt>(
|
||||
JwtTimestampValidator(),
|
||||
// Hier koennte man weitere Validatoren hinzufuegen, aber wir lassen den Issuer weg
|
||||
)
|
||||
nimbusDecoder.setJwtValidator(withAudience)
|
||||
|
||||
delegate = nimbusDecoder
|
||||
logger.info("JWT Decoder successfully initialized (Issuer check disabled for environment flexibility).")
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not initialize JWT Decoder: {}", e.message)
|
||||
// Throw BadJwtException so Spring Security returns 401, not 500 or passthrough
|
||||
return Mono.error(org.springframework.security.oauth2.jwt.BadJwtException("Identity Provider unavailable: ${e.message}"))
|
||||
return Mono.error(BadJwtException("Identity Provider unavailable: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +117,7 @@ class SecurityConfig(
|
||||
val configuration = CorsConfiguration().apply {
|
||||
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
|
||||
allowedMethods = securityProperties.cors.allowedMethods.toList()
|
||||
allowedHeaders = securityProperties.cors.allowedHeaders.toList()
|
||||
allowedHeaders = listOf("*") // Alles erlauben fuer Postman/Frontend
|
||||
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
|
||||
allowCredentials = securityProperties.cors.allowCredentials
|
||||
maxAge = securityProperties.cors.maxAge.seconds
|
||||
|
||||
@@ -44,6 +44,27 @@ spring:
|
||||
issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI:http://localhost:8180/realms/meldestelle}
|
||||
jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI:http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs}
|
||||
|
||||
gateway:
|
||||
security:
|
||||
cors:
|
||||
allowed-origin-patterns:
|
||||
- "http://localhost:*"
|
||||
- "https://*.meldestelle.at"
|
||||
- "https://*.mo-code.at"
|
||||
- "https://*.postman.co"
|
||||
- "postman://*"
|
||||
allowed-methods:
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "DELETE"
|
||||
- "OPTIONS"
|
||||
- "PATCH"
|
||||
allowed-headers:
|
||||
- "*"
|
||||
allow-credentials: true
|
||||
max-age: 3600s
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
|
||||
+95
-1
@@ -8,7 +8,7 @@
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://localhost:8080",
|
||||
"value": "http://localhost:8081",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
@@ -221,6 +221,100 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Connectivity Context (Ping Service)",
|
||||
"item": [
|
||||
{
|
||||
"name": "Simple Ping",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/ping/simple",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "ping", "simple"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Health Check",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/ping/health",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "ping", "health"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Public Info",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/ping/public",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "ping", "public"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Enhanced Ping (Resilience)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/ping/enhanced",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "ping", "enhanced"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Sync Delta Diagnostic",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/ping/sync?lastSyncTimestamp=0",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "ping", "sync"],
|
||||
"query": [
|
||||
{
|
||||
"key": "lastSyncTimestamp",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Secure Ping (Login Required)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/ping/secure",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "ping", "secure"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Master Data Context",
|
||||
"item": [
|
||||
|
||||
+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.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.CorsConfigurationSource
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -19,19 +27,18 @@ class GlobalSecurityConfig {
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
.csrf { it.disable() } // CSRF nicht nötig für Stateless REST APIs
|
||||
// WICHTIG: CORS explizit deaktivieren!
|
||||
// Das API-Gateway kümmert sich um CORS. Die Microservices dürfen KEINE
|
||||
// Access-Control-Allow-Origin Header setzen, sonst haben wir doppelte Header beim Client.
|
||||
.cors { it.disable() }
|
||||
// WICHTIG: CORS wieder aktivieren für Plan-B (Direktzugriff ohne Gateway möglich)
|
||||
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||
.addFilterBefore(DeviceSecurityFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
||||
.authorizeHttpRequests { auth ->
|
||||
// Explizite Freigaben (Health, Info, Public Endpoints)
|
||||
// Explizite Freigaben (Health, Information, Public-Endpoints)
|
||||
auth.requestMatchers("/actuator/**").permitAll()
|
||||
auth.requestMatchers("/api/v1/devices/register").permitAll() // Onboarding erlauben
|
||||
auth.requestMatchers("/api/mail/nennung").permitAll() // Plan-B Nennungen erlauben
|
||||
auth.requestMatchers("/api/mail/nennungen").authenticated() // Liste schützen
|
||||
auth.requestMatchers("/ping/public").permitAll()
|
||||
auth.requestMatchers("/ping/simple").permitAll()
|
||||
auth.requestMatchers("/ping/enhanced").permitAll()
|
||||
auth.requestMatchers("/ping/health").permitAll()
|
||||
auth.requestMatchers("/error").permitAll()
|
||||
|
||||
@@ -41,16 +48,48 @@ class GlobalSecurityConfig {
|
||||
.oauth2ResourceServer { oauth2 ->
|
||||
oauth2.jwt { jwt ->
|
||||
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
||||
// Auch hier den Issuer-Check entspannen, da der Service intern validiert
|
||||
jwt.decoder(jwtDecoder())
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder {
|
||||
// 1. Suche in System-Properties (Spring injects these)
|
||||
// 2. Suche in Environment Variables
|
||||
// 3. Fallback auf localhost (IDE-Start) oder keycloak (Docker-Start)
|
||||
val jwkSetUri = System.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
|
||||
?: System.getenv("SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI")
|
||||
?: "http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs"
|
||||
|
||||
val decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
val validator = DelegatingOAuth2TokenValidator<Jwt>(JwtTimestampValidator())
|
||||
decoder.setJwtValidator(validator)
|
||||
return decoder
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
|
||||
val converter = JwtAuthenticationConverter()
|
||||
converter.setJwtGrantedAuthoritiesConverter(KeycloakRoleConverter())
|
||||
return converter
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
val configuration = CorsConfiguration()
|
||||
configuration.allowedOrigins = listOf("*")
|
||||
configuration.allowedOriginPatterns = listOf("*")
|
||||
configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD")
|
||||
configuration.allowedHeaders = listOf("*")
|
||||
configuration.exposedHeaders = listOf("*")
|
||||
configuration.maxAge = 3600L
|
||||
configuration.allowCredentials = false
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
source.registerCorsConfiguration("/**", configuration)
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
spring:
|
||||
application:
|
||||
name: billing-service
|
||||
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
cloud:
|
||||
consul:
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
@@ -15,13 +18,19 @@ spring:
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
health-check-port: 8089
|
||||
# health-check-port: 8089
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
port: ${billing.http.port:8089}
|
||||
|
||||
server:
|
||||
port: 8089
|
||||
|
||||
billing:
|
||||
http:
|
||||
port: 8089 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
||||
address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
@@ -30,3 +39,12 @@ management:
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
# at.mocode.billing: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
@@ -13,10 +13,6 @@ version = "1.0.0"
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ plugins {
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ dependencies {
|
||||
// Common service extras
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.mail)
|
||||
implementation(libs.spring.boot.starter.actuator)
|
||||
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on the classpath
|
||||
//implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
||||
@@ -10,10 +10,6 @@ plugins {
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ dependencies {
|
||||
|
||||
// Spring Boot Starters
|
||||
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.actuator)
|
||||
implementation(libs.spring.boot.starter.mail)
|
||||
|
||||
+3
-1
@@ -10,9 +10,9 @@ import jakarta.mail.Session
|
||||
import jakarta.mail.internet.InternetAddress
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
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.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.util.*
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
@@ -27,6 +28,7 @@ import kotlin.uuid.Uuid
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Service
|
||||
@EnableScheduling
|
||||
@ConditionalOnProperty(value = ["mail.polling.enabled"], havingValue = "true", matchIfMissing = false)
|
||||
class MailPollingService(
|
||||
private val mailSender: JavaMailSender,
|
||||
private val nennungRepository: NennungRepository,
|
||||
|
||||
+28
-1
@@ -4,22 +4,49 @@ import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.event.EventListener
|
||||
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) {
|
||||
|
||||
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)
|
||||
fun onApplicationReady() {
|
||||
val springPort = env.getProperty("server.port", "8083")
|
||||
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("Application '{}' is running!", appName)
|
||||
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("----------------------------------------------------------")
|
||||
}
|
||||
|
||||
-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)
|
||||
@RestController
|
||||
@RequestMapping("/api/mail")
|
||||
@CrossOrigin(origins = ["http://localhost:8080", "https://nennung.mo-code.at"]) // Für Wasm-Web-App (Compose HTML/Wasm)
|
||||
class MailController(
|
||||
private val nennungRepository: NennungRepository,
|
||||
private val mailSender: JavaMailSender
|
||||
@@ -50,7 +49,7 @@ class MailController(
|
||||
private lateinit var baseMailAddress: String
|
||||
|
||||
@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}")
|
||||
|
||||
val entity = NennungEntity(
|
||||
@@ -71,19 +70,45 @@ class MailController(
|
||||
nennungRepository.save(entity)
|
||||
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 {
|
||||
val message = SimpleMailMessage()
|
||||
|
||||
// Dynamische Absenderadresse mit Plus-Addressing (z.B. online-nennen+26128@mo-code.at)
|
||||
val dynamicFrom = try {
|
||||
val (user, domain) = baseMailAddress.split("@")
|
||||
"$user+${request.turnierNr}@$domain"
|
||||
} catch (_: Exception) {
|
||||
baseMailAddress
|
||||
}
|
||||
|
||||
message.from = dynamicFrom
|
||||
// PLAN B Fallback: Kein Plus-Addressing, da World4You es nicht unterstützt
|
||||
// Wir verwenden als Absender einfach die Basis-Adresse
|
||||
message.from = baseMailAddress
|
||||
message.setTo(request.email)
|
||||
message.subject = "Bestätigung: Ihre Online-Nennung für Turnier ${request.turnierNr}"
|
||||
message.text = """
|
||||
@@ -103,12 +128,57 @@ class MailController(
|
||||
mailSender.send(message)
|
||||
logger.info("Bestätigungs-Mail an ${request.email} gesendet.")
|
||||
} 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")
|
||||
fun getAllNennungen(): List<NennungEntity> {
|
||||
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.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@@ -12,23 +12,28 @@ spring:
|
||||
show-sql: true
|
||||
mail:
|
||||
host: ${SPRING_MAIL_HOST:smtp.world4you.com}
|
||||
port: ${SPRING_MAIL_PORT:587}
|
||||
port: 587
|
||||
username: ${SPRING_MAIL_USERNAME:online-nennen@mo-code.at}
|
||||
password: ${SPRING_MAIL_PASSWORD:}
|
||||
password: ${SPRING_MAIL_PASSWORD:Mogi#2reiten}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH:true}
|
||||
connectiontimeout: 5000
|
||||
timeout: 5000
|
||||
writetimeout: 5000
|
||||
starttls:
|
||||
enable: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||
required: ${SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_REQUIRED:true}
|
||||
|
||||
cloud:
|
||||
consul:
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
enabled: ${SPRING_CLOUD_CONSUL_ENABLED:false}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
enabled: ${SPRING_CLOUD_CONSUL_DISCOVERY_ENABLED:false}
|
||||
register: ${SPRING_CLOUD_CONSUL_DISCOVERY_REGISTER:false}
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
@@ -43,4 +48,14 @@ management:
|
||||
endpoints:
|
||||
web:
|
||||
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
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS --max-time 2 http://localhost:8086/actuator/health/readiness || exit 1
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \
|
||||
CMD curl -fsS --max-time 5 http://localhost:${SERVER_PORT:-8086}/actuator/health/readiness || exit 1
|
||||
|
||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||
-XX:+UseG1GC \
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
|
||||
* GET /funktionaer — Alle Funktionäre (paginiert).
|
||||
*/
|
||||
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 results = funktionaerRepository.findAll(limit, offset)
|
||||
|
||||
+5
-5
@@ -62,11 +62,11 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||
route("/horse") {
|
||||
|
||||
/**
|
||||
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang.
|
||||
* GET /horse — alle Pferde (paginiert), optional gefiltert nach Jahrgang.
|
||||
*/
|
||||
get {
|
||||
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 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") {
|
||||
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}") {
|
||||
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 {
|
||||
val req = call.receive<HorseCreateRequest>()
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
|
||||
* GET /reiter — Alle Reiter (paginiert).
|
||||
*/
|
||||
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 results = reiterRepository.findAll(limit, offset)
|
||||
|
||||
+5
-5
@@ -76,11 +76,11 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||
route("/verein") {
|
||||
|
||||
/**
|
||||
* GET /verein — Alle Vereine (paginiert), optional gefiltert nach verband/bundesland.
|
||||
* GET /verein — alle Vereine (paginiert), optional gefiltert nach Verband/Bundesland.
|
||||
*/
|
||||
get {
|
||||
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 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") {
|
||||
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}") {
|
||||
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 {
|
||||
val req = call.receive<VereinCreateRequest>()
|
||||
|
||||
@@ -10,10 +10,6 @@ plugins {
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
@@ -1,51 +1,61 @@
|
||||
server:
|
||||
port: ${MASTERDATA_SERVER_PORT:8086}
|
||||
|
||||
ktor:
|
||||
port: ${MASTERDATA_KTOR_PORT:8091}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: masterdata-service
|
||||
main:
|
||||
banner-mode: "off"
|
||||
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
|
||||
cloud:
|
||||
consul:
|
||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
enabled: true
|
||||
register: true
|
||||
prefer-ip-address: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 20s
|
||||
health-check-timeout: 10s
|
||||
# deregister-critical-service-after: 5m
|
||||
# health-check-port: 8086 # Spring Boot Management Port (Actuator)
|
||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||
health-check-path: /actuator/health/readiness
|
||||
health-check-interval: 10s
|
||||
health-check-timeout: 5s
|
||||
health-check-port: 8086
|
||||
health-check-critical-timeout: 2m
|
||||
deregister-critical-service-after: 5m
|
||||
instance-id: ${spring.application.name}:${random.uuid}
|
||||
service-name: ${spring.application.name}
|
||||
port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel)
|
||||
port: 8091
|
||||
|
||||
server:
|
||||
port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
||||
address: 0.0.0.0 # Erreichbar für Consul Health Checks
|
||||
#server:
|
||||
# port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
||||
# address: 0.0.0.0 # Erreichbar für Consul Health Checks
|
||||
|
||||
masterdata:
|
||||
http:
|
||||
port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
||||
address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
||||
#masterdata:
|
||||
# http:
|
||||
# port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
||||
# address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "health,info,metrics,prometheus"
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
probes:
|
||||
enabled: true
|
||||
prometheus:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
plugins {
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
@@ -37,8 +37,7 @@ dependencies {
|
||||
implementation(libs.bundles.database.complete)
|
||||
|
||||
// === Resilience ===
|
||||
implementation(libs.resilience4j.spring.boot3)
|
||||
implementation(libs.resilience4j.reactor)
|
||||
implementation(libs.bundles.resilience)
|
||||
implementation(libs.spring.boot.starter.aop)
|
||||
|
||||
// === Testing ===
|
||||
|
||||
+17
-8
@@ -2,6 +2,7 @@ package at.mocode.ping.infrastructure.web
|
||||
|
||||
import at.mocode.ping.api.*
|
||||
import at.mocode.ping.application.PingUseCase
|
||||
import at.mocode.ping.domain.Ping
|
||||
import at.mocode.ping.infrastructure.PingProperties
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -20,7 +21,7 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||
*/
|
||||
@RestController
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class PingController(
|
||||
open class PingController(
|
||||
private val pingUseCase: PingUseCase,
|
||||
private val properties: PingProperties
|
||||
) : PingApi {
|
||||
@@ -43,10 +44,16 @@ class PingController(
|
||||
override suspend fun enhancedPing(
|
||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||
): EnhancedPingResponse {
|
||||
logger.info("Enhanced ping requested, simulate: {}", simulate)
|
||||
val start = System.nanoTime()
|
||||
|
||||
if (simulate && Random.nextDouble() < 0.6) {
|
||||
throw RuntimeException("Simulated service failure")
|
||||
if (simulate) {
|
||||
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")
|
||||
@@ -61,6 +68,8 @@ class PingController(
|
||||
)
|
||||
}
|
||||
|
||||
class SimulatedException(message: String) : RuntimeException(message)
|
||||
|
||||
// Neue Endpunkte
|
||||
|
||||
@GetMapping("/ping/public")
|
||||
@@ -70,7 +79,7 @@ class PingController(
|
||||
}
|
||||
|
||||
@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 {
|
||||
val domainPing = pingUseCase.executePing("Secure Ping")
|
||||
return createResponse(domainPing, "secure-pong")
|
||||
@@ -79,7 +88,7 @@ class PingController(
|
||||
@GetMapping("/ping/sync")
|
||||
override suspend fun syncPings(
|
||||
// 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> {
|
||||
return pingUseCase.getPingsSince(since).map {
|
||||
PingEvent(
|
||||
@@ -91,7 +100,7 @@ class PingController(
|
||||
}
|
||||
|
||||
// Helper
|
||||
private fun createResponse(domainPing: at.mocode.ping.domain.Ping, status: String) = PingResponse(
|
||||
private fun createResponse(domainPing: Ping, status: String) = PingResponse(
|
||||
status = status,
|
||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||
service = properties.serviceName
|
||||
@@ -99,8 +108,8 @@ class PingController(
|
||||
|
||||
// Fallback
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
||||
open fun fallbackPing(simulate: Boolean, ex: Throwable): EnhancedPingResponse {
|
||||
logger.error("CIRCUIT BREAKER FALLBACK TRIGGERED! Reason: {}", ex.message, ex)
|
||||
return EnhancedPingResponse(
|
||||
status = "fallback",
|
||||
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],
|
||||
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])
|
||||
@ActiveProfiles("test")
|
||||
@Import(PingControllerTest.PingControllerTestConfig::class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class PingControllerTest {
|
||||
open class PingControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
@@ -125,11 +130,24 @@ class PingControllerTest {
|
||||
|
||||
// Then
|
||||
val json = objectMapper.readTree(result.response.contentAsString)
|
||||
assertThat(json["status"].asText()).isEqualTo("pong")
|
||||
assertThat(json["service"].asText()).isEqualTo(properties.serviceName)
|
||||
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
|
||||
fun `should return health check response with status up`() {
|
||||
// When
|
||||
@@ -159,7 +177,7 @@ class PingControllerTest {
|
||||
)
|
||||
|
||||
// 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())
|
||||
.andReturn()
|
||||
|
||||
@@ -183,7 +201,7 @@ class PingControllerTest {
|
||||
every { pingUseCase.getPingsSince(timestamp) } returns emptyList()
|
||||
|
||||
// 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())
|
||||
.andReturn()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
metrics
|
||||
}
|
||||
|
||||
:4000 {
|
||||
:80 {
|
||||
root * /usr/share/caddy
|
||||
log {
|
||||
output stdout
|
||||
@@ -17,14 +17,56 @@
|
||||
|
||||
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/* {
|
||||
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 {
|
||||
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 {
|
||||
try_files {path} /index.html
|
||||
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
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Copy Pre-built Static Assets from Host
|
||||
# NOTE: You must run `./gradlew :frontend:shells:meldestelle-portal:jsBrowserDistribution -Pproduction=true` locally first!
|
||||
COPY frontend/shells/meldestelle-portal/build/dist/js/productionExecutable/ /usr/share/caddy/
|
||||
# Copy Pre-built Static Assets from Host (WasmJs)
|
||||
# NOTE: BUILD_DATE wird hier genutzt, um den Layer-Cache zu invalidieren,
|
||||
# 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
|
||||
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
|
||||
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 \
|
||||
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"]
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"apiBaseUrl": "${API_BASE_URL}",
|
||||
"mailServiceUrl": "${MAIL_SERVICE_URL}",
|
||||
"keycloakUrl": "${KEYCLOAK_URL}"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#!/bin/sh
|
||||
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.
|
||||
envsubst '${API_BASE_URL} ${KEYCLOAK_URL}' \
|
||||
< /usr/share/caddy/index.html.tmpl \
|
||||
# Wir fügen zusätzlich einen Cache-Buster (Zeitstempel) an den Script-Tag in der index.html an
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -13,15 +13,6 @@ version = "1.0.0"
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
binaries.library()
|
||||
browser {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
@@ -10,17 +12,7 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
js(IR) {
|
||||
binaries.library()
|
||||
// Re-enabled browser environment after Root NodeJs fix
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
binaries.library()
|
||||
browser()
|
||||
@@ -35,17 +27,10 @@ kotlin {
|
||||
commonMain.dependencies {
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.datetime)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
|
||||
jsTest.dependencies {
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
@@ -5,15 +7,7 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js {
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wasm support enabled?
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
@OptIn(ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
@@ -10,15 +10,6 @@ plugins {
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
js(IR) {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wasmJs {
|
||||
binaries.library()
|
||||
browser {
|
||||
|
||||
+3
-1
@@ -59,7 +59,7 @@ services:
|
||||
|
||||
# --- SERVICE URLs ---
|
||||
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"
|
||||
MAIL_SERVICE_URL: "http://mail-service:8083"
|
||||
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_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}"
|
||||
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 -
|
||||
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||
|
||||
+37
-37
@@ -6,43 +6,43 @@ services:
|
||||
# ==========================================
|
||||
|
||||
# --- WEB-APP ---
|
||||
web-app:
|
||||
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/web-app:${DOCKER_TAG:-latest}"
|
||||
build:
|
||||
context: . # Wichtig: Root Context für Monorepo Zugriff
|
||||
dockerfile: config/docker/caddy/web-app/Dockerfile
|
||||
args:
|
||||
# Frontend spezifisch:
|
||||
CADDY_VERSION: "${DOCKER_CADDY_VERSION:-2.11-alpine}"
|
||||
# Metadaten:
|
||||
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
labels:
|
||||
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||
container_name: "${PROJECT_NAME:-meldestelle}-web-app"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${WEB_APP_PORT:-4000:4000}"
|
||||
environment:
|
||||
# Runtime Configuration — via envsubst in entrypoint.sh in config.json & index.html injiziert.
|
||||
# Muss die öffentlich erreichbare URL sein (Browser-Zugriff!), NICHT localhost.
|
||||
API_BASE_URL: "${WEB_APP_API_URL:-http://localhost:8081}"
|
||||
# Keycloak Public URL (muss vom Browser aus erreichbar sein)
|
||||
KEYCLOAK_URL: "${WEB_APP_KEYCLOAK_URL:-http://localhost:8180}"
|
||||
depends_on:
|
||||
api-gateway:
|
||||
condition: "service_started"
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:4000/" ]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
networks:
|
||||
meldestelle-network:
|
||||
aliases:
|
||||
- "web-app"
|
||||
profiles: [ "gui", "all" ]
|
||||
# web-app:
|
||||
# image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/web-app:${DOCKER_TAG:-latest}"
|
||||
# build:
|
||||
# context: . # Wichtig: Root Context für Monorepo Zugriff
|
||||
# dockerfile: config/docker/caddy/web-app/Dockerfile
|
||||
# args:
|
||||
# # Frontend spezifisch:
|
||||
# CADDY_VERSION: "${DOCKER_CADDY_VERSION:-2.11-alpine}"
|
||||
# # Metadaten:
|
||||
# VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||
# BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||
# labels:
|
||||
# - "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||
# container_name: "${PROJECT_NAME:-meldestelle}-web-app"
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "${WEB_APP_PORT:-4000:4000}"
|
||||
# environment:
|
||||
# # Runtime Configuration — via envsubst in entrypoint.sh in config.json & index.html injiziert.
|
||||
# # Muss die öffentlich erreichbare URL sein (Browser-Zugriff!), NICHT localhost.
|
||||
# API_BASE_URL: "${WEB_APP_API_URL:-http://localhost:8081}"
|
||||
# # Keycloak Public URL (muss vom Browser aus erreichbar sein)
|
||||
# KEYCLOAK_URL: "${WEB_APP_KEYCLOAK_URL:-http://localhost:8180}"
|
||||
# depends_on:
|
||||
# api-gateway:
|
||||
# condition: "service_started"
|
||||
# healthcheck:
|
||||
# test: [ "CMD", "wget", "--spider", "-q", "http://localhost:4000/" ]
|
||||
# interval: 20s
|
||||
# timeout: 5s
|
||||
# retries: 5
|
||||
# start_period: 20s
|
||||
# networks:
|
||||
# meldestelle-network:
|
||||
# aliases:
|
||||
# - "web-app"
|
||||
# profiles: [ "gui", "all" ]
|
||||
|
||||
networks:
|
||||
meldestelle-network:
|
||||
|
||||
+8
-2
@@ -82,13 +82,18 @@ services:
|
||||
restart: unless-stopped
|
||||
profiles: [ "infra", "all" ]
|
||||
environment:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: "${KC_ADMIN_USERNAME:-kc-admin}"
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: "${KC_ADMIN_PASSWORD:-kc-password}"
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: "${KC_BOOTSTRAP_ADMIN_USERNAME:-kc-admin}"
|
||||
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_SCHEMA: "${KC_DB_SCHEMA:-keycloak}"
|
||||
KC_DB_URL: "jdbc:postgresql://postgres:5432/${POSTGRES_DB:-pg-meldestelle-db}"
|
||||
KC_DB_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||
KC_DB_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||
|
||||
# Hostname-Konfiguration: Für lokale Entwicklung "localhost", auf dem Server die echte IP/Domain setzen
|
||||
KC_HOSTNAME: "${KC_HOSTNAME:-localhost}"
|
||||
# WICHTIG: false erlaubt Zugriff über beliebige Hostnamen (nötig für Server-Betrieb ohne TLS)
|
||||
@@ -98,6 +103,7 @@ services:
|
||||
KC_HTTP_ENABLED: "true"
|
||||
# Admin-Interface explizit auf allen Interfaces binden (0.0.0.0)
|
||||
KC_HTTP_MANAGEMENT_PORT: "9000"
|
||||
|
||||
KC_HEALTH_ENABLED: "true"
|
||||
KC_METRICS_ENABLED: "true"
|
||||
# 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
|
||||
status: ACTIVE
|
||||
owner: Lead Architect
|
||||
last_update: 2026-04-11
|
||||
last_update: 2026-04-21
|
||||
---
|
||||
|
||||
# MASTER ROADMAP: Meldestelle
|
||||
|
||||
🏗️ **[Lead Architect]** | 11. April 2026
|
||||
🏗️ **[Lead Architect]** | 20. April 2026
|
||||
|
||||
**Strategisches Ziel:**
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
*Ziel: `competition-context` und `event-management-context` implementieren.*
|
||||
@@ -276,10 +339,18 @@ und über definierte Schnittstellen kommunizieren.
|
||||
|
||||
## 4. Geplante Phasen
|
||||
|
||||
### PHASE 13: Export & ZNS-Rückmeldung
|
||||
*Ziel: Finalisierung der Turnier-Daten und Rückübermittlung an den OEPS.*
|
||||
### PHASE 13: Frontend-Modernisierung & Cleanup ✅ ABGESCHLOSSEN (20. April 2026)
|
||||
*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).
|
||||
* [ ] **ZNS-Portal:** Upload-Integration in das OEPS-ZNS.
|
||||
* [ ] **Archivierung:** Langzeit-Archivierung abgeschlossener Turniere.
|
||||
@@ -307,6 +378,8 @@ und über definierte Schnittstellen kommunizieren.
|
||||
| 15 | Masterdata: Observability & Operations | ✅ | masterdata-ops.md, CHANGELOG |
|
||||
| 16 | Tenant-Resolution: Schema-per-Tenant | ✅ | ADR-0021 |
|
||||
| 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